并发使用MyBatis selectKey 更新update问题总结

问题业务场景:按日期累加的流水号出现大量重复流水号问题
代码模拟场景:

		String sss = TestMapper.getMaxSerialNo("LiuShuiHaoCuoLuan");
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            Thread a = new Thread(() -> {
                String str = businessSerialNoService.getMaxSerialNo("ImTest");
                System.out.println(Thread.currentThread().getName() + "......." + finalI + ":" + str);
                try {
                	//睡眠的作用让cpu进行线程调度切换
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            });
            i.start()
        }
<insert id="getMaxSerialNo">
        <selectKey keyProperty="data.num" resultType="int" order="AFTER">
            SELECT S.NUM FROM TEST_NUMS WHERE S.TYPE = #{data.type,jdbcType=VARCHAR}
            AND S.TEST_DATE = #{data.testDate,jdbcType=VARCHAR};
        </selectKey>
        insert into TEST_NUM(TYPE , TEST_DATE, NUM)
        values (#{data.type,jdbcType=VARCHAR}, #{data.testDate,jdbcType=VARCHAR},
        #{data.num,jdbcType=DECIMAL})
        ON DUPLICATE KEY UPDATE NUM = NUM + 1;
    </insert>

本地单机测试都出现高概率重复(30%)的现象,问题很严重,跟进解决。贴出一小段日志供参考

2021-08-17 17:39:16.606 DEBUG [Thread-238] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - ==> Parameters: ImTest(String), 20210817(String) 
2021-08-17 17:39:16.607 DEBUG [Thread-249] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==    Updates: 2 
2021-08-17 17:39:16.607 DEBUG [Thread-242] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==      Total: 1 
2021-08-17 17:39:16.609 DEBUG [Thread-249] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - ==>  Preparing: SELECT S.NUM FROM C_BUSINESS_NUM S WHERE S.BUSINESS_TYPE = ? AND S.BUSINESS_DATE = ?;  
2021-08-17 17:39:16.607 DEBUG [Thread-236] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==      Total: 1 
2021-08-17 17:39:16.609 DEBUG [Thread-235] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==      Total: 1 
2021-08-17 17:39:16.609 DEBUG [Thread-241] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==      Total: 1 
2021-08-17 17:39:16.610 DEBUG [Thread-249] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - ==> Parameters: ImTest(String), 20210817(String) 
2021-08-17 17:39:16.609 DEBUG [Thread-245] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - ==> Parameters: ImTest(String), 20210817(String), 1(Integer) 
2021-08-17 17:39:16.610 DEBUG [Thread-253] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - ==>  Preparing: insert into C_BUSINESS_NUM(BUSINESS_TYPE, BUSINESS_DATE, NUM) values (?, ?, ?) ON DUPLICATE KEY UPDATE NUM = NUM + 1;  
2021-08-17 17:39:16.611 DEBUG [Thread-253] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - ==> Parameters: ImTest(String), 20210817(String), 1(Integer) 
2021-08-17 17:39:16.612 DEBUG [Thread-238] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==      Total: 1 
2021-08-17 17:39:16.612 DEBUG [Thread-234] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==      Total: 1 
2021-08-17 17:39:16.613 DEBUG [Thread-249] [o.a.i.l.j.BaseJdbcLogger:debug:159][][][] - <==      Total: 1 
......63:191
......66:196
......61:193
......60:197
......65:197
......59:197
......58:197
......62:197
......73:197

问题定位方向:

  1. select 和 update 并非同一个事务,可能是spring的事务设置问题
    尝试策略:
    增加事务注解,设置默认事务管理器(我这里是双数据源,才设置的),设置为MySQL的默认隔离级别
    @Transactional(propagation = Propagation.REQUIRES_NEW, value = “mysqlTransactionManager”, isolation = Isolation.REPEATABLE_READ)
    效果:对序列的产生没有影响,但是发现了隐藏bug彩蛋
    隐藏彩蛋: 由于是多数据源,如果不指定Transactional的value,是无法设置隔离级别RR的,也就是说虽然单独增加了注解Transactional,但是默认级别降级到了Oracle RC。这个是之前没发现的,后面再具体研究如何处理多数据源的默认隔离级别的问题。

  2. 语句语法有问题
    尝试策略: 返回修正mysql的语法,包括selectKey 、replace into、for update、乐观锁表锁的使用。此处浪费了大量的时间,应该有4、5个小时。实际是方向走错了
    展示下调整的另外一种bug语句,这种重复的几率更高了,100次才生成20个序号,数据库值也错乱了,因为先select再update。

        <insert id="getNewSerialNo">
            <selectKey keyProperty="data.num" resultType="int" order="BEFORE">
                SELECT IFNULL((SELECT S.NUM FROM C_BUSINESS_NUM S WHERE
                S.BUSINESS_DATE = #{data.businessDate,jdbcType=VARCHAR}
                AND S.BUSINESS_TYPE = #{data.businessType,jdbcType=VARCHAR}
                )+1,1) AS NUM;
            </selectKey>
            REPLACE INTO C_BUSINESS_NUM (business_date,business_type,num)
            VALUES(#{data.businessDate,jdbcType=VARCHAR},#{data.businessType,jdbcType=VARCHAR}
            ,#{data.num,jdbcType=DECIMAL})
        </insert>
    
  3. MyBatis的一级缓存问题,导致每次的select都取了缓存最新的查询值,理论应该返回同一个值才对
    **尝试策略:**犯了一个严重意识上的错误,认为一级缓存不会影响多个事务,最先排查了是否关闭一级缓存,发现Oracle关闭了,MySQL没关闭,Oracle之前也出现过重复数据,所以认为一级缓存不会影响。因为没去测试,导致白白浪费了4个小时,两行代码就能搞定的问题。

    SqlSessionFactory sqlSessionFactory = sessionFactory.getObject();
        if (sqlSessionFactory.getConfiguration() != null) {
            sqlSessionFactory.getConfiguration().setCacheEnabled(false);
            sqlSessionFactory.getConfiguration().setLocalCacheScope(LocalCacheScope.STATEMENT);
            log.info("设置默认SqlSessionFactory的缓存开关为关闭,避免分布式或者单事务内多次查询出现缓存问题!");
        }
    

遗留问题跟进:

  1. 为什么在Service层增加了 REQUIRES_NEW 的事务处理,RR级别,还会出现不再同一个事务的问题,是否是sqlSession复用了?
  2. MyBatis一级缓存的逻辑处理流程
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值