springboot+mybatis,声明式事务偶尔不提交的问题,最高10倍悬赏

更新记录

  1. 2022-08-03 10:32:修改了代码片段getErrorOrder上面的注释
  2. 2022-08-03 13:52:增加数据库连接池配置
  3. 2022-08-04 09:27:修改orderService.save(prevSubPlanErrorOrder)为orderService.save(errorOrder)
  4. 2022-08-05 09:52:OrderEntity errorOrder = getErrorOrder(key, user) 上方增加重要注释!!!

问题描述

主要的问题如标题所述,就是发现在进行事务操作时,比如save、update时,进行5000次操作,可能会有几百1000次没有提交,也没报错的情况,而出现这种情况时,我们发现在数据库里面会存在,十几个sleep的长事务(持续几千秒)。

这个问题是在生产上出现的,在本地运行,或开发环境运行时无法复现。另外这套代码,我们独立部署了两套,两边的配置都是一模一样的,都是阿里云的ecs+rds mysql,并发也差不多,而现在只有一边出了问题,另一边正常。

环境及依赖描述

  • 服务器:阿里云ECS 4核16G
  • 数据库:阿里云 RDS Mysql 8.0(innoDb) 4核16G
  • java版本:java8
  • 项目框架:springboot+mybatis

代码片段

  • 其实出问题的代码不止只一点,只是因为运行中,save订单的场景是最多的,所以就贴这里的代码,代码我经过了简化,只保留了关键的逻辑部分。
    这个方法只会从controller进入,下面对代码逻辑进行描述,代码逻辑很简单:
    1、查询用户信息
    2、根据用户信息去redis查询之前的错误订单,如果查询到错误订单,就从第三方接口查询错误订单的状态,并保存,然后方法结束
    3、如果没查询到之前的错误订单,就生成新订单,并提交到三方接口

      @Override
      @SneakyThrows
      @Transactional(rollbackFor = {Exception.class, RuntimeException.class})
      public void submitOrder(OrderDTO dto) {
          
    
          //查询签约用户信息,这里只有查询操作
          UserEntity user = userService.getUser(dto.getUserBizid());
          if (user == null) {
              throw new BizException(Msg.MUST_BIND_CHANNEL);
          }
    
          // 从redis中取出之前提交失败的订单,重新查询该订单的状态,如果成功则不需要重新发起订单,直接保存该订单即可,如果失败才需要走后面的流程
          
          //生成该订单的子计划的异常订单redis key,该方法只有字符串操作
          String key = redisKeys.generateErrorOrderKey();
    
          //getErrorOrder这个方法是本service中的方法,该方法声明为private,没有事务注解,该方法中会调用其他service的方法
          //其他service的方法中有可能会存在编程事务,这些编程事务的事务传播为PROPAGATION_REQUIRES_NEW,这些编程事务在其方法退出前都保证commit或rollback了的
          OrderEntity errorOrder = getErrorOrder(key, user);
          if (errorOrder != null) {
              orderService.save(errorOrder);
              //清除redis中的异常订单记录
              redisComponent.delete(key);
              return;
          }
    
          //未查询到之前的失败订单,组装新的order(此处我省略了一些set操作,为了方便大家观看)
          OrderEntity orderEntity = new OrderEntity();
          orderEntity.setDefaultClientRate(dto.getDefaultClientRate());
          orderEntity.setDefaultClientTips(dto.getDefaultClientTips());
          orderEntity.setRatePrice(dto.getRatePrice());
          orderEntity.setTipsPrice(dto.getTipsPrice());
          orderEntity.setDebitCardBankBizid(user.getBankBizid());
          orderEntity.setLocation(dto.getLocation());
          orderEntity.setBizid(idWorkerComponent.nextStringId());
          //这里是调用的mapper自带的save方法  
          orderService.save(orderEntity);
    
          //获取调用第三方接口的service
          PayChannelService payChannelService = PayChannelFactory.getPayChannel();
          //调用三方接口,trade方法里面会有save、select、和http的相关操作
          ClientResultDTO resultDTO = payChannelService.trade();
    
          if (!resultDTO.isSuccess()) {
              //如果三方接口调用失败,则抛出异常,由于方法头加了事务注解,所以理论上应该回滚
              throw new BizException(Msg.BALANCE_REPAY_SUBMIT_ORDER_ERROR.getCode(), resultDTO.getResultMsg());
          }
      }
    

