1. Spring数据库事务管理器的设计
在Spring中数据库事务是通过PlatformTransactionManager进行管理的。TransactionTemplate源码:

事务的创建、提交和回滚都是通过PlatformTransactionManager接口来完成,当事务产生异常时会回滚事务。在默认的实现中所有的异常都会回滚,我们可以通过配置去修改在某些异常发生时回滚或者不回滚事务。当无异常时会提交事务。
PlatformTransactionManager接口的源码:
1.配置事务管理器
这里先引入了 XML 的命名空间,然后定义了数据库连接池,于是使用了
DataSourceTransactionManager去定义数据库事务管理器,并且注入了数据库连接池。这样,Spring就知道已经将数据库事务委托给事务管理器transactionManger管理。
在Spring中可以使用声明式事务或编程式事务,如今编程式事务几乎不用,因为其会产生冗余,代码可读性差。声明式事务分为xml配置和注解事务,但是xml方法也已经不常用了,目前主流的方法是注解@Transaction。
2.用java配置方法实现Spring数据库事务
用java配置的方式实现Spring数据库事务,需要在配置类中实现接口TransactionManagementConfigurer的annotationDrivenTransactionManager方法。Spring将annotationDrivenTransactionManager方法返回的事务管理器作为程序中的事务管理器。


加粗的代码实现了TransactionManagermentConfigurer接口所定义的方法annotationDrivenTransactionManager,并且我们使用DataSourceTransactionManager去定义数据库事务管理器的实例,再将数据源设置给它。使用注解@EnableTransactionManagement后,在Spring上下文中使用事务注解@Transaction,Spring会知道使用该数据库事务管理器管理事务了。
2. 编程式事务
编程式事务以代码的方式管理事务,事务由开发者通过自己的代码实现,这里需要使用一个事务定义类接口TransactionDefinition。编程式事务的代码如下:
注意加粗的代码,可以看到所有的事务都是由开发者自己进行控制的,由于事务已经交由事务管理器管理,所以jdbcTemplate本身的数据库资源已经由事务管理器管理,因此当执行完insert语句时不会自动提交事务,这时需要使用事务管理器的commit方法,回滚事务需要使用rollback方法。
3.声明式事务
编程式事务是一种约定型事务,大部分情况下,当使用数据库事务时,大部分场景在代码中发生了异常时,需要回滚事务,而不发生异常时则提交事务,从而保证数据库数据的一致性。
从这点出发,Spring给了一个约定,如果使用的是声明式事务,则当你的业务方法不发生异常,Spring会让事务管理器提交事务,而发生异常则让事务管理器回滚事务。
1、 Transactional的配置项
Transactional配置项:

value、transactionManager、timeout、readOnly、rollbackFor、rollbackForClassName、noRollbackFor和noRollbackForClassName都是容易理解的,这些属性将会被Spring放到事务定义类TransactionDefinition中,事务声明器的配置内容也是以这为主。使用声明式事务需要配置注解驱动:
- 使用xml进行配置事务管理器
配置事务拦截器:

