事务基本概念
数据库事务是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分隔的工作单位。主要包括四大特性:
- 原子性(Atomicity):事务是数据库的逻辑工作单位,事务中包括的多个操作要么都做,要么都不做。
- 举例:B向A转账100元,A账户余额增加,B账户余额扣减,两步操作属于一个事务,要么全执行,要么全不执行。比如遇到B账户由于余额不足扣减失败未执行的时候,已经执行的A账户余额增加操作也需要回滚。
- 一致性(Consistency):事务执行的结果必须是使数据库从一个一致性状态变为另外一个一致性状态。
- 举例:B向A转账100,转账之前A余额500,B余额300,转账后A余额600,B余额200,被认为数据库从一个一致性状态变为了另外一个一致性状态,如果出现A增加了,B未扣减,数据库就会凭空多出100,这时数据库就处于不一致状态。和原子性类似,实际上一致性特性是由其他三个特性共同保证的,其他三个特性的目标也是为一致性服务。
- 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务的内部操作及使用的数据对其他并发事务是隔离的。
- 举例:数据库火车票剩余5张,T1、T2售票点同时卖出1张票。T1读出剩余票=5,T2读出剩余票=5,两个售票点分别执行 5-1=4,并写回数据库,最终数据库中火车票剩余4张;明明卖出两张但数据库只减少了1张,这就是多个事务并发带来的数据库不一致问题。并发操作带来的数据不一致还包括丢失修改、不可重复读、脏读等情况。隔离性的目标就是有针对性的解决这些问题确保数据的一致性。
- 持久性(Durability):也称永久性,指一个事务一旦提交,其对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其执行结果由任何影响。核心就是针对已提交的事务,如何将数据持久化到磁盘上确保操作结果不丢。
数据库事务原理
事务本质
数据库事务管理的核心是保证ACID特性,事务特性可能遭到破坏的因素包括
- 事务在运行过程中被强行停止
- 多个事务并行,不同事务的操作交叉执行(并行事务轮流运行)
为了应对这两种情况数据库提供了恢复和并发控制机制以确保强制终止事务对数据库和其他事务没有任何影响,并行事务下数据的一致性。
事务保障机制
恢复机制
恢复机制通过保证在事务中断情况下对已执行操作能回滚、异常情况下对已提交数据能恢复;从而确保事务的A(原子性)、D(持久性)特性。
相关机制不同数据库在实现上存在差异,这里主要以Mysql为例说明相关原理
Mysql包含三个关键日志,Server层的binlog(归档日志),引擎层的redo log(重做日志)和undo log(回滚日志),binlog和redo log是物理日志,undo log是逻辑日志。由于Mysql的事务是在引擎层实现的,所以这里不过多赘述Server层内容。
出于性能考虑Mysql通常不会每一次更新操作都写磁盘,而是采用WAL( Write-Ahead Logging)技术,其关键点是先写日志,再写磁盘,将磁盘的随机写变为顺序写;具体就是当有一条记录需要更新的时候,InnoDB 会先把记录写到redo log,并更新内存,更新操作完成;同时InnoDB会在适当的时候,将这个操作记录从内存更新到磁盘。如果当数据库返回成功(事务提交),数据没有写回磁盘的情况下数据库崩溃,重启后内存中的数据就会丢失,数据库会通过redo log的事务提交记录对数据进行恢复,确保数据的持久性。
同时,当对数据库记录做增删改操作时,都会产生undo记录,undo log通过链表形式记录了每条数据的多个版本及生成每个版本对应的事务ID;当事务回滚或数据库崩溃时,利用undo log撤销未提交事务对数据库产生的影响,确保了事务执行的原子性。
并发控制机制
数据库是一个共享资源,为了充分利用系统资源,数据库允许多个事务并行执行。但由于多个事务是交叉执行(并行事务轮流运行),如果没有相关机制进行有效控制,则可能出现由于并发导致的数据不一致情况,比如:
- 丢失修改:T1和T2读入同一数据并修改,T2提交的结果覆盖了T1修改的数据,比如上面提到的火车票的情况。
- 不可重复读:T1读取数据后,T2执行更新操作,使T1无法再现前一次读取结果;具体表现为以下三种情况 :
- T1读取数据后,T2将数据修改为其他值,T1再次读取结果不一致
- T1读取数据后,T2删除了某些数据,T1再次读取,部分数据不见
- T1读取数据后,T2新增了某些数据,T1再次读取,结果集变多
后两种情况通常也叫幻读
- 脏读:T1修改某一数据后未提交事务,T2读取到T1修改后的数据,T1进行回滚,将修改后的值恢复,这个时候T2读取到的就是脏数据,即不正确的数据
产生这些不一致性的主要原因是并发操作破坏了事务的隔离性,数据库通过定义事务隔离级来保障事务的隔离性。
事务隔离级别
不同的数据库会定义不同的隔离级别,对于InnoDB来说遵循SQL92标准定义了四种事物隔离级别:
- 读未提交:一个事务未提交,它做的变更就能被别的事务看到
- 读提交:一个事务提交后,它做的变更才会被其他事务看到
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的
- 串行化:当出现多个事务访问同一数据冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
下面通过一个例子说明在不同隔离级下数据的差异性;当两个事务按如下顺序执行
图中红色标注的v1、v2、v3的值在不同的隔离级下值会有所不同,具体如下:
- 读未提交:T1可以读到T2未提交的内容,所以v1=8、v2=8、v3=8
- 读提交:只有提交后的内容才会被其他事务看到,所以v1=5、v2=8、v3=8 -> 解决脏读
- 可重复读:遵循事务是执行期间看到的数据前后是一致的,所以v1=5、v2=5、v3=5 -> 解决脏读、不可重复读
- 串行化:T2在执行update时会产生等待,直到T1提交后才能执行,所以v1=5、v2=5、v3=8 -> 解决脏读、不可重复读、幻读
四种隔离级逐步严格,隔离越严,效率就会越低。事务隔离级的实现主要是基于锁和MVCC,当然也有部分数据库只用了锁
MVCC
MVCC即多版本并发控制,是一种无锁并发控制技术,避免了用锁导致的读写阻塞、死锁等问题;
前面提到undo log记录了每条数据的多个版本,每个版本记录了包括指向上一个版本的指针、生成当前版本的事务ID以及对应版本的数据
在事务启动并执行读操作的时候,数据库会给每个事务生成单独的readview(一致性视图),视图记录了当前数据库活跃的事务ID列表、最小活跃事务ID和最大事务ID+1
事务可以看到的数据版本依赖数据库定义的可见性规则,如下:
- 比较当前版本trx_id 如果小于 m_up_limit_id,则当前事务能看到trx_id所在的记录,如果大于进入下一个判断
- 如果trx_id >= m_low_limit_id则代表trx_id所在记录在readview生成后才出现,则对当前事务不可见,如果小于进行下一个判断
- trx_id是否在活跃事务中,如果在,则代表readview生成时刻,这个事务处于活跃状态,还没有commit,修改的数据,当前事务看不到,如果不在,则说明这个事务在readview生成之前就已经commit,则修改的结果能够看到
所以基于undolog、readview和可见性规则就实现了MVCC确保并发事务读写间的隔离性,程度在于readview生成的时机,读提交是事务中每次执行快照读都会基于当前数据库情况重新生成一个readview,不可重复读是事务启动后第一次快照读生成readview,后续该事务中的所有快照读操作都基于这个readview进行判断。
锁
锁是数据库实现并发控制的重要技术,所谓锁就是事务在对某个数据对象,比如表或者记录操作之前,先向系统发出请求,对其加锁,加锁后事务就对该数据对象有了一定的控制,在该事务释放它之前,其他事务不能更新此数据对象。
锁的类型分共享锁和排他锁,也叫读锁和写锁,读锁和读锁间不互斥,读锁和写锁、写锁和写锁互斥,即多个事务可以同时给同一个数据对象加读锁,但不能加写锁。事务给某个数据对象加写锁后,其他事务不能再对其加任何锁。
介绍完数据库的事务原理后下面介绍一下在开发过程中如何使用事务;不管是jdbc、hibernate、mybatis,他们的事务都是提供了一种事务管理的工具,本质上依赖的还是数据库的事务功能。
JDBC Transation
JDK对数据库操作提供了事务API的支持,核心类包括java.sql.Connection及java.sql.SavePoint,其中SavePoint是jdbc面对一个事务包含一组复杂语句,需要按需回滚,即回滚到事务中某个指定点时提供的一种机制;注意当把事务回滚到一个保存点,会使其他所有保存点自动释放并变为无效。
Connectionconn=openConnection(); try { // 设置数据库事务隔离级,包括// TRANSACTION_READ_UNCOMMITTED-读未提交// TRANSACTION_READ_COMMITTED-读提交// TRANSACTION_REPEATABLE_READ-可重复读// TRANSACTION_SERIALIZABLE-串行化conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // true(默认):所有的SQL语句都将作为单个事务执行和提交// false:多个SQL语句在事务中被分组,并通过调用commit或rollback终止conn.setAutoCommit(false); delete(); // 在当前事务中创建指定名称或ID的保存点,并返回表示该保存点的新保存点对象Savepointsavepoint1=conn.setSavepoint("保存点1"); insert(); update(); if(select() ==null){ // 撤消设置给定保存点对象后所做的所有更改conn.rollback(savepoint1); } // 使自上次提交/回滚以来所做的所有更改永久化,并释放此连接对象当前持有的所有数据库锁conn.commit(); } catch (SQLExceptione) { // 撤消当前事务中所做的所有更改,并释放此连接对象当前持有的所有数据库锁conn.rollback(); } finally { if(conn!=null){ conn.close(); } }
Connection本身不具备事务的能力,只是将命令发送给数据库,使用了数据库本身的事务功能。
为了简化数据访问层逻辑,通常不会直接使用jdbc,而是使用hibernate、ibatis等框架,不同的框架实现事务功能的方式不尽相同,所以在Spring中进行了统一抽象,为应用代码使用不同厂商的数据库和框架提供了一致的事务模型。
Spring Transation
核心接口
TransactionManager(统一事务管理)
Spring对不同厂商对事务的处理进行了统一的抽象,即PlatformTransactionManager;它定义了事务的创建(getTransaction:获取当前激活的事务或者创建一个事务)、提交(commit)及回滚(rollback)操作标准,不同厂商通过实现它扩展自己的事务处理功能。
AbstractPlatformTransactionManager实现了PlatformTransactionManager,是实现Spring标准事务工作流的抽象基类,它定义了事务实现的模板方法(开始、挂起、恢复、提交、回滚)及事务同步处理。
DataSourceTransactionManager、JtaTransactionManager即不同厂商对Spring统一事务抽象的实现,其他的还包括HibernateTransactionManager、JpaTransactionManager等。
TransactionDefinition(事务属性)
定义与Spring兼容的事务属性接口,包括事务的隔离级别、事务的传播属性、超时时间设置、是否只读等。其中事务隔离级是数据库的功能,事务传播属性是Spring提供的功能。
事务的传播属性
事务的传播行为一般发生在事务嵌套的场景中,比如一个有事务的方法内调用了另外一个有事务的方法时,事务如何在方法间传播。Spring定义了七种传播行为:
传播属性 |
说明 |
REQUIRED(默认) |
如果当前存在事务,则使用当前事务,不存在则新建事务 |
REQUIRED_NEW |
如果当前存在事务,则挂起当前事务,创建新的事务 |
SUPPORTS |
如果当前存在事务,则使用当前事务,如果不存在,则不使用事务 |
NOT_SUPPORTED |
如果当前存在事务,则挂起当前事务,使用非事务方式执行 |
MANDATORY |
如果当前不存在事务,则抛出异常 |
NEVER |
如果当前存在事务,则抛出异常 |
NESTED |
如果当前存在事务,创建一个带有保存点的嵌套事务,如果当前不存在事务,则创建一个新的事务 |
下面是一个事务方法被调用时,当前是否存在事务的两种情况,源码在AbstractPlatformTransactionManager.getTransaction中,执行逻辑和上图说明一致,不再赘述;其中Nested是基于SavePoint机制实现,RequiredNew和Nested的区别在于内部事务执行完,外部事务回滚,RequiredNew不影响内部事务结果,Nested会一起回滚。
if (definition.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_NEVER) { thrownewIllegalTransactionStateException( "Existing transaction found for transaction marked with propagation 'never'"); } if (definition.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { if (debugEnabled) { logger.debug("Suspending current transaction"); } ObjectsuspendedResources=suspend(transaction); booleannewSynchronization= (getTransactionSynchronization() ==SYNCHRONIZATION_ALWAYS); returnprepareTransactionStatus( definition, null, false, newSynchronization, debugEnabled, suspendedResources); } if (definition.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_REQUIRES_NEW) { if (debugEnabled) { logger.debug("Suspending current transaction, creating new transaction with name ["+definition.getName() +"]"); } SuspendedResourcesHoldersuspendedResources=suspend(transaction); try { returnstartTransaction(definition, transaction, debugEnabled, suspendedResources); } catch (RuntimeException|ErrorbeginEx) { resumeAfterBeginException(transaction, suspendedResources, beginEx); throwbeginEx; } } if (definition.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_NESTED) { if (!isNestedTransactionAllowed()) { thrownewNestedTransactionNotSupportedException( "Transaction manager does not allow nested transactions by default - "+"specify 'nestedTransactionAllowed' property with value 'true'"); } if (debugEnabled) { logger.debug("Creating nested transaction with name ["+definition.getName() +"]"); } if (useSavepointForNestedTransaction()) { // Create savepoint within existing Spring-managed transaction,// through the SavepointManager API implemented by TransactionStatus.// Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.DefaultTransactionStatusstatus=prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); status.createAndHoldSavepoint(); returnstatus; } else { // Nested transaction through nested begin and commit/rollback calls.// Usually only for JTA: Spring synchronization might get activated here// in case of a pre-existing JTA transaction.returnstartTransaction(definition, transaction, debugEnabled, null); } } // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.if (debugEnabled) { logger.debug("Participating in existing transaction"); } if (isValidateExistingTransaction()) { if (definition.getIsolationLevel() !=TransactionDefinition.ISOLATION_DEFAULT) { IntegercurrentIsolationLevel=TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); if (currentIsolationLevel==null||currentIsolationLevel!=definition.getIsolationLevel()) { ConstantsisoConstants=DefaultTransactionDefinition.constants; thrownewIllegalTransactionStateException("Participating transaction with definition ["+definition+"] specifies isolation level which is incompatible with existing transaction: "+ (currentIsolationLevel!=null?isoConstants.toCode(currentIsolationLevel, DefaultTransactionDefinition.PREFIX_ISOLATION) : "(unknown)")); } } if (!definition.isReadOnly()) { if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { thrownewIllegalTransactionStateException("Participating transaction with definition ["+definition+"] is not marked as read-only but existing transaction is"); } } } booleannewSynchronization= (getTransactionSynchronization() !=SYNCHRONIZATION_NEVER); returnprepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); // No existing transaction found -> check propagation behavior to find out how to proceed.if (def.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_MANDATORY) { thrownewIllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); } elseif (def.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_REQUIRED||def.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_REQUIRES_NEW||def.getPropagationBehavior() ==TransactionDefinition.PROPAGATION_NESTED) { SuspendedResourcesHoldersuspendedResources=suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name ["+def.getName() +"]: "+def); } try { returnstartTransaction(def, transaction, debugEnabled, suspendedResources); } catch (RuntimeException|Errorex) { resume(null, suspendedResources); throwex; } } else { // Create "empty" transaction: no actual transaction, but potentially synchronization.if (def.getIsolationLevel() !=TransactionDefinition.ISOLATION_DEFAULT&&logger.isWarnEnabled()) { logger.warn("Custom isolation level specified but no actual transaction initiated; "+"isolation level will effectively be ignored: "+def); } booleannewSynchronization= (getTransactionSynchronization() ==SYNCHRONIZATION_ALWAYS); returnprepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null); }
TransactionStatus
用来保存事务状态,包括是否是新事务、是否有保存点、是否已被标记为回滚等,它继承了SavepointManager,SavepointManager是对事务保存点功能的封装,所以可以通过AbstractTransactionStatus操作保存点
介绍完核心基础类后,下面介绍在Spring中如何使用事务,Spring支持两种方式接入事务,分别是编程式事务和声明式事务
编程式事务
编程式事务使用TransactionTemplate或者直接使用底层PlatformTransactionManager进行事务管理;就编程式事务管理而言,Spring推荐使用TransactionTemplate
PlatformTransactionManager
// 开启事务TransactionStatusstatus=transactionManager.getTransaction(newDefaultTransactionDefinition()); //业务代码(SQL)....... // 事务提交transactionManager.commit(status) // 事务回滚transactionManager.rollback(status)
TransactionTemplate
Spring提供的统一事务模版类,它对Spring事务管理做了进一步封装,目的是通过模版定义+回调的形式简化事务操作,让应用开发专注于业务代码
transactionTemplate.execute(newTransactionCallbackWithoutResult() { protectedvoiddoInTransactionWithoutResult(TransactionStatusstatus) { //业务代码(SQL) } });
执行流程:
实际上TransactionTemplate内部依然是调用PlatformTransactionManager的相关方法对事务进行处理;execute方法入参为TransactionCallback,有且仅有一个抽象类TransactionCallbackWithoutResult实现了它,两者的区别在于是否有返回值。
声明式事务
声明式事务是基于AOP实现的;Spring容器在初始化时会通过扫描对标记了事务的方法(打上@Transactional注解的方法)进行拦截增强,在目标方法执行前创建或加入事务并基于执行结果执行提交或回滚;相比较编程式事务,声明式事务最大的优点是对业务代码无侵入。
属性=属性值) (publicvoidtransfer(intinAccount, intoutAccount, intmoney) { //业务代码(SQL)}
通过@Transactional内可以配置包括事务传播行为、事务隔离级别、超时时间等如下属性
执行流程:
打上@Transactional注解的方法Spring会通过TransactionInterceptor对其进行拦截增强,所以在事务方法被调用执行的时候,实际上是执行TransactionInterceptor的invoke方法,其会调用父类TransactionAspectSupport的invokeWithinTransaction方法,该方法优先通过已声明的事务管理器名称尝试从缓存中获取PlatformTransactionManager,如果缓存中没有,则到Spring容器中拿,然后调用PlatformTransactionManager的getTransaction方法获取事务(在PlatformTransactionManager内可能开启一个新的事务,可能返回已经存在的,取决于设置的事务传播行为及当前是否存在已被激活的事务),开始事务后,调用原方法的业务逻辑,如果执行成功则提交,失败抛异常的情况调用rollbackOn判断是否是指定异常或错误,是的话执行回滚,否则提交,可以通过rollbackFor、rollbackForClassName指定回滚异常类型,不指定默认是Error或RuntimeException。
最后再介绍几个在使用声明式事务容易踩坑的点
常见问题
- 问题
- 现象:事务方法明明抛了异常,事务怎么还是提交了
- 原因:TransactionAspectSupport在执行业务逻辑抛异常的情况下,会调用rollbackOn方法判断,返回true则回滚,否则提交;而在rollbackOn中的会优先判断是否设置了指定回滚异常属性,指定了则看是否匹配,没指定默认是Error或RuntimeException,所以虽然抛了异常,但不是指定异常,事务还是会提交。具体可以参照上面的时序查看源码。
- 问题
- 现象:事务方法明明抛了异常,事务怎么还是提交了
privatevoida() { //业务代码(SQL)thrownewRuntimeException(); }
- 原因:在容器初始化的时候会调用AopUtils.canApply方法,判断是否可以生成代理,其会通过TransactionAttributeSourcePointCut.matches -> AbstractFallbackTransactionAttributeSource.computeTransactionAttribute ,在computeTransactionAttribute中会判断只有public的方法返回非空,即只有public的方法才能生成增强代理类,所以在private或protected方法上增加事务注解,该方法不会被增强。
- 问题
- 现象:嵌套事务抛了异常,外部事务明明trycatch了,为什么外部事务也回滚了
publicvoida() { //业务代码(SQL)try{ C.c() }catch(Exceptione){ log(); } } publicvoidc() { //业务代码(SQL)thrownewRuntimeException(); }
- 原因:Spring的事务传播属性默认是required,即调用c的时候发现当前已经存在由a开启的事务,则直接加入;在c抛出异常后,拦截器异常处理中会调用到AbstractPlatformTransactionManager的processRollback方法,其中会依次判断当前是否存在保存点、是否是新事务,是否属于一个更大的事务中,required会命中第三种情况,并执行doSetRollbackOnly方法,该方法会调用子类DataSourceTransactionManager重写后的方法将事务rollbackOnly设置为true并结束处理,外部事务提交的时候会判断当rollbackOnly = true时执行回滚
总结
事务管理通过数据恢复、并发控制来保障事务的AID特性,从而实现数据的一致性。针对我们经常遇到的单机事务,不管是JDK提供的jdbc,还是开源orm框架中的事务,只提供了应用管理事务的工具,本质上依赖的还是数据库的事务功能。Spring对各种事务(包括分布式事务)管理工具进行了统一抽象,形成了一致的事务模型;并提供了编程式事务和声明式事务两种使用方式,进一步降低了应用事务管理的成本。