现在的问题是,这个方法没报异常,但是有时候数据并没有save到库中

当出现问题的时候,我们在阿里云rds的性能监控界面,会看到很多挂起的会话,如图:

img

数据库连接池配置

# -------------------druid 配置-------------------
# 初始化时建立物理连接的个数
spring.datasource.druid.initial-size = 50
# 最大连接池数量
spring.datasource.druid.max-active = 5000
# 最小连接池数量
spring.datasource.druid.min-idle = 50
# 获取连接时最大等待时间,单位毫秒
spring.datasource.druid.max-wait = 10000
# 连接保活
spring.datasource.druid.keep-alive=true
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis = 60000
# 连接保持空闲而不被驱逐的最小时间
spring.datasource.druid.min-evictable-idle-time-millis = 300000
# 用来检测连接是否有效的sql,要求是一个查询语句
spring.datasource.druid.validation-query = SELECT 1 FROM DUAL
# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
spring.datasource.druid.test-while-idle = true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-borrow = false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-return = false
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计
spring.datasource.druid.filters = stat,wall
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.druid.connection-properties = druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 合并多个DruidDataSource的监控数据
spring.datasource.druid.use-global-data-source-stat = true
spring.datasource.druid.stat-view-servlet.enabled = true
spring.datasource.druid.web-stat-filter.enabled = true

我现在就是一脸懵逼,因为这个问题是偶发的,就像开头说,save 几千次,可能会出现几百次没入库的情况,没入库的原因应该就是事务没提交,但是不知道为什么没提交。希望有朋友能提供一下排查思路,或手把手指导一下,可以VX,由于CSDN上面的最高只能给到500,不过如果能定位和解决问题的话,我可以给到10倍。

1.你方法加上了声明事务 @Transactional(rollbackFor = {Exception.class, RuntimeException.class})
如果save失败时,事务会自动回滚。
2.我没有在你的方法里看到你有捕获错误,也没有log记录日志,所以日志里异常是没有写入的。
3.你可以在部署的nohup.out文件中找寻错误日志。
4.你这种几千条成功,几百条失败,有可能是数据库某个字段长度不够等导致的。你可以查查。
5.也有可能网络原因(概率小)
6.数据库是否使用的是长连接?是否超过最大连接数?
7.建议你替换成这个: @Transactional(rollbackFor = DataAccessException.class)

/**
 * 数据库自定义异常
 *
 * @author yuanpeng
 * @date 2018/12/28 0028 下午14:05
 */
public class DataAccessException extends RuntimeException {

    public DataAccessException() {
        super();
    }

    public DataAccessException(String message) {
        super(message);
    }

    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }

    public DataAccessException(Throwable cause) {
        super(cause);
    }
}

本地无法复现的话,可以配一下远程debug

看下日志中是否有commit,没提交说明还没执行commit提交事务。
代码中是否可能存在异常被捕获了。

1.没报错是不是你内部try catch了?
2.注解的方法是不是有非public的?

1、是分布式应用吗?
2、数据库隔离级别是什么?
3、并发情况
“比如save、update时,进行5000次操作,可能会有几百1000次没有提交”,“十几个sleep的长事务(持续几千秒)”
你这应该不能算作没提交吧,是不是提交不了,有没有可能碰到了行锁,InnoDB 来说,只有insert和update,这些挂起的长事务所要修改的数据是不是被其他线程占用了,但是锁一直没有释放

另外这个代码规范来说,实在有点儿……
写个编程式事务只包括该有是数据库操作部分不好嘛。退一步说你开启了事务,在事物里面调用第三方接口(不知道http请求连接超时时间设置多少哈),有没有考虑过在第三方长时间不返回,在稍微加上点儿你自身应用的并发,是不是服务器资源占用会有点儿问题啊