配置 transactionAttributes 的内容是需要关注的重点,Spring IoC 启动时会解析这些内容,放到事务定义类 TransactionDefinition中, 再运行时会根据正则式的匹配度决定方法采取哪种策略。这使用了拦截器和SpringAOP。
指明事务拦截器拦截哪些类:
BeanName属性告诉Spring如何拦截类,由于声明为*ServiceImpl,所有关于Service是现实类都会被其拦截,然后interceptorNames这是定义事务拦截器,这样对应的类和方法就会被事务拦截器所拦截。
3.事务定义器
从注解@Transactional或者xml中可看到事务定义器的身影。事务定义器源码:
4.声明式事务的约定流程
@Transaction注解可以使用在方法或者类上,在SpringIoC容器初始化时,Spring会读入这个注解或者xml配置的事务信息,并且保存到一个事务定义类里面。
首先Spring通过事务管理器创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置。根据传播行为配置采取一种特定的策略。
Spring会通过反射的方法调度开发者的业务代码,但是反射的截获是正常返回或者产生异常返回,则它给的约定只要发生异常且符合事务定义类回滚条件,Spring就会将数据库事务回滚,否则将数据库事务提交。
声明式事务的流程:
如插入角色代码:
只看到了注解@Transactional,其配置了Propagation。REQUIRED的传播行为,这意味着当别的方法调度时,如果存在事务就沿用下来,如果不存在事务就开启新的事务。
4. 数据库相关知识
1、数据库事务ACID特性
数据库事务正确执行的4个基础要素是原子性(Atomicity )、一致性(Consistency )、隔离性 (lsolation )和持久性(Durability)。
- 原子性:整个事务的所有操作,要么全完成,要么全部不完成,不能停留在中间的某个过程。事务在执行过程中发生错误会被回滚到事务开始之前的状态。
- 一致性:一个事务可以改变封装状态,事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
- 隔离性:指两个事务之间的隔离程度。
- 持久性:在事务完成后,该事务对数据库所做的改变会持久保存在数据库中不会被回滚。
2、丢失更新
在互联网中存在着抢购、秒杀等高并发场景,使得数据库在一个多事务的环境中运行,多个事务的井发会产生一系列的问题,主要的问题之一就是丢失更新,一般而言存在两类丢失更新。
第一类丢失:假设有两个事务并发,一个回滚,一个提交成功导致结果不一致。如下表所示:
第二类丢失更新:
由于在不同的事务中,无法探知其他事务的操作,导致两者提交后,余额都是9000,实际正确的为8000。
3、隔离级别
按照SQL的标准规范,将隔离级别定义为4层,分别是:脏读、读\写提交、可重复读、序列化。
脏读:允许一个事务读取另一个事务中未提交的数据,如下面例子。
在T3时刻事务二启动了消费,但是并没有提交,事务一在T4时刻消费,由于脏读,所以能够读取事务二的内容,此时事务一读取的金额是事务二操作后未提交的剩余金额。在T5时刻事务一提交,余额为8000,事务二在T6时刻回滚事务,余额为8000,但是是错误的,因为在T4时刻事务一读取了事务二未提交的事务。
第二层隔离级别:
读\写提交:读写提交就是一个事务只能读取另一个事务已经提交的数据,例如下面的例子。
T3时刻,事务二使用读写提交的隔离级别,则事务一无法读取事务二的内容,只能等事务二提交之后才可以访问事务二,因此在T3时刻事务一读取的金额还是10000,事务一在T5时刻提交事务后,事务二在T6时刻回滚事务,此时的余额为9000。
第三层隔离级别:
不可重复读:
在T7时刻事务一知道事务二的提交结果,余额1000元,但是在T4时刻事务一使用2000元,在T7时刻事务一提交事务时发现余额不足,但是事务一并不知道事务二所进行的事务操作,金额从10000变为1000,对于事务一,余额不能重复读取,而是一个会变化的值,这样的场景称为不可重复读。
第四层隔离级别:
幻读:
在T1时刻,事务二读到10条信息,在T4时刻打印记录时,并不知道事务一在T2和T3时刻进行消费,造成多出一条消费记录的生成,但是事务二不知道该条记录是不是幻读出的,因此称为幻读。
第五层隔离级别:
序列化:
是一种让SQL按照顺序读\写的方法,能够消除数据库事务之间并发生成数据不一致的问题。
各类隔离级别和产生的现象:
5. 选择隔离级别和传播行为
1、选择隔离级别
互联网应用中,需要考虑数据库数据的一致性和系统的性能。从脏读到序列化的系统性能直线下降,因此设置高的隔离级别会对系统并发造成压制,从而引起大量的线程挂起,直到获得锁才能进一步操作,恢复时需要大量的等待时间。在大部分场景下,选择读写提交的方式设置事务,这样就有助于提高并发,压制脏读。使用读写提交隔离级别:
当业务并发量不是很大或根本不需要考虑的情况下,使用序列化隔离级别用来保证数据的一致性。隔离级别需要根据并发的大小和性能来进行决定,对于并发不大又要保证数据安全性的可以使用序列化的隔离级别,这样就可以保证数据库在多事务环境中的一致性。
使用序列化隔离级别:
该类方法在高并发情况下不适用。实际工作中,注解@Transactional的隔离级别的默认值为Isolation.DEFAULT,随着数据库默认值的变化而变化。对于不同的数据库,隔离级别的支持是不同的。
2、传播行为
传播行为是指方法之间的调用事务策略的问题。一般情况下都希望事务能够同时成功或者同时失败。
但是还有其他的情况,例如信用卡的还款,当只有一条事务,则当调用RepaymentService的repay方法对某张信用卡进行还款,当出现异常,如果对这条事务进行回滚则造成所有的数据操作都会被回滚,已经正常还款的也会还款失败。当batch方法调用repay方法时,为repay方法创建一条新的事务。当这个方法产生异常,只会回滚自身的事务不会影响主事务和其他事务。
当通过batch方法去调度repay方法时能产生一条新事务,去处理一个信用卡还款。当这张卡还款异常则会回滚该条新事务,不回滚主事务。可以对事务的特性进行传播配置,称为传播行为。在Spring中传播行为类型是通过一个枚举类型去定义。信用卡还款的调用设计:
Spring的7种传播方式:
6. 在Spring+MyBatis组合中使用事务
需要创建如下文件:
首先配置Spring+MyBatis的测试环境:



创建POJO类:
搭建MyBatis的映射文件,建立SQL和POJO的关系:
搭建MyBatis的RoleMapper.xml:
RoleMapper的接口:

为了引入该映射器,需要配置一个MyBatis的配置文件Mybatis-config.xml:
随后配置服务类(Service),对于服务类,在开发的过程中一般都坚持“接口+实现类”规则,这有利于实现类变化。操作角色的两个接口:
RoleService接口的insertRole方法可以对单个角色进行插入,而RoleListService的insertRoleList可以对角色列表进行插入。insertRoleList方法会调用insertRole。两个接口的实现类:


以上代码中有两个服务实现类方法标注@Transactional注解,这样他们会在对应的隔离级别和传播行为中运行。因为在insertRole方法中标注了:
所以当insertRoleList方法调度了insetRole方法时就会产生一个新的事务。
使用log4j输出日志:
对隔离级别和传播行为进行测试:

这里插入了两个角色,由于insertRoleList会调用insertRole,而insertRole标注了REQUIRES_NEW,所以每次调用产生新事务。
7. @Transactional的自调用失效问题
有时候配置了注解@Transactional,但它会失效。注解@Transactional的底层实现是SpringAOP技术,而SpringAOP使用动态代理,意味着对于静态方法和非public方法,注解@Transactional是失效的。还有一个在使用过程中极其易犯的错误-自调用:一个类的一个方法去调用自身另外一个方法的过程。
对RoleServiceImpl代码进行修改:

出现自调用失效的根本原因是在于AOP的实现原理,因为@Transactional的实现原理是AOP,而AOP是动态代理的,在上面的程序中使用的是自身调用自身的过程,并不是代理对象的调用,这样就不会产生AOP去设置@Transactional配置的参数,这样就出现自调用注解失效问题。
解决该问题可以使用两个服务类,SpringIoC容器中生成RoleService代理对象,这样就可以使用AOP,且不会出现自调用问题。或者也可以直接从容器获取RoleService的代理对象。
获取RoleService代理对象,克服自调用问题:
