【转】Spring MySQL 事务隔离级别,传播机制,savepoint
MySQL的四种事务隔离级别
https://www.cnblogs.com/huanongying/p/7021555.html
spring事物的七种事物传播属性行为及五种隔离级别
https://www.cnblogs.com/yuanfy008/p/4174340.html
PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED区别
https://blog.csdn.net/u011285162/article/details/19247711
https://www.cnblogs.com/deepminer/p/12128677.html
savepoint
https://blog.csdn.net/ytfy12/article/details/52489371
mysql 可重复读 通过加锁解决幻读问题
https://www.cnblogs.com/cat-and-water/p/6429268.html
手动回滚事务
https://blog.csdn.net/weixin_41141219/article/details/80751258
https://my.oschina.net/jiansin/blog/3023799
MySQL的四种事务隔离级别
一、事务的基本要素(ACID)
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
二、事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
三、MySQL事务隔离级别
mysql默认的事务隔离级别为repeatable-read
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(read uncommitted) | 是 | 是 | 是 |
读已提交(read committed) | 否 | 是 | 是 |
可重复读(repeatable read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
补充:
1、事务隔离级别为读已提交时,写数据只会锁住相应的行
2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
MYSQL可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)
如果select 操作加锁(LOCK IN SHARE MODE, for update),则不使用快照,使用最新当前版本数据,
如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。
本身,可重复读和提交读是矛盾的。在同一个事务里,如果保证了可重复读,就会看不到其他事务的提交,违背了提交读;如果保证了提交读,就会导致前后两次读到的结果不一致,违背了可重复读。
可以这么讲,InnoDB提供了这样的机制,在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据。
http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
If you want to see the “freshest” state of the database, you should use either the READ COMMITTED isolation level or a locking read:
SELECT * FROM t_bitfly LOCK IN SHARE MODE;
结论:MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。
3、事务隔离级别为串行化时,读写数据都会锁住整张表
4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
5、MYSQL MVCC实现机制参考链接:https://blog.csdn.net/whoamiyang/article/details/51901888
6、关于next-key 锁可以参考链接:https://blog.csdn.net/bigtree_3721/article/details/73731377
Mysql操作:
select @@tx_isolation; //查询隔离级别
set session transaction isolation level read uncommitted;//设置隔离级别
start transaction;//开启事务
rollback;//回滚事务
commit;//提交事务
spring事物的七种事物传播属性行为及五种隔离级别
其中spring七个事物传播属性:
PROPAGATION_REQUIRED -- 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,
则进行与PROPAGATION_REQUIRED类似的操作。
五个隔离级别:
ISOLATION_DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.
另外四个与JDBC的隔离级别相对应;
ISOLATION_READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。
这种隔离级别会产生脏读,不可重复读和幻像读。
ISOLATION_READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取
该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
ISOLATION_REPEATABLE_READ 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证
一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,
不可重复读外,还避免了幻像读。
关键词:
1)幻读:事务1读取记录时事务2增加了记录并提交,事务1再次读取时可以看到事务2新增的记录;
2)不可重复读取:事务1读取记录时,事务2更新了记录并提交,事务1再次读取时可以看到事务2修改后的记录;
3)脏读:事务1更新了记录,但没有提交,事务2读取了更新后的行,然后事务T1回滚,现在T2读取无效。
savepoint
NESTED 嵌套事务里用的,spring 动态拦截器 进入被NESTED事务标记的方法之前,会设置保存点 SAVEPOINT ·savepoint_1`;
如果方法出现异常执行回滚到savepoint,ROLLBACK TO SAVEPOINT;
如果没有异常,则方法执行完后,释放保存点,RELEASE SAVEPOINT;
嵌套的外层事务commit时,一起提交到数据库;
NESTED 嵌套事务,使用存在的事务,并且使用存在的数据库连接,conn,sqlsession
事务套事务Bug记录分析:
package com.jd.project.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class ServiceA { @Autowired private ServiceB serviceB; @Transactional(propagation = Propagation.REQUIRED) public void doSometing() { saveToDb1(); try { serviceB.doSometing(); } catch (Exception e) { e.printStackTrace(); } saveToDb2(); } private void saveToDb1() { System.out.println("saveToDb1"); } private void saveToDb2() { System.out.println("saveToDb2"); } }
package com.jd.project.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class ServiceB { @Transactional(propagation = Propagation.REQUIRED) public void doSometing() { //插入数据到数据库 insertData(); throw new RuntimeException("抛出异常,触发事务回滚"); } private void insertData() { System.out.println("insertData"); } }
现象描述: 调用类调用ServiceA的doSomething()方法, saveToDb1()、saveToDb2()没有发生异常,serviceB.doSometing() 异常也被捕获,却发生了回滚,saveToDb1(), saveToDb2()没有生效。
原因分析:serviceB.doSometing()的事务隔离级别为Propagation.REQUIRED,使用了外层已经的存在的事务,serviceB.doSometing()发生异常,事务被标记为rollback-only,等saveToDb2()执行完,事务触发了回滚,导致saveToDb1(), saveToDb2(),没有生效。
修复方法:serviceB.doSometing()的事务隔离级别为Propagation.REQUIRED_NEW,或者Propagation.PROPAGATION_NESTED ;
Propagation.REQUIRED_NEW为新建事务,新建会话sqlsession,
Propagation.PROPAGATION_NESTED,使用当前事务,当前会话sqlsession, 但是提供savepoint,发生异常时,回滚到savepoint, serviceA的后面的saveToDb2()方法会继续执行。
手动回滚事务
https://blog.csdn.net/weixin_41141219/article/details/80751258
手动回滚:
方法1:在service层方法的catch语句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚,这样上层就无需去处理异常(现在项目的做法)
方法2:例如service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加throw new RuntimeException()语句,以便让aop捕获异常再去回滚,并且在service上层(webservice客户端,view层action)要继续捕获这个异常并处理
@Transactional public String commonMoney(Receipt rpt,Moneyrecord mors){ rpt.setState(1); int a=dao.insert(rpt); if(a<=0) return"缴费失败"; mors.setPric(rpt.getPic()); mors.setExid(rpt.getPid()); mors.setState(1); boolean tf=mrs.custom(mors); if(!tf){ TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return"余额不足"; } return "OK"; }