数据源是什么,这种最好是debug一下驱动库,着重在autocommit这个属性上,之前看到过MySQL有个版本的驱动就有问题

参考一下:

可以参考写这个的解决思路去解决你的问题

事务不提交问题解决思路_这有一块小饼干的博客-CSDN博客_spring事务不提交 ​开发中偶尔会遇到sql已经执行,日志都打印出来了但是数据并没有任何变化,此时多半为事务没有提交,下面记录一下最近一段时间遇到的事务没有提交的问题排查思路,下文默认可以本地debu复现问题,如果是无法debug的环境只是增加了获取相应数据的复杂度(无法debug),以及查看相应数据的复杂的(需要打日志才看得到)等等,原理并没有变化首先查看执行sql的方法是否已经开启了事务有时可能会因为事务切面没有切到,或者没有打注解等等原因导致事务没有生效,此时可以在执行sql的方法处打断点,然后执行Transac https://blog.csdn.net/dstears/article/details/122824058

网络层渲染错误!或者代码层不合适,渲染也错误

  1. 先检查日志里面有没有Rolled back transaction相关字样,如果有的话,再分析原因
  2. 在日志中检索一下相关表所执行的sql,检查是否有覆盖更新的问题。比如你意欲将A表的a行第一个字段从1更新成2,中间的业务逻辑确实更新成了2,但后续逻辑又将该字段更新为了1
  3. 再检查一下spring的事务传播吧,这个其实问题不大
  4. 看一下代码里面有没有try catch不throw异常的地方,如果更新报错被catch住不抛出,也有可能出现这个问题
  5. 查看一下工程里面有没有错误的配置@ExceptionHandler全局异常处理

暂时想到了这些,具体的,看一下你的代码应该更好分析

有多个service方法间的调用吗? 事务的传播机制设置的是什么? 如果因为多个方法间调用事务传播机制设置的是开启新事物,或者开启一个内嵌事务,是有可能造成新事物或内嵌事务不提交的。
可以发我具体一点的代码,我看下。

是不是某个事务存在问题,导致影响了其他的事务。如果有更多信息的话,才可能进一步确认问题

1:通过指定 @Transactional(rollbackFor = Exception.class)的方式进行全异常捕获
2:多线程调用 主线程A调用线程B保存Id为1的数据,然后主线程A等待线程B执行完成再通过线程A查询id为1的数据。
这时你会发现在主线程A中无法查询到id为1的数据。因为这两个线程在不同的Spring事务中,本质上会导致它们在Mysql中存在不同的事务中。
Mysql中通过MVCC保证了线程在快照读时只读取小于当前事务号的数据,在线程B显然事务号是大于线程A的,因此查询不到数据。
3:异常被内部catch 因此这里如果需要对特定的异常进行捕获处理,记得再次将异常抛出,让最外层的事务感知到。

img

请问那个最小单元的复现代码,朋友现在发出来了嘛
发出来看看才好解决

事务注解加方法上

看下是不是生产那边的数据库的事物隔离级别和开发环境这边不一样!!!

有没有可能是数据库有唯一键约束吗?

这个真是为了解决工作问题,佩服

根据你的逻辑,19行查到错误订单,21行不应该更新这个错误订单么,21行方法里应该是保存errorOrder 而不是prev…

所以你说到的失败应该是能查到错误订单的这块就都失败了~ 查不到保存的情况应该没问题

线上环境,把异常放出来,放到最大能见度。使用这个之后@Transactional感觉会把异常吞掉,没办法看到异常实时具体的信息。可以放开排查下。

  • 是批量更新时,数据冲突导致的问题
  • 需要从技术上改造下处理逻辑
  • 可以考虑把同步更新数据库改成异步更新
  • 先把需要更新的数据放入一张临时表或者消息队列中
  • 然后异步消费取出来去更新
  • 这样可以最大程度避免数据冲突
  • 更新数据库的时候控制一次修改的量在100左右,一次不要太多

