让⼈头痛的⼤事务问题到底要如何解决?
前⾔
最近有个⽹友问了我⼀个问题:系统中⼤事务问题要如何处理?
正好前段时间我在公司处理过这个问题,我们当时由于项⽬初期时间⽐较紧张,为了快速完成业务功能,忽略了系统部分性能问题。项⽬顺利上线后,专门抽了⼀个迭代的时间去解决⼤事务问题,⽬前已经优化完成,并且顺利上线。现给⼤家总结了⼀下,我们当时使⽤的⼀些解决办法,以便⼤家被相同问题困扰时,可以参考⼀下。
⼤事务引发的问题
在分享解决办法之前,先看看系统中如果出现⼤事务可能会引发哪些问题
从上图可以看出如果系统中出现⼤事务时,问题还不⼩,所以我们在实际项⽬开发中应该尽量避免⼤事务的情况。如果我们已有系统中存在⼤事务问题,该如何解决呢?
解决办法
少⽤@Transactional注解
⼤家在实际项⽬开发中,我们在业务⽅法加上@Transactional注解开启事务功能,这是⾮常普遍的做法,它被称为声明式事务。
部分代码如下:
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
<
}
然⽽,我要说的第⼀条是:少⽤@Transactional注解。
为什么?
1. 我们知道@Transactional注解是通过spring的aop起作⽤的,但是如果使⽤不当,事务功能可能会失效。如果恰巧你经验不⾜,这种问
题不太好排查。⾄于事务哪些情况下会失效,可以参考我之前写的《》这篇⽂章。
2. @Transactional注解⼀般加在某个业务⽅法上,会导致整个业务⽅法都在同⼀个事务中,粒度太粗,不好控制事务范围,是出现⼤事
务问题的最常见的原因。
那我们该怎么办呢?
可以使⽤编程式事务,在spring项⽬中使⽤TransactionTemplate类的对象,⼿动执⾏事务。
部分代码如下:
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
<
return Boolean.TRUE;
})
}
从上⾯的代码中可以看出,使⽤TransactionTemplate的编程式事务功能⾃⼰灵活控制事务的范围,是避免⼤事务问题的⾸选办法。
当然,我说少使⽤@Transactional注解开启事务,并不是说⼀定不能⽤它,如果项⽬中有些业务逻辑⽐较简单,⽽且不经常变动,使⽤@Transactional注解开启事务开启事务也⽆妨,因为它更简单,开发效率更⾼,但是千万要⼩⼼事务失效的问题。
将查询(select)⽅法放到事务外
如果出现⼤事务,可以将查询(select)⽅法放到事务外,也是⽐较常⽤的做法,因为⼀般情况下这类⽅法是不需要事务的。
⽐如出现如下代码:
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
queryData1();
queryData2();
addData1();
updateData2();
}
可以将queryData1和queryData2两个查询⽅法放在事务外执⾏,将真正需要事务执⾏的代码才放到事务中,⽐
如:addData1和updateData2⽅法,这样就能有效的减少事务的粒度。
如果使⽤TransactionTemplate的编程式事务这⾥就⾮常好修改。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
addData1();
updateData2();
return Boolean.TRUE;
})
}
但是如果你实在还是想⽤@Transactional注解,该怎么拆分呢?
public void save(User user) {
queryData1();
queryData2();
doSave();
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
这个例⼦是⾮常经典的错误,这种直接⽅法调⽤的做法事务不会⽣效,给正在坑中的朋友提个醒。因为@Transactional注解的声明式事务是通过spring aop起作⽤的,⽽spring aop需要⽣成代理对象,直接⽅法调⽤使⽤的还是原始对象,所以事务不会⽣效。
有没有办法解决这个问题呢?
1.新加⼀个Service⽅法
这个⽅法⾮常简单,只需要新加⼀个Service⽅法,把@Transactional注解加到新Service⽅法上,把需要事务执⾏的代码移到新⽅法中。具体代码如下:
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
2.在该Service类中注⼊⾃⼰
如果不想再新加⼀个Service类,在该Service类中注⼊⾃⼰也是⼀种选择。具体代码如下:
@Servcie
public class ServiceA {
@Autowired
prvate ServiceA serviceA;
public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
可能有些⼈可能会有这样的疑问:这种做法会不会出现循环依赖问题?
其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。如果你想进⼀步了解循环依赖问题,可以看看我之前⽂章《》。
3.在该Service类中使⽤AopContext.currentProxy()获取代理对象
上⾯的⽅法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使⽤AOPProxy获取代理对象,实现相同的功能。具体代码如下:
@Servcie
public class ServiceA {
public void save(User user) {
queryData1();
spring到底是干啥的queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
事务中避免远程调⽤
我们在接⼝中调⽤其他系统的接⼝是不能避免的,由于⽹络不稳定,这种远程调的响应时间可能⽐较长,如果远程调⽤的代码放在某个事物中,这个事物就可能是⼤事务。当然,远程调⽤不仅仅是指调⽤接⼝,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
callRemoteApi();
addData1();
}
远程调⽤的代码可能耗时较长,切记⼀定要放在事务之外。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
callRemoteApi();
addData1();
return Boolean.TRUE;
})
}
有些朋友可能会问,远程调⽤的代码不放在事务中如何保证数据⼀致性呢?这就需要建⽴:重试+补偿机制,达到数据最终⼀致性了。
事务中避免⼀次性处理太多数据
如果⼀个事务中需要处理的数据太多,也会造成⼤事务问题。⽐如为了操作⽅便,你可能会⼀次批量更新1000条数据,这样会导致⼤量数据锁等待,特别在⾼并发的系统中问题尤为明显。
解决办法是分页处理,1000条数据,分50页,⼀次只处理20条数据,这样可以⼤⼤减少⼤事务的出现。
⾮事务执⾏
在使⽤事务之前,我们都应该思考⼀下,是不是所有的数据库操作都需要在事务中执⾏?
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
addData();
addLog();
updateCount();
return Boolean.TRUE;
})
}
上⾯的例⼦中,其实addLog增加操作⽇志⽅法 和 updateCount更新统计数量⽅法,是可以不在事务中执⾏的,因为操作⽇志和统计数量这种业务允许少量数据不⼀致的情况。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
addData();
return Boolean.TRUE;
})
addLog();
updateCount();
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论