事务是逻辑上的一组操做,要么都执行,要么都不执行,一荣俱荣,一损俱损。数据库事务有严格的定义,必须知足4个特性。java
原子性:事务是最小的执行单位,不容许分割。事务的原子性确保动做要么所有完成,要么彻底不起做用;
一致性:执行事务先后,数据保持一致,多个事务对同一个数据读取的结果是相同的,事务操做成功先后,数据库所处的状态和它的业务规则是一致的。在这些事务特性中,数据一致性是最终目标,其它特性都是为达到这个目标采起的措施、要求或者手段。mysql
ACID中的一致性和CAP中的一致性有什么区别?面试
两者彻底不是一个事情,数据库对于 ACID 中的一致性的定义是这样的:若是一个事务原子地在一个一致地数据库中独立运行,那么在它执行以后,数据库的状态必定是一致的。对于这个概念,它的第一层意思就是对于数据完整性的约束,包括主键约束、引用约束以及一些约束检查等等,在事务的执行的先后以及过程当中不会违背对数据完整性的约束,全部对数据库写入的操做都应该是合法的,并不能产生不合法的数据状态。而第二层意思实际上是指逻辑上的对于开发者的要求,咱们要在代码中写出正确的事务逻辑,好比银行转帐,事务中的逻辑不可能只扣钱或者只加钱,这是应用层面上对于数据库一致性的要求。即,数据库 ACID 中的一致性对事务的要求不止包含对数据完整性以及合法性的检查,还包含应用层面逻辑的正确。算法
CAP 定理中的数据一致性,实际上是说分布式系统中的各个节点中对于同一数据的拷贝有着相同的值。spring
隔离性:并发访问数据库时,一个用户的事务不被其余事务所干扰,各并发事务之间数据库是独立的,采用数据库锁机制来保证事务的隔离性;
持久性:一个事务被提交以后。它对数据库中数据的改变是持久的,即便数据库发生故障也不该该对其有任何影响。sql
在典型的应用程序中,多个事务并发运行,常常会操做相同的数据来完成各自的任务(多个用户对统一数据进行操做)。并发虽然是必须的,但可能会致使如下的问题:
脏读(Dirtyread):当一个事务正在访问数据而且对数据进行了修改,而这种修改尚未提交到数据库中,这时另一个事务也访问了这个数据,而后使用了这个数据。由于这个数据是尚未提交的数据,那么另一个事务读到的这个数据是“脏数据”,依据“脏数据”所作的操做多是不正确的。
丢失修改(Losttomodify):指在一个事务读取一个数据时,另一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,所以称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread):指在一个事务内屡次读同一数据。在这个事务尚未结束时,另外一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,因为第二个事务的修改致使第一个事务两次读取的数据可能不太同样。这就发生了在一个事务内两次读到的数据是不同的状况,所以称为不可重复读。
幻读(Phantomread):幻读与不可重复读相似。它发生在一个事务(T1)读取了几行数据,接着另外一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些本来不存在的记录,就好像发生了幻觉同样,因此称为幻读。数据库
不可重复度和幻读区别:不可重复读的重点是修改,幻读的重点在于新增或者删除。例1(一样的条件,你读取过的数据,再次读取出来发现值不同了):事务1中的A先生读取本身的工资为1000的操做还没完成,事务2中的B先生就修改了A的工资为2000,致使A再读本身的工资时工资变为2000;这就是不可重复读。
例2(一样的条件,第1次和第2次读出来的记录数不同):假某工资单表中工资大于3000的有4人,事务1读取了全部工资大于3000的人,共查到4条记录,这时事务2又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就致使了幻读。编程
SQL标准定义了四个隔离级别,隔离性和一致性实际上是一个须要开发者去权衡的问题,为数据库提供什么样的隔离性层级也就决定了数据库的性能以及能够达到什么样的一致性。
READ-UNCOMMITTED(读取未提交):最低的隔离级别,容许读取还没有提交的数据变动,可能会致使脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交):容许读取并发事务已经提交的数据,能够阻止脏读,可是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读):对同一字段的屡次读取结果都是一致的,除非数据是被自己事务本身所修改,能够阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化):最高的隔离级别,彻底服从ACID的隔离级别。全部的事务依次逐个执行,这样事务之间就彻底不可能产生干扰,也就是说,该级别能够防止脏读、不可重复读以及幻读。安全
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
表-数据并发问题在不一样隔离级别的出现多线程
这里须要注意的是:与SQL标准不一样的地方在于InnoDB存储引擎在REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-KeyLock锁算法,所以能够避免幻读的产生,这与其余数据库系统(如SQLServer)是不一样的。因此说InnoDB存储引擎的默认支持的隔离级别是REPEATABLE-READ(可重读)已经能够彻底保证事务的隔离性要求,即达到了SQL标准的SERIALIZABLE(可串行化)隔离级别。由于隔离级别越低,事务请求的锁越少,因此大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,可是你要知道的是InnoDB存储引擎默认使用REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB存储引擎在分布式事务的状况下通常会用到SERIALIZABLE(可串行化)隔离级别。
事务的原子性和持久性是由事务日志(transaction log)保证的,回滚日志用于对事务的影响进行撤销,重作日志在错误处理时对已经提交的事务进行重作。它们能保证两点:
UndoLog的原理很简单,为了知足事务的原子性,在操做任何数据以前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog)。而后进行数据的修改。若是出现了错误或者用户执行了ROLLBACK语句,系统能够利用Undo Log中的备份将数据恢复到事务开始以前的状态,UndoLog并不能将数据库物理地恢复到执行语句或者事务以前的样子;它是逻辑日志,当回滚日志被使用时,它只会按照日志逻辑地将数据库中的修改撤销掉看,能够理解为咱们在事务中使用的每一条INSERT都对应了一条DELETE,每一条UPDATE也都对应一条相反的UPDATE语句。 和UndoLog相反,RedoLog记录的是新数据的备份。在事务提交前,只要将RedoLog持久化便可,不须要将数据持久化。当系统崩溃时,虽然数据没有持久化,可是RedoLog已经持久化。系统能够根据RedoLog的内容,将全部数据恢复到最新的状态。 (深刻分析能够参考面向信仰编程-浅入深出MySQL中事务的实现-回滚日志、重作日志)
数据库对于隔离级别的实现就是使用并发控制机制对在同一时间执行的事务进行控制,限制不一样的事务对于同一资源的访问和更新,而最重要也最多见的并发控制机制,主要有锁、时间戳(即乐观锁,并非真正的锁机制,而是一种思想)、多版本MVCC。
Spring为事务管理提供了一致的编程模板,在高层创建了统一的事务抽象。也就是说,无论是选择Spring JDBC、Hibernate、JPA仍是选择MyBatis,Spring均可以让用户使用统一的编程模型进行事务管理。
Spring为事务管理提供了一致的编程模板,在高层次创建了统一的事务抽象。像Spring DAO为不一样的持久化技术实现提供模板类同样,Spring事务管理继承了这一风格,也提供了事务模板类TransactionTemplate。经过TransactionTemplate并配合使用事务回调TransactionCallback指定具体的持久化操做就能够经过编程方式实现事务管理,而无须关注资源获取、复用、释放、事务同步和异常处理的操做。
在Spring事务管理SPI的抽象层主要包括3个接口,分别是PlatformTransactionManager、TransactionDefinition和TransactionStatus,它们位于org.springframework.transaction包中。3者关系如图:
图-Spring事务管理SPI抽象
其中,TransactionDefinition用于描述事务的隔离级别,超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性。这些事务属性能够经过XML配置、注解描述或手工编程的方式设置。PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息建立事务,并用TransactionStatus描述这个激活事务的状态。
事务隔离:TransactionDefinition使用了java.sql.Connection接口中同名的4个隔离级别,此外,TransactionDefinition还定义了一个默认的隔离级别,它表示使用底层数据库的默认隔离级别。
事务传播:一般在一个事务中执行的全部代码都会同一事务的上下文中。可是Spring也提供了几个可选的事务传播类型,例如简单地参与到现有的事务中,或者挂起当前的事务,建立一个新事务。
事务超时:事务在超时前能运行多久,超过期间后,事务被回滚。有些事务管理器不支持事务过时的功能,这时若是设置TIMEOUT_DEFAULT等值时将抛出异常。
只读状态:只读事务不修改任何数据,主要用于优化,若是更改数据就会抛出异常。
spring容许经过XML或者注解元数据的方式为一个有事务要求的服务类方法配置事务属性,这些信息做为Spring事务管理框架的输入,Spring将自动按照事务属性信息的指示,为目标方法提供相应的事务支持。
TransactionStatus表明一个事务的具体运行状态,事务管理器经过该接口获取事务的运行期状态信息,也能够经过该接口间接地回滚事务,它相比于在抛出异常时回滚事务的方式更具备可控性。
PlatformTransactionManager是事务的最高层抽象,它提供了3个接口方法:
TransactionStatus getTransaction(TransactionDefinition definition):该方法根据事务定义信息从事务环境中返回一个已存在的事务,或者建立一个新的事务,并用TransactionStatus描述这个事务的状态。
commit(TransactionStatus status):根据事务的状态提交事务,若是事务状态已经被标识为rollback-only,该方法将执行一个回滚事务的操做。
rollback(TransactionStatus status):回滚事务,当提交事务抛出异常时,回滚会被隐式执行。
Spring将事务管理委托给底层具体的持久化实现框架完成,所以Spring为不一样的持久化框架提供了PlatformTransactionManager接口的实现类,以下图:
图-不一样持久化技术对应的事务管理器实现类
这些事务管理器都是对特定事务实现框架的代理,这样咱们就能够经过spring的高级抽象,对不一样种类的事务实现使用相同的方式进行管理,而不用关心具体的实现。要实现事务管理,首先要在Spring中配置好相应的事务管理器,为事务管理器指定数据资源及一些其它事务管理控制属性。
Spring将JDBC的Connection、Hibernate的Session等访问数据库的链接或会话对象统称为资源。这些资源在同一时刻是不能多线程共享的,为了让DAO、Service能作到singleton,Spring的事务同步管理器类org.springframework.transaction.support.TransactionSynchronizationManager使用ThreadLocal为不一样事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。
在一个service接口中可能会调用另外一个service接口的方法,以共同完成一个完整的业务操做,Spring经过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。Spring在TransactionDefinition接口中规定了7种类型的事务传播行为,以下图
图-事务传播行为类型
Spring为编程式事务管理提供了模板类org.springframework.transaction.support.TransactionTemplate,和那些持久化模板类同样,TransactionTemplate也是线程安全的。TransactionTemplate有2个重要的方法:
// 设置事务管理器 void setTransactionManager(PlatformTransactionManager transactionManager) // 在TransactionCallback回调接口中定义须要以事务的方式组织的数据访问逻辑 Object execute(TransactionCallback action) TransactionCallback接口只有一个方法:Object doInTransaction(TransactionStatus status)。若是操做不会返回结果,可使用TransactionCallback的子接口TransactionCallbackWithoutResult。
Spring Boot中使用@Transactional注解配置事务管理
是否用了Spring,就必定要用Spring事务管理器,不然就没法进行数据的持久化操做呢?答案是否认的,脱离了事务性,DAO照样能够顺利地进行数据操做。对于强调速度的应用,数据库自己可能就不支持事务,如MyISAM引擎的数据库,这是无需配置事务管理器,即便配置了,也是没有实际用处的。
将面向接口编程奉为圭臬,过度强制面向接口编程除了会带来更多的的类文件,并不会有什么好处。Spring事务管理支持在Controller层直接添加事务注解并生效,所以事务管理并不必定强制应用必须严格分层,能够根据实际应用出发,根据实际须要进行编程。
除了事务的传播行为,对于事务的其它特性,Spring是借助底层资源的功能来完成的,无非充当了一个代理的角色。可是Spring事务的传播行为倒是Spring凭借自身的框架提供的功能。
例如对于调用链Service1#method1()->Service2#method2()->Service3#method3(),那么这三个方法经过Spring的事务传播机制均可以工做在同一个事务中。
首先调用的是AOP代理对象而不是目标对象,首先执行事务切面,事务切面内部经过TransactionInterceptor环绕加强进行事务的加强,即进入目标方法以前开启事务,退出目标方法时提交/回滚事务。目标对象内部的自我调用将没法实施切面中的加强。
public interface AService { public void a(); public void b(); } @Service() public class AServiceImpl1 implements AService{ @Transactional(propagation = Propagation.REQUIRED) public void a() { // 此处的this指向目标对象,所以调用this.b()将不会执行b事务切面,即不会执行事务加强(经过日志能够观察到) this.b(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void b() { } }
如何解决?
方法一:调用AOP代理对象的b方法便可执行事务切面进行事务加强
须要开启暴露Aop代理到ThreadLocal支持,并将this.b()修改成((AService) AopContext.currentProxy()).b();
这种经过ThreadLocal暴露Aop代理对象适合解决全部场景(无论是singleton Bean仍是prototype Bean)的AOP代理获取问题(即能解决目标对象的自我调用问题);
方法二:经过初始化方法在目标对象中注入代理对象
这种方式不是很灵活,全部须要自我调用的实现类必须重复实现代码
@Service public class AServiceImpl3 implements AService{ @Autowired //① 注入上下文 private ApplicationContext context; private AService proxySelf; //② 表示代理对象,不是目标对象 @PostConstruct //③ 初始化方法 private void setSelf() { //从上下文获取代理对象(若是经过proxtSelf=this是不对的,this是目标对象) //此种方法不适合于prototype Bean,由于每次getBean返回一个新的Bean proxySelf = context.getBean(AService.class); } @Transactional(propagation = Propagation.REQUIRED) public void a() { proxySelf.b(); //④ 调用代理对象的方法 这样能够执行事务切面 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void b() { } }
方法三:经过BeanPostProcessor在目标对象中注入代理对象
须要注意到循环依赖和非singleton bean的影响,如下方式能解决singleton之间的循环依赖问题,可是不能解决循环依赖中包含prototype Bean的自我调用问题。
// 即咱们自定义的BeanPostProcessor (InjectBeanSelfProcessor) // 若是发现咱们的Bean是实现了该标识接口就调用setSelf注入代理对象。 public interface BeanSelfAware { void setSelf(Object proxyBean); } @Component public class InjectBeanSelfProcessor implements BeanPostProcessor, ApplicationContextAware { private ApplicationContext context; //① 注入ApplicationContext public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if(!(bean instanceof BeanSelfAware)) { //② 若是Bean没有实现BeanSelfAware标识接口 跳过 return bean; } if(AopUtils.isAopProxy(bean)) { //③ 若是当前对象是AOP代理对象,直接注入 ((BeanSelfAware) bean).setSelf(bean); } else { //④ 若是当前对象不是AOP代理,则经过context.getBean(beanName)获取代理对象并注入 //此种方式不适合解决prototype Bean的代理对象注入 ((BeanSelfAware)bean).setSelf(context.getBean(beanName)); } return bean; } public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } }
扩展:灵活使用事务注解并不是必定须要接口
若是在一个业务方法中包含查询、修改等逻辑,但其实只有这些修改操做须要实施业务加强,这是应该怎么处理?可让改service实现BeanSelfAware接口,而后在service注入代理对象,经过代理对象
@Service public class AServiceImpl1 implements AService,BeanSelfAware{ private AServiceImpl1 self; @Override public void setProxy(AServiceImpl1 proxy) { this.self = proxy; } @Override public void a() { // ... 其它查询等无需事务逻辑 // 须要事务加强的修改逻辑 self.m(); } @Transactional public void m() { } }
在相同线程中进行相互的嵌套调用的事务方法工做在相同的事务中,若是这些相互嵌套调用的方法工做在不一样线程中,则不一样的线程下的事务方法工做在独立的事务中。
若是用户采用了一种高端的ORM技术(Hibernate、JPA、JDO),同时还采用了一种JDBC技术(Spring JDBC、Mybatis),因为前者的会话Session是对后者链接Connection的封装,Spring会足够智能地在同一个事务线程中让前者的会话封装后者的链接,因此只要只要直接采用前者前者的事务管理器就能够了。
图-混合数据访问技术所对应的事务管理器
因为Spring事务管理是基于接口代理或者动态字节码技术,经过AOP实施事务加强的,虽然Spring依然支持AspectJ在类的加载期间实施加强,但这种方法不多使用,这里不作讨论。
对于基于接口动态代理的AOP事务加强来讲,因为接口的方法都必须是public的,这就要求实现类的实现方法也必须是public的(不能是protected、private)的,同时不能使用static修饰符。因此,能够实施接口动态代理的方法只能是public或public final修饰符的方法,其它方法都不能被动态代理,相应地也就不能实施AOP加强,换句话说即不能进行Spring事务的加强。
基于CGLib字节码动态代理的方案是经过扩展被加强类,动态建立起子类的方式进行AOP加强植入的,因为使用final、static、private修饰符的方法都不能被子类覆盖,相应地这些方法没法实施AOP加强。因此方法签名必须特别注意这些修饰符的使用,以避免成为事务管理的漏网之鱼。
须要注意的是,咱们说这些方法不能被Spring进行AOP事务加强,是指这些方法不能启动事务,可是外层方法的事务上下文依旧能够顺利地传播到这些方法中。这些不能被事务加强的方法和可被事务加强的方法的惟一区别在于“是否能够主动开启一个新事务”,前者能够然后者不能够,对于事务传播行为来讲,两者是彻底相同的。
图-Spring事务管理漏网之鱼
参考资料