如有帮助,请采纳,十分感谢!

说下问题,整个submitOrder是一个大的逻辑处理,你把整个逻辑用大事务进行包裹,是很不妥当的事情。
并且整个方法还有第三方的RPC的调用等等,很可能因为通讯的网络问题挂起当前事务,从而导致行锁无法释放,阻塞其他事务,
也就是出现较多的事务被挂起。
处理方式
建议将submitOrder方法里,读写操作分开,需要归到事务的写操作写在一起。用编程式事务处理写逻辑,尽量的保证事务逻辑操作少
实例代码

  @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         transactionTemplate.execute((status) => {
            doSameThing...
            return Boolean.TRUE;
         })
   }

https://ask.csdn.net/questions/7489322?spm=1005.2026.3001.5635&utm_medium=distribute.pc_relevant_ask_down.none-task-ask-2~default~OPENSEARCH~Rate-1-7489322-ask-7766916.pc_feed_download_top3ask&depth_1-utm_source=distribute.pc_relevant_ask_down.none-task-ask-2~default~OPENSEARCH~Rate-1-7489322-ask-7766916.pc_feed_download_top3ask

orderService.save(orderEntity);

try{__
//获取调用第三方接口的service
PayChannelService payChannelService = PayChannelFactory.getPayChannel();
//调用三方接口,trade方法里面会有save、select、和http的相关操作
ClientResultDTO resultDTO = payChannelService.trade();

}catch(throwable t){
log.error("在这里记录一些报错日志")
throw t; //再抛出异常使事务回滚
}__

if (!resultDTO.isSuccess()) {
//如果三方接口调用失败,则抛出异常,由于方法头加了事务注解,所以理论上应该回滚
//log.error("这里也记录一下日志")__
throw new BizException(Msg.BALANCE_REPAY_SUBMIT_ORDER_ERROR.getCode(), resultDTO.getResultMsg());
}

可以考虑在代码中增加一些日志打印(如果没有的话)
感觉save方法之后,调用第三方接口出现异常的概率较高,所以因为这个原因导致事务回滚的可能性比较大,
而题主说没有看到报异常,会不会是jvm对于一段时间内重复出现的大量异常不打印堆栈信息 (可以考虑加这个参数 -XX:-OmitStackTraceInFastThrow).而实际上是有报错导致事务回滚的.我之前的项目就碰到过有报错但是不打印堆栈信息的场景

从你的描述来看不像是事物没提交,而是提交后一直没返回,应该是有慢sql导致数据库处理不过来,看下你的事物超时时间是多少,适当调小点应该会报超时异常出来,解决的话最好查看sleep进程里是哪些sql,也可以开启mysql的慢查询日志,以定位慢sql

以前碰到过类似的。
有一个定时任务,因为定时任务执行上一个sql时间超时被阿里检测到挂起了,直接导致这个定时任务后续的执行全部停止了。
把这个任务设置成多线程后 ,后续的任务才会正常执行 ,但是卡住的sql还是彻底挂起。
当时我这排查的是sql效率问题。
你还是先把save数据保存一份到缓存比如redis , 然后每天晚上定时排查看下是否漏单吧 。
然后再排查问题, 不然数据不全很麻烦。

看起来是后边的事务一直获取不到锁,没办法执行,加上mysql的lock_wait_timeout 超时时间设置过大。导致后面的事务就一直等待。
首先检查下你的数据库配置
lock_wait_timeout 锁超时时间,这个参数的默认时间非常长,会严重影响正常的业务操作。所以我们需要将之设为50秒或者更短。
这个设置太长导致行锁不释放,其他想更新改行数据的事务就会一直等待也不报错。这个时间设置短一些后续事物超过了就会抛出Lock wait timeout exceeded; try restarting transaction
前一个事务不提交的原因有很多,最常见的是1,HTTP请求无限等待, 2,其他业务的事务中有一个更新操作之后的慢查询,这个查询语句一直执行不完,你可以排查下系统中是否有慢查询,优化掉。
有3个改进点:
1减少每次事务处理的数据量,降低锁冲突的概率。
2,手动控制事务,把比较耗时的IO,HTTP代码放在事务之外处理
3,如果是分布式的话在不影响性能的情况下可以考虑加上分布式锁串行处理该业务(最有效的懒人办法)
4,检查下是否有慢SQL

