因为一次服务器卡顿,再来好好理解Spring事务传播
文章目录
- 事故描述
- 问题排查
- 事故原因
- Spring事务的基本原理
- 事务的传播与嵌套
-
- PROPAGATION_REQUIRED (Spring 默认)
- PROPAGATION_REQUIRES_NEW
- PROPAGATION_SUPPORTS
- PROPAGATION_NESTED
- PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED之间的比较
- 为什么要手动开启事务
-
- 注入自身的示例:
在某天的一个下午,突然用户报系统使用特别卡顿。有些操作不能执行。查看数据库发现运行的线程数开始增加,每秒执行的事务次数也开始增加。
数据库运行的线程数开始增加
每秒执行的事务次数也开始增加
一开始很痛苦,查不到是什么原因。
查慢sql,查不到。查执行的process,只看到某个process一直在执行,但看不到sql语句,居然看不到具体是哪个sql,懵逼了
SELECT * from performance_schema.events_statements_history order by LOCK_TIME desc limit 10;
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX order by trx_started asc
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS where lock_trx_id = 6037541544;
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX where trx_id = 6037541544;
后来根据process开始的时间点,分析错误日志,才定位到此问题。
spring手动事务没能关闭,而spring默认的事务传播机制是:PROPAGATION_REQUIRED,支持当前事务;如果不存在,创建一个新的。导致后面的Spring事务都往这个事务里面去,还一直提交不了,导致系统崩溃。
@Autowired
private PlatformTransactionManager txManager;
@Autowired
private ShopGroupBuyDao shopGroupBuyDao;
@GetMapping(value = "/transactionDemo")
public void ceshi() {
// 手动开启事务 start
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = txManager.getTransaction(def);
// 手动开启事务 end
ShopGroupBuy shopGroupBuy = shopGroupBuyDao.selectOne(new LambdaQueryWrapper()
.eq(ShopGroupBuy::getGroupBuyId, 505));
shopGroupBuy.setGroupBuyTheme("wulin11");
int i = shopGroupBuyDao.updateById(shopGroupBuy);
int a = 1 / 0;
try {
// 手动提交事务 start
txManager.commit(status);
// 手动提交事务 end
if (i > 0) {
System.out.println("更新成功");
} else {
System.out.println("更新失败");
}
} catch(Exception e) {
e.printStackTrace();
// 手动回滚事务 start
txManager.rollback(status);
// 手动回滚事务 end
}
}

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
- 获取连接 Connection con = DriverManager.getConnection()
- 开启事务con.setAutoCommit(true/false);
- 执行CRUD
- 提交事务/回滚事务 con.commit() /con.rollback()
- 关闭连接 conn.close();
使用Spring的事务管理功能后,我们可以不再写步骤2和4的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子。
- 配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
- spring在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
- 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
所谓spring事务的传播属性,就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在TransactionDefinition中定义,具体常量的解释见下表:
通过上面的理论知识的铺垫,我们大致知道了数据库事务和spring事务的一些属性和特点,接下来我们通过分析一些嵌套事务的场景,来深入理解spring事务传播的机制。
假设外层事务 ServiceA的Method A() 调用内层ServiceB的Method B()
PROPAGATION_REQUIRED (Spring 默认)
如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候Spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。
假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。
PROPAGATION_REQUIRES_NEW
比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。
那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。
它与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。
PROPAGATION_SUPPORTS
假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。
PROPAGATION_NESTED
开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED之间的比较
ServiceA {
/** * 事务属性配置为 PROPAGATION_REQUIRED */
@Transactional(propagation=Propagation.REQUIRED) // 1
void methodA() {
insertData(); //2
try {
ServiceB.methodB(); //3
} catch (SomeException) {
// 执行其他业务, 如 ServiceC.methodC(); //5
}
updateData(); //6
}
}
ServiceB {
@Transactional(propagation=Propagation.NESTED)
void methodB(){
//
updateData(); //4
}
}
在上面的1,将开起新事务A,2的时候会插入数据,此时事务A挂起,没有commit,3的时候,使用PROPAGATION_NESTED传播,将在3点的时候新建一个savepoint保存2插入的数据,不提交。
- 如果methodB出现异常,将回滚4的操作,不影响2的操作,同时可以处理后面的5,6逻辑,最后一起commit: 2,5,6
- 如果methodB没有出现异常,那么将一起commit: 2,4,6。
- 假如methodB使用的PROPAGATION_REQUIRES_NEW,那么B异常,会commit: 2,5,6,和NESTED一致,如果methodB没有出现异常,那么会先commit4,再commit:6,那么事务将分离开,不能保持一致,假如执行6报错,2和6将回滚,而4却没有被回滚,不能达到预期效果。
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。这是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
解决方法如下:
- 注入自身(比较快捷,后面建议采用这种形式)
- 拆成两个类(略有点麻烦)
- AopContext.currentProxy()获取代理类(使用起来有点麻烦)
- 使用手写事务(容易出现事务未关闭的情况)
注入自身的示例:
@Service
public class AreaService {
@Autowired
private AreaMapper areaMapper;
@Autowired
private AreaService service;//将自身service注入
//没有加事务
public void insertDelete(Area area){
service.insert(area);
int abc = 1/0; //出现异常,程序结束
service.deleteById(area.getId());//不会执行
}
@Transactional(propagation =Propagation.REQUIRES_NEW)
public int insert(Area area) {
area.setDelFlag(0);
area.setCreateTime(LocalDateTime.now());
int insert = areaMapper.insert(area);
int a = 1/0;//出现异常,事务回滚
return insert;
}
@Transactional(propagation =Propagation.REQUIRES_NEW)
public int deleteById(Long id) {
Long[] ids = {
id};
return areaMapper.updateDelFlagByIds(ids);
}
}
本文仅代表作者观点,版权归原创者所有,如需转载请在文中注明来源及作者名字。
免责声明:本文系转载编辑文章,仅作分享之用。如分享内容、图片侵犯到您的版权或非授权发布,请及时与我们联系进行审核处理或删除,您可以发送材料至邮箱:service@tojoy.com



