前言
最近有个网友问了我一个问题:零碎中大事务问题要如何解决?
正好前段时间我在公司解决过这个问题,咱们过后因为我的项目初期工夫比拟缓和,为了疾速实现业务性能,疏忽了零碎局部性能问题。我的项目顺利上线后,专门抽了一个迭代的工夫去解决大事务问题,目前曾经优化实现,并且顺利上线。现给大家总结了一下,咱们过后应用的一些解决办法,以便大家被雷同问题困扰时,能够参考一下。
大事务引发的问题
在分享解决办法之前,先看看零碎中如果呈现大事务可能会引发哪些问题
从上图能够看出如果零碎中呈现大事务时,问题还不小,所以咱们在理论我的项目开发中应该尽量避免大事务的状况。如果咱们已有零碎中存在大事务问题,该如何解决呢?
解决办法
少用@Transactional注解
大家在理论我的项目开发中,咱们在业务办法加上@Transactional注解开启事务性能,这是十分广泛的做法,它被称为申明式事务。
局部代码如下:
@Transactional(rollbackFor=Exception.class) public void save(User user) { doSameThing... }
然而,我要说的第一条是:少用@Transactional注解。
为什么?
咱们晓得@Transactional注解是通过spring的aop起作用的,然而如果使用不当,事务性能可能会生效。如果凑巧你经验不足,这种问题不太好排查。至于事务哪些状况下会生效,能够参考我之前写的《spring事务的这10种坑,你稍不留神可能就会踩中!!!》这篇文章。
@Transactional注解个别加在某个业务办法上,会导致整个业务办法都在同一个事务中,粒度太粗,不好管制事务范畴,是呈现大事务问题的最常见的起因。
那咱们该怎么办呢?
能够应用编程式事务,在spring我的项目中应用TransactionTemplate类的对象,手动执行事务。
局部代码如下:
@Autowired private TransactionTemplate transactionTemplate; ... public void save(final User user) { transactionTemplate.execute((status) => { doSameThing... 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(); transactionTemplate.execute((status) => { 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 publicclass ServiceA { @Autowired prvate ServiceB serviceB; public void save(User user) { queryData1(); queryData2(); serviceB.doSave(user); } } @Servcie publicclass ServiceB { @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } }
2.在该Service类中注入本人
如果不想再新加一个Service类,在该Service类中注入本人也是一种抉择。具体代码如下:
@Servcie publicclass 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外部的三级缓存保障了它,不会呈现循环依赖问题。如果你想进一步理解循环依赖问题,能够看看我之前文章《spring解决循环依赖为什么要用三级缓存?》。
3.在该Service类中应用AopContext.currentProxy()获取代理对象
下面的办法2的确能够解决问题,然而代码看起来并不直观,还能够通过在该Service类中应用AOPProxy获取代理对象,实现雷同的性能。具体代码如下:
@Servcie publicclass ServiceA { public void save(User user) { queryData1(); 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(); transactionTemplate.execute((status) => { addData1(); return Boolean.TRUE; }) }
有些敌人可能会问,近程调用的代码不放在事务中如何保证数据一致性呢?这就须要建设:重试+弥补机制,达到数据最终一致性了。
事务中防止一次性解决太多数据
如果一个事务中须要解决的数据太多,也会造成大事务问题。比方为了操作不便,你可能会一次批量更新1000条数据,这样会导致大量数据锁期待,特地在高并发的零碎中问题尤为显著。
解决办法是分页解决,1000条数据,分50页,一次只解决20条数据,这样能够大大减少大事务的呈现。
非事务执行
在应用事务之前,咱们都应该思考一下,是不是所有的数据库操作都须要在事务中执行?
@Autowired private TransactionTemplate transactionTemplate; ... public void save(final User user) { transactionTemplate.execute((status) => { addData(); addLog(); updateCount(); return Boolean.TRUE; }) }
下面的例子中,其实addLog减少操作日志办法 和 updateCount更新统计数量办法,是能够不在事务中执行的,因为操作日志和统计数量这种业务容许大量数据不统一的状况。
@Autowired private TransactionTemplate transactionTemplate; ... public void save(final User user) { transactionTemplate.execute((status) => { addData(); return Boolean.TRUE; }) addLog(); updateCount(); }
当然大事务中要甄别出哪些办法能够非事务执行,其实没那么容易,须要对整个业务梳理一遍,能力找出最正当的答案。
异步解决
还有一点也十分重要,是不是事务中的所有办法都须要同步执行?咱们都晓得,办法同步执行须要期待办法返回,如果一个事务中同步执行的办法太多了,势必会造成等待时间过长,呈现大事务问题。
看看上面这个列子:
@Autowired private TransactionTemplate transactionTemplate; ... public void save(final User user) { transactionTemplate.execute((status) => { order(); delivery(); return Boolean.TRUE; }) }
order办法用于下单,delivery办法用于发货,是不是下单后就肯定要马上发货呢?
答案是否定的。
这里发货性能其实能够走mq异步解决逻辑。
@Autowired private TransactionTemplate transactionTemplate; ... public void save(final User user) { transactionTemplate.execute((status) => { order(); return Boolean.TRUE; }) sendMq(); }
总结
自己从网友的一个问题登程,联合本人理论的工作教训分享了解决大事务的6种方法:
- 少用@Transactional注解
- 将查问(select)办法放到事务外
- 事务中防止近程调用
- 事务中防止一次性解决太多数据
- 非事务执行
- 异步解决
最初说一句(求关注,不要白嫖我)
如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多大厂的前辈交换和学习。
本文由博客群发一文多发等经营工具平台 OpenWrite 公布