1、不提交的事务是否已经开启了
2、管理拿到的和提交的是否一致

我觉得是网络原因,本地测试的时候,吧数据库和程序分开部署,然后试试

基本上这类型的问题,依据以往的经验,十之八九是编程式事务导致的。对于声明式事务,spring全盘接管,出现问题可能性较低。
而题主在示例代码第9行描述其它service可能使用了编程事务,且事务传播机制是PROPAGATION_REQUIRES_NEW,我们就分析这一情况:
1、其它service使用编程式事务,意味着事务由程序员自己控制,事务控制可能被人为破坏(有bug)
2、其它service事务传播机制是PROPAGATION_REQUIRES_NEW,意味着被调用方会开启新的事务,当前方法的主事务会被挂起
3、如果其它service在使用编程式事务过程中出现了问题(比如:发生运行时异常,但是在catch异常后未把事务rollback),这样子事务一直不会结束,会导致主事务一直被挂起
结论:建议第一步先排查编程式事务是否已正常commit或rollback

大概率是提交时被锁住了,建议将大事务分解成若干小事务,并分时段进行提交。
你可以看下information_schema 库中三个关于锁的表,
innodb_trx:当前运行的所有事务
innodb_locks:当前出现的锁
innodb_lock_waits:锁等待的对应关系
该问题可以直接从这个几张表入手,找到了一直没有提交的只读事务,然后 kill thread id
,最后确认只读事物是否被干掉了就OK了。解决步骤如下:

mysql> select * from information_schema.innodb_trx;
mysql> SHOW FULL PROCESSLIST;
mysql> kill 'thread id';
mysql> select * from information_schema.innodb_trx;

另外,如需要查看定位是哪条语句,可以在MySQL的binlog日志中查看根据id和时间定位查找语句。

相关命令:
1、查看正在执行的事务

img


上图可以查看到正在进行的事务(未进行commit)操作的线程信息,线程id为122985;
查看正在锁的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

查看等待锁的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

查看所有的线程列表

show full processlist;

结合第一步查到的线程id,可以确定该事务涉及到的表名和数据库链接信息。
定位未提交的事务执行的sql语句
通过以上步骤并不能直观的定位到出问题的sql语句,此时我们可以通过开启通用日志的方式,定位具体的sql语句。

# 查看general log配置
show variables like '%general_log%'

# 开启general log
SET GLOBAL general_log = 1;

通用日志会记录所有sql信息,数据量很大,建议只在排查错误时开启,线上关闭。
此时根据找到进程号和时间戳找到对应的记录,定位具体的sql。

Java中的 service是为 controller服务得 因此建议不要使用 service调用另一个 service。因为你得事务使用注解方式来控制 那么执行 service开始时已经开始事务,而你在调用其他 service时,这个 service中可能存在数据库操作访问,如果其中有对事务操作的或者大量操作 DB的,很可能影响你这个 service操作 DB,因此而延迟! mysql其实应用于大批量操作还是有缺陷滴 可能考虑使用企业版数据库

一、根据阿里云RDS的监控分析,你的系统中有持续时间很长的事务。根据这一点可能有两种情况。
1、有慢查询,去看一下慢SQL的监控。确认一下。
2、开启事务的service中是否有批处理、耗时长的三方接口调用、进程休眠等程序,导致进程一直卡在service里面,service程序执行不完,导致事务不能提交。有能力可以搞个切面,抓取一下每个service方法的执行时间。搞不定可以私聊我。
二、上面有很多兄弟也说过了,程序里面@Transactional(rollbackFor = {Exception.class, RuntimeException.class}) 会回滚事务,可以找一下日志分析回滚原因,是获取不到链接?还是由于第一点长事务导致数据获取锁失败?找到错误日志就可以分析得出来。如果没有日志,就在出错的save()和update()方法try catch住打印出来。
三、数据库连接数有很多限制,大概在下面几个地方:
1、程序中连接池配置的连接数。(你给出的配置并发数不是很高应该够用,你要评估一下你项目的tps和qps)。
2、RDS数据库中每个账号的连接数限制,有些项目部署的集群,用的同一个账号,也可能导致连接数不够用。我映像中mysql5.7中大概是单个用户200个链接。你可以看一下RDS的文档。
3、RDS根据不同的配置有连接数限制。看一下RDS的监控。是否触达上限。

首先:要确定你的数据库是否是支持事务,并且一定要关闭连接池的auto-commit自动提交功能
几种事务的配置方法:

  1. myBatis单独使用时,使用SqlSession来处理事务:
public class MyBatisTxTest {  
  
    private static SqlSessionFactory sqlSessionFactory;  
    private static Reader reader;  
  
    @BeforeClass  
    public static void setUpBeforeClass() throws Exception {  
        try {  
            reader = Resources.getResourceAsReader("Configuration.xml");  
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);  
        } finally {  
            if (reader != null) {  
                reader.close();  
            }  
        }  
    }  
      
    @Test  
    public void updateUserTxTest() {  
        SqlSession session = sqlSessionFactory.openSession(false); // 打开会话,事务开始  
          
        try {  
            IUserMapper mapper = session.getMapper(IUserMapper.class);  
            User user = new User(9, "Test transaction");  
            int affectedCount = mapper.updateUser(user); // 因后面的异常而未执行commit语句  
            User user = new User(10, "Test transaction continuously");  
            int affectedCount2 = mapper.updateUser(user2); // 因后面的异常而未执行commit语句  
            int i = 2 / 0; // 触发运行时异常  
            session.commit(); // 提交会话,即事务提交  
        } finally {  
            session.close(); // 关闭会话,释放资源  
        }  
    }  
}  

  1. 和Spring集成后,使用Spring的事务管理:

a. @Transactional方式:

在类路径下创建beans-da-tx.xml文件,在beans-da.xml的基础上加入事务配置:

<!-- 事务管理器 -->  
<bean id="txManager"  
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource" />  
</bean>  
  
<!-- 事务注解驱动,标注@Transactional的类和方法将具有事务性 -->  
<tx:annotation-driven transaction-manager="txManager" />  
  
<bean id="userService" class="com.john.hbatis.service.UserService" />  

服务类:

@Service("userService")  
public class UserService {  
  
    @Autowired  
    IUserMapper mapper;  
  
    public int batchUpdateUsersWhenException() { // 非事务性  
        User user = new User(9, "Before exception");  
        int affectedCount = mapper.updateUser(user); // 执行成功  
        User user2 = new User(10, "After exception");  
        int i = 1 / 0; // 抛出运行时异常  
        int affectedCount2 = mapper.updateUser(user2); // 未执行  
        if (affectedCount == 1 && affectedCount2 == 1) {  
            return 1;  
        }  
        return 0;  
    }  
  
    @Transactional  
    public int txUpdateUsersWhenException() { // 事务性  
        User user = new User(9, "Before exception");  
        int affectedCount = mapper.updateUser(user); // 因后面的异常而回滚  
        User user2 = new User(10, "After exception");  
        int i = 1 / 0; // 抛出运行时异常,事务回滚  
        int affectedCount2 = mapper.updateUser(user2); // 未执行  
        if (affectedCount == 1 && affectedCount2 == 1) {  
            return 1;  
        }  
        return 0;  
    }  
} 


在测试类中加入:

Java代码   
@RunWith(SpringJUnit4ClassRunner.class)  
@ContextConfiguration(locations = { "classpath:beans-da-tx.xml" })  
public class SpringIntegrateTxTest {  
  
    @Resource  
    UserService userService;  
  
    @Test  
    public void updateUsersExceptionTest() {  
        userService.batchUpdateUsersWhenException();  
    }  
  
