本文转载自微信公众号《苏三说技术》,作者因热爱而坚持ing。转载本文请联系苏三硕科技公众号。前言最近有网友问我一个问题:系统出现大额交易问题怎么处理?就在前段时间,我在公司处理了这个问题。当时由于项目前期时间紧,为了快速完成业务功能,我们忽略了系统部分。性能问题。项目上线成功后,投入了一段迭代时间解决大业务问题。目前已经完成优化并成功上线。现在我想总结一下我们当时使用的一些解决方案,方便大家遇到同样的问题时可以参考。大交易带来的问题在分享解决方案之前,我们先来看看系统中大交易可能带来的问题。从上图可以看出,如果系统中存在大事务,问题不小,所以我们在实际项目开发中应该尽量避免出现大事务的情况。如果我们现有的系统出现了很大的交易问题,怎么解决呢?解决方案是少用@Transactional注解。在实际项目开发中,我们在业务方法上添加@Transactional注解,开启事务功能。这是一种非常普遍的做法。这称为声明性交易。部分代码如下:@Transactional(rollbackFor=Exception.class)publicvoidsave(Useruser){doSameThing...}不过,我首先要说的是:少用@Transactional注解。为什么?我们知道@Transactional注解是通过spring的aop来工作的,但是如果使用不当,可能会导致事务功能失效。如果碰巧没有经验,这种问题不容易排查。至于在什么情况下交易会失败,可以参考我之前写的文章?。@Transactional注解一般加在一个业务方法上,会导致整个业务方法都在同一个事务中。粒度太粗,无法控制事务的范围。这是大事务问题的最常见原因。那我们该怎么办呢?我们可以使用程序化的事务,在spring项目中使用TransactionTemplate对象来手动执行事务。部分代码如下:@AutowiredprivateTransactionTemplatetransactionTemplate;...publicvoidsave(finalUseruser){transactionTemplate.execute((status)=>{doSameThing...returnBoolean.TRUE;})}从上面的代码可以看出,使用TransactionTemplate的编程风格事务函数本身灵活地控制事务的范围,是避免大事务问题的首选。当然,我少说使用@Transactional注解来启动事务,并不是说一定不能用。如果项目中的一些业务逻辑比较简单,变化不频繁,那么使用@Transactional注解来开启一个事务也是可以的,因为这样比较简单。开发效率更高,但是要注意事务失败的问题。将查询(select)方法放在事务之外如果发生大事务,可以将查询(select)方法放在事务之外,这也是一种常见的做法,因为一般这样的方法不需要事务。比如出现如下代码:@Transactional(rollbackFor=Exception.class)publicvoidsave(Useruser){queryData1();queryData2();addData1();updateData2();}这两个查询方法queryData1和queryData2可以执行在事务之外,将真正需要执行事务的代码放到事务中,比如:addData1和updateData2方法,这样可以有效降低事务的粒度。如果你使用TransactionTemplate的程序化事务,这里修改起来非常方便。@AutowiredprivateTransactionTemplatetransactionTemplate;...publicvoidsave(finalUseruser){queryData1();queryData2();transactionTemplate.execute((status)=>{addData1();updateData2();returnBoolean.TRUE;})}但是如果你还想有了@Transactional注解,怎么拆分呢?publicvoidsave(Useruser){queryData1();queryData2();doSave();}@Transactional(rollbackFor=Exception.class)publicvoiddoSave(Useruser){addData1();updateData2();}这个例子很经典的错误.这种直接方法调用是不会生效的,提醒一下入坑的朋友。因为@Transactional注解的声明式事务是通过springaop来工作的,而springaop需要生成一个代理对象,直接方法调用还是使用原来的对象,所以事务不会生效。有办法解决这个问题吗?1.增加一个新的服务方法。这个方法很简单。只需要增加一个新的Service方法,在新的Service方法上加上@Transactional注解,将需要执行事务的代码移到新的方法中即可。.具体代码如下:@ServciepublicclassServiceA{@AutowiredprvateServiceBserviceB;publicvoidsave(Useruser){queryData1();queryData2();serviceB.doSave(user);}}@ServciepublicclassServiceB{@Transactional(rollbackFor=Exception.class)publicvoiddoSave(Useruser){addData1();updateData2();}}2.将自己注入到Service类中如果你不想新增一个Service类,将自己注入到Service类中也是一种选择。具体代码如下:@ServciepublicclassServiceA{@AutowiredprvateServiceAserviceA;publicvoidsave(Useruser){queryData1();queryData2();serviceA.doSave(user);}@Transactional(rollbackFor=Exception.class)publicvoiddoSave(Useruser){addData1();updateData2();}}可能有人会有这样的疑惑:这种做法会不会造成循环依赖问题?其实springioc里面的三级缓存就保证了,不会出现循环依赖的问题。如果想深入了解循环依赖,可以看我之前的文章《spring解决循环依赖为什么要用三级缓存?》。3、在Service类中使用AopContext.currentProxy()获取代理对象。上面的方法2确实可以解决问题,但是代码看起来不直观。也可以在Service类中使用AOPProxy获取代理对象,实现同样的功能。具体代码如下:@ServciepublicclassServiceA{publicvoidsave(Useruser){queryData1();queryData2();((ServiceA)AopContext.currentProxy()).doSave(user);}@Transactional(rollbackFor=Exception.class)publicvoiddoSave(Useruser){addData1();updateData2();}}避免事务中的远程调用。我们在界面中调用其他系统的接口是不可避免的。由于网络不稳定,本次远程调用的响应时间可能会比较长。如果在远程调用代码中放入一些东西,那东西可就大不了了。当然,远程调用不仅仅是指调用接口,还包括:发送MQ消息,或者连接redis、mongodb保存数据等。@Transactional(rollbackFor=Exception.class)publicvoidsave(Useruser){callRemoteApi();addData1();}远程调用代码可能会耗时较长,记得放在事务外。@AutowiredprivateTransactionTemplatetransactionTemplate;...publicvoidsave(finalUseruser){callRemoteApi();transactionTemplate.execute((status)=>{addData1();returnBoolean.TRUE;})}有朋友可能会问,远程调用的代码没有放在事务中如何保证数据的一致性?这就需要建立重试+补偿的机制来实现最终的数据一致性。避免在一个事务中一次处理太多数据。如果一个事务中要处理的数据过多,也会造成大事务的问题。比如你为了操作方便,可能一次批量更新1000条数据,这会造成大量的数据锁等待,尤其是在高并发系统中。解决方案是分页处理,1000条数据分成50页,一次只处理20条数据,可以大大减少大事务的发生。非事务执行在使用事务之前,我们都应该思考一下是否所有的数据库操作都需要在事务中执行?@AutowiredprivateTransactionTemplatetransactionTemplate;...publicvoidsave(finalUseruser){transactionTemplate.execute((status)=>{addData();addLog();updateCount();returnBoolean.TRUE;})}在上面的例子中,其实增加操作日志的addLog方法和更新统计数量的updateCount方法不能在事务中执行,因为操作日志和统计数量相同,该业务允许有少量数据不一致。@AutowiredprivateTransactionTemplatetransactionTemplate;...publicvoidsave(finalUseruser){transactionTemplate.execute((status)=>{addData();returnBoolean.TRUE;})addLog();updateCount();}当然需要识别哪个方法可以用在大事务中非事务执行其实没那么容易。需要对整个业务进行审查才能找到最合理的答案。异步处理也很重要。事务中的所有方法都需要同步执行吗?我们都知道同步方法执行需要等待方法返回。如果一个事务中同步执行的方法过多,必然会造成等待时间。如果太长,就会出现很大的交易问题。看下面的例子:@AutowiredprivateTransactionTemplatetransactionTemplate;...publicvoidsave(finalUseruser){transactionTemplate.execute((status)=>{order();delivery();returnBoolean.TRUE;})}order方法用于下单,delivery方法用于发货。是否必须在下订单后立即交付?答案是不。这里的传递函数其实可以使用mq异步处理逻辑。@AutowiredprivateTransactionTemplatetransactionTemplate;...publicvoidsave(finalUseruser){transactionTemplate.execute((status)=>{order();returnBoolean.TRUE;})sendMq();}总结一下,我是从一个网友的提问开始的,结合我的实际工作经验分享处理大事务的6种方法:少用@Transactional注解将查询(select)方法放在事务外事务中,避免远程调用事务避免一次处理太多数据非异步处理的事务执行
