关于spring:拜托别再让我优化大事务了我的头都要裂开了

3次阅读

共计 5090 个字符,预计需要花费 13 分钟才能阅读完成。

前言

最近有个网友问了我一个问题:零碎中大事务问题要如何解决?

正好前段时间我在公司解决过这个问题,咱们过后因为我的项目初期工夫比拟缓和,为了疾速实现业务性能,疏忽了零碎局部性能问题。我的项目顺利上线后,专门抽了一个迭代的工夫去解决大事务问题,目前曾经优化实现,并且顺利上线。现给大家总结了一下,咱们过后应用的一些解决办法,以便大家被雷同问题困扰时,能够参考一下。

大事务引发的问题

在分享解决办法之前,先看看零碎中如果呈现大事务可能会引发哪些问题

从上图能够看出如果零碎中呈现大事务时,问题还不小,所以咱们在理论我的项目开发中应该尽量避免大事务的状况。如果咱们已有零碎中存在大事务问题,该如何解决呢?

解决办法

少用 @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 公布

正文完
 0