    @Test  
    public void txUpdateUsersExceptionTest() {  
        userService.txUpdateUsersWhenException();  
    }  
} 


如果可以测试环境复现 建议在函数上加个锁 测试一下
或者把数据库事务隔离级别设置到最大测试一下
因为mysql大量事务的时候可能会存在丢失的情况

  1. 2个环境的差异在哪里?
  2. 线上的线程dump看看, 卡在哪了?
  3. 考虑下redisComponent, 是不是它卡死了, 例如: 自己去获取了reds connection, 但是没有释放

可以把锁的部分代码贴出来吗,线程锁的问题导致没有报错,但是同时执行,最终会有部分数据未提交

img


标红的地方每个地方都加个日志打印么,看方法执行的次数是否的确都执行了那么多次。

看一下Close_Wait,如果很多,说明就是连接池或者并发线程数不够用。原因只有一个业务处理太慢。

有没有可能是因为第三方接口导致的问题,调用第三方接口改成异步调用或者通过调度任务的形式实现试试。
还有需要排除这个注解@SneakyThrows的影响,这个注解是做了什么功能操作

不知道,但是你可以试试,数据库连接池休眠,加上这个配置

autoReconnect=true&failOverReadOnly=false

autoReconnect: 驱动程序是否应该尝试重新建立连接? 如果启用了,驱动程序将会对陈旧或死亡连接上发出的查询抛出异常,这些查询属于当前事务,但是在新事务中对连接发出的下一个查询之前会尝试重新连接。
failOverReadOnly:自动重连后连接是否设置为‘只读’模式 ,false是为了避免在重连后,有数据更新或插入操作在‘read-only’连接中无法执行而抛出新的异常

如:

spring.datasource.url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&autoReconnect=true&failOverReadOnly=false&useUnicode=true&characterEncoding=utf-8&useLegacyDatetimeCode=false&serverTimezone=UTC

我觉得我们应该先锁定问题出现的点,现在理论上没有能够分析出点在那里,那么我们就是用比较笨的方法,传说中的排除法,理论分析不到位就用事实来验证;
关注点分析:
1、关于第三方接口调用部分,虽然涉及rpc,但方法返回时间短,并且没有异常抛出,可能性较低;
2、调用了包含编程式事务方法,本问题就是事务未被提交问题,可能性较高;
建议优先测试关注点2
方式1、直接干掉这部分代码(直截了当,但是可能需要自己后续处理测试阶段造成的数据污染);
方法2、添加一次层service,将方法包装一下并且添加上异步操作(@Async),但,但,但监听该方法的返回,然后继续后面逻辑(通过异步的方式切断主线程事务可能被编程式事务干扰的可能,业务流没有变化,建议使用)。

会不会是请求并发数过多,数据库连接池或者数据库无法提供过多的数据库连接,

//调用三方接口,trade方法里面会有save、select、和http的相关操作
ClientResultDTO resultDTO = payChannelService.trade();

然后你这个事务还包含有http请求,如果被调用方的操作时间很长的话,就会导致长事务,从而导致事务超时造成回滚

有没有一种可能,你的远程调用没有超时,在没有返回的时候一直在等待。所以开启了很多的事物没有提交

代码方面没看出什么问题
ClientResultDTO resultDTO = payChannelService.trade(); 会不会问题在这里 你的rpc调用超时时间是多少 有没有排查出问题时候的远程接口是否畅通

一般业务不会吧rpc 调用整入业务层

第一种说法(个人觉得这个真的非常规):
mapper.xml文件中 条件判断错误,类似这种

nmpa_id,

说是字段不存在,查之,非问题所在

其他说法:
常规@Transactional的相关错误,这一块的各种博客非常多,解释的也很详细,总结一下大致以下,不清楚的可以自行搜索.
1.方法必须是public
2.内部调用的问题
3.异常要抛出来,不要try catch 处理掉了
4.抛出的异常不支持回滚,unchecked什么的

然后就是各种我查询的配置的相关问题
1.启动类要加@EnableTransactionManagement
2.application.yml 要加

spring:
transaction:
rollback-on-commit-failure: true

