项目中常用到的数据库事务
一.什么是事务
正式解释
一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。
项目使用中的解释
将许多相关联的数据操作合并为原子操作的过程
二.事务的特性
- 原子性(Atomicity):组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交。事务中的任何一个数据库操作失败,已经执行的任何操作都必须被撤销,让数据库返回初始状态。
- 一致性(Consistency):一致性是事务结果的体现,其他三个特性用于保证一致性的实现
- 隔离性(Isolation):是对不同事务之间,数据界限定义,用于表示不同事务间的数据连通性,隔离性可影响到数据得一致性的实现
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
三.mysql对事务支持的简要说明
原子性
mysql引擎主要有InnoDB, MyISAM, MEMORY, Archive.而支持事务的只有InnoDB,所以mysql关于事务的讨论都是基于InnoDB引擎的基础上进行讨论。mysql在事务中对于原子性的保证主要是通过undo log来实现的,在进行cud操作是,首先会将执行记录写到undo中,但执行回滚时,mysql会根据undo的记录进行相反的操作,进而取消本次事务对数据得影响。
持久性
与原子性一样,事务的持久性也是通过日志来实现的,MySQL 使用重做日志(redo log)实现事务的持久性,重做日志由两部分组成,一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以它是易失的,另一个就是在磁盘上的重做日志文件,它是持久的。除了所有对数据库的修改会产生重做日志,因为回滚日志也是需要持久存储的,它们也会创建对应的重做日志,在发生错误后,数据库重启时会从重做日志中找出未被更新到数据库磁盘中的日志重新执行以满足事务的持久性。当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上,图中的第 4、5 步就是在事务提交时执行的。
一致性
一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
如A给B转账,不论转账的事务操作是否成功,其两者的存款总额不变(这是业务逻辑的一致性,至于数据库关系约束的完整性就更好理解了)。
保障机制(也从两方面着手):数据库层面会在一个事务执行之前和之后,数据会符合你设置的约束(唯一约束,外键约束,check约束等)和触发器设置;此外,数据库的内部数据结构(如 B 树索引或双向链表)都必须是正确的。业务的一致性一般由开发人员进行保证,亦可转移至数据库层面。但是一致性是相对的,在存在并发事务的情况下,需要在隔离级别和一致性直接作出平衡。
隔离性
事务的隔离性主要体现在事务的并行执行中,如果没有数据库的事务之间没有隔离性设置,各个事务之间的数据改动会相互影响,影响数据得一致性,甚至发生级联回滚等问题,造成性能上的巨大损失。如果所有的事务的执行顺序都是线性的,那么对于事务的管理容易得多,但是允许事务的并行执行却能能够提升吞吐量和资源利用率,并且可以减少每个事务的等待时间。事务的隔离级别分别为READ UNCOMMITED、READ COMMITED、REPEATABLE READ 和 SERIALIZABLE,后面将结合spring对事务的支持做具体的讲解。
四.spring中的事务
spring事务支持编程式事务和声明式事务两种方式进行事务管理,由于声明式事务的无侵入性及易用性,下面会通过声明式事务的使用来说明spring对事务的支持,即使用注解的方式.如果你需要对事务边界进行特别精细的控制,那么请使用编程式事务。spring通过注解开启事务管理的方式是在需要事务管理的业务方法上添加注解@Transactional,对事务的自定义控制是通过设置事务的基本属性来实现的,包括传播行为、隔离规则、回滚规则、事务超时、是否只读,@Transactional提供了可选参数来对事务进行配置,项目使用中常见的注解使用形式为:@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, rollbackFor = Exception.class, timeout = 600, readOnly = false),这个里面包含了五种基本属性的设置,一般情况下不做配置,使用默认即可,下面结合这五个属性来说明spring中事务的使用。
传播行为(propagation)
传播行为(propagation behavior),用来标记事务在方法之间的传递关系,基于事务的使用场景,spring定义了7中传播行为:
传播行为 | 含义 | 效果说明 |
---|---|---|
PROPAGATION_REQUIRED(spring默认) | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 | MethodA中调用MethodB,在MethodB上的事务传播行为设置为PROPAGATION_REQUIRED,1.MethodA开启事务,调用MethodB后,MethodB会加入到MethodA的事务中,当MethodA或MethodB发生异常时,MethodA,MethodB会进行回滚2.MethodA无事务,调用MethodB后,MethodB会生成一个新事务,当MethodA发生异常时,都不回滚,当MethodB发生异常时,只有MethodB进行回滚 |
PROPAGATION_SUPPORTS | 方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 | MethodA中调用MethodB,在MethodB上的事务传播行为设置为PROPAGATION_SUPPORTS,1.MethodA开启事务,调用MethodB后,MethodB会加入到MethodA的事务中,当MethodA或MethodB发生异常时,MethodA,MethodB会进行回滚。2.MethodA无事务,调用MethodB后,MethodB也会无事务,任何情况下,MethodA和MethodB都不会回滚 |
PROPAGATION_MANDATORY | 方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 | MethodA中调用MethodB,在MethodB上的事务传播行为设置为PROPAGATION_MANDATORY,1.MethodA开启事务,调用MethodB后,MethodB会加入到MethodA的事务中,当MethodA或MethodB发生异常时,MethodA,MethodB进行回滚。2.MethodA无事务,调用MethodB后,MethodB会报异常 |
PROPAGATION_REQUIRED_NEW | 方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起 | MethodA中调用MethodB,在MethodB上的事务传播行为设置为PROPAGATION_REQUIRED_NEW,1.MethodA开启事务,调用MethodB,MethodB会新建事务,MethodA的事务会被挂起,当MethodB发生异常时,MethodB进行回滚,如果MethodB异常未捕获,MethodA回滚。MethodA发生异常,MethodA发生回滚,MethodB正常提交。2.MethodA无事务,调用MethodB后,MethodB会新建事务,MethodB报异常,MethodB会回滚,任何情况下,MethodA都不会回滚 |
PROPAGATION_NOT_SUPPORTED | 方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起 | MethodA中调用MethodB,在MethodB上的事务传播行为设置为PROPAGATION_NOT_SUPPORTED,1.MethodA开启事务,调用MethodB,MethodA的事务会被挂起,当MethodB发生异常时,MethodB不会回滚,如果MethodB异常未捕获,MethodA回滚。MethodA发生异常,MethodA发生回滚,MethodB正常提交。2.MethodA无事务,调用MethodB后,MethodB也会无事务,任何情况下,MethodA和MethodB都不会回滚 |
PROPAGATION_NEVER | 当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 | MethodA中调用MethodB,在MethodB上的事务传播行为设置为PROPAGATION_NEVER,1.MethodA开启事务,调用MethodB,MethodB会抛出异常,MethodA回滚。2.MethodA无事务,调用MethodB后,MethodB也会无事务,任何情况下,MethodA和MethodB都不会回滚 |
PROPAGATION_NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。 | MethodA中调用MethodB,在MethodB上的事务传播行为设置为PROPAGATION_NESTED,1.MethodA开启事务,调用MethodB,调用MethodB后,MethodB会加入到MethodA的事务中,MethodB发生异常时,MethodB回滚,MethodA不会回滚,MethodA发生异常时,MethodA,MethodB都会回滚。2.MethodA无事务,调用MethodB后,MethodB会生成一个新事务,当MethodA发生异常时,都不回滚,当MethodB发生异常时,只有MethodB进行回滚 |
隔离规则(isolation)
隔离规则主要用于标记并发事务之间的数据界限,用来控制并发事务之间对同一数据的修改,对其他事务造成的影响,为了解决并行事务可能产生的脏读、幻读、不可重复读等问题,spring定义了五种隔离级别:
隔离级别 | 含义 | 存在的问题 |
---|---|---|
ISOLATION_DEFAULT(spring默认) | 使用数据库默认的隔离级别,mysql的默认隔离级别为REPEATABLE-READ,对应spring的ISOLATION_REPEATABLE_READ | |
ISOLATION_READ_UNCOMMITTED | 读未提交,最低的隔离级别,允许读取尚未提交的数据变更,一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据 | 脏读、幻读、不可重复读 |
ISOLATION_READ_COMMITTED | 读已提交,允许读取并发事务已经提交的数据,读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行 | 幻读、不可重复读 |
ISOLATION_REPEATABLE_READ | 可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。 | 幻读 |
ISOLATION_SERIALIZABLE | 可序列化,最高的隔离级别,事务序列化执行,事务只能一个接着一个地执行,不能并发执行 | 串行执行事务,对并发事务的执行效率影响大 |
tip:
- 脏读:一个事务读取到了另一个事务未提交的数据操作结果,如果发生回滚,则相当于读取到了一个不存在的数据,出现脏读
- 不可重复读:一个事务操作中,进行两次一样的查询,但由于两次查询间隔期间,有其他并行事务进行了数据的修改,导致两次读取的数据不一致,这种现象称为不可重复读或者虚度。
- 幻读:一个事务操作中,两次读库,但由于两次查询间隔期间,有其他并行事务进行了数据的删除或者新增,导致两次读取数据不一致,这种现象称为幻读。
只读(readOnly)
从设置的时间点A开始到事务结束时间点B的过程中,该事务将看不见其他事务所提交的数据,即查询中不会出现别的并行事务在A之后提交的数据。在将事务设置成只读后,相当于将数据库设置成只读数据库,此时若要进行写的操作,会出现错误。使用只读时,相当于提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力。
事务超时(timeout)
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
回滚规则(rollbackFor)
回滚规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚,但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。需要注意的是,在异常被捕获的情况下,需要在catch里面手动进行回滚,或者抛出异常,否则事务不会进行自动回滚。
五.spring中事务使用常见坑
事务超时设置不生效
实例代码
@Transactional(rollbackFor= Exception.class, timeout=2)
public void testTimeout() throws InterruptedException {
System.out.println(System.currentTimeMillis());
testBo bo = new testBo();
testBo.setId("15462");
testBo.setStatus("2");
testMapper.updateById(bo);
Thread.sleep(3000L);
}
这个代码实例中,对于整个方法来说肯定会超时,但是,实际上,事务却不会回滚,因为在方法的执行过程中,检查是否超时是在sql执行过程中进行判断的,当sql执行完成后,无论方法执行多久,都不会再对超时进行判断,也就不会报超时异常,更不会因超时进行回滚,更改为如下代码,超时生效
@Transactional(rollbackFor= Exception.class, timeout=2)
public void testTimeout() throws InterruptedException {
System.out.println(System.currentTimeMillis());
testBo bo = new testBo();
testBo.setId("15462");
testBo.setStatus("2");
testMapper.updateById(bo);
Thread.sleep(3000L);
testMapper.selectById("15462");
}
内部调用方法的事务不生效
实例代码
public void testTarget(){
testTimeout();
}
@Transactional(rollbackFor= Exception.class)
public void testTimeout() throws Exception {
System.out.println(System.currentTimeMillis());
testBo bo = new testBo();
testBo.setId("15462");
testBo.setStatus("2");
testMapper.updateById(bo);
throw new Exception();
}
这种情况,我们会发现,事务没有生效,执行的更新语句没有回滚,这是因为,声明式事务默认是通过jdk动态代理的方式进行事务管理的,而jdk动态代理只有在外部调用其方法时才会代理调用,自己调用自己的方法是不会走代理调用的,这就是为什么内部调用方法事务不生效的原因所在。解决办法就是在事务注解配置的地方,选择使用cglib动态代理模式,添加proxy-target-class=“true”
<tx:annotation-driven transaction-manager="transactionManager" mode="proxy" proxy-target-class="true"/
内部方法之间调用,事务传播特性不生效的因为也是如此。
多数据源的情景下事务不生效
这种情况可能是由于spring事务的数据源选择失误造成的,在多数据源的情况下,应该使用属性 value指定TransactionManager,否则,spring会默认按照TransactionManager声明的顺序去选择第一个TransactionManager。使用方式是@Transactional(value= targetTransactionManager)