.要配置sqlsessionfactory
4.要配置transactionManage

问题的初始是,根本就没有事务,就算加了@transaction标签,也没有事务,

于是有人说不同的数据源要配不同的transactionManager,于是有了如下错误配置

//@Bean(name = TransManageType.SQLSERVER)
//@Primary
public DataSourceTransactionManager mssqlTransactionManager(@Qualifier("sqlserverDataSource") DataSource sqlserverDataSource)
{
return new DataSourceTransactionManager(sqlserverDataSource);
}

//@Bean(name = "mssqlErpTransactionManager")
public DataSourceTransactionManager mssqlErpTransactionManager(@Qualifier("sqlserverErpDataSource") DataSource sqlserverErpDataSource)
{
    return new DataSourceTransactionManager(sqlserverErpDataSource);
}

这里确实是配了不同的数据源,也是这里配置了之后,出现了明明已经加入了事务,为什么就不回滚的问题springboot+mybatis不能回滚

最终问题解决(正确配置):
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
{
return new DataSourceTransactionManager(dynamicDataSource);
}

没有配多事务管理,也可能是这个加入了动态了数据源后,spring自动根据数据源切换,进行事务的切换吧,具体源码没有深究,不过最终问题解决了.

springboot的sqlsessionFactory配置,其实在application.yml文件中就能配置了,也就是下面这一段

MyBatis

mybatis:
# 搜索指定包别名
typeAliasesPackage: com.xxx
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml

给个最小单元复现代码吧

https://www.likecs.com/show-203524071.html

你再重新部署一套,看看是否还有问题,如果没有问题,就把有问题那一套下了就行了,找这里面的变量。

不知道


指事务非自动提交,此句执行以后,每个SQL语句或者语句块所在的事务都需要显示"commit"才能提交事务。

小实验
做了一个小实验,打开一个窗口,开了一个连接,set autocommit = 0开冲。
set autocommit=0为何不起作用及对于事务隔离性的理解
发现怎么不起作用???
经过同事指点,又开了另外的一个窗口,建立了数据库连接。
set autocommit=0为何不起作用及对于事务隔离性的理解
发现这个新建立的窗口是看不到ppp这一行的,也就是看不到之前那个窗口的那次insert结果。

set autocommit=0为何不起作用及对于事务隔离性的理解
直到之前的那个窗口提交后(commit后),才能在这个窗口看得到。

思考
其实一个窗口对应着一个数据库连接,那么在同一个窗口里 = 在同一个连接里 = 在同一个事务里。
那么问题来了,事务的隔离性隔离的到底是什么。从上面看的话,隔离的是这个数据库的多个连接。也就是说事务和连接是一对一的关系。

tips
set autocommit 和 START TRANSACTION
不管autocommit 是1还是0 ,START TRANSACTION 后,只有当commit数据才会生效,ROLLBACK后就会回滚。
当autocommit 为 0 时,不管有没有START TRANSACTION。
只有当commit数据才会生效,ROLLBACK后就会回滚。
为什么不推荐set autocommit=0
如果使用set autocommit=0,如果数据库是长连接,这就导致接下来的查询都在事务中,出现了长事务,长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。

如何避免长事务对业务的影响?
出自林晓斌老师的mysql45讲。
首先,从应用开发端来看:

确认是否使用了set autocommit=0。这个确认工作可以在测试环境中开展,把MySQL的general_log开起来,然后随便跑一个业务逻辑,通过general_log的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成1。

确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用begin/commit框起来。我见过有些是业务并没有这个需要,但是也把好几个select语句放到了事务中。这种只读事务可以去掉。

业务连接数据库的时候,根据业务本身的预估,通过SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。(为什么会意外?在后续的文章中会提到这类案例)

其次,从数据库端来看:

监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill;
Percona的pt-kill这个工具不错,推荐使用;
在业务功能测试阶段要求输出所有的general_log,分析日志行为提前发现问题;
如果使用的是MySQL 5.6或者更新版本,把innodb_undo_tablespaces设置成2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。