java陷阱之spring事物未提交和回滚导致不可预知问题

建议

强烈建议不要手动打开和关闭事物,交给spring 托管

案发现场

复制代码
//防止全局配置了 所以这里定义sprnig 不托管事物
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public boolean  deductNumber(Long id,int i){
        
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//设置事物传播行为
        TransactionStatus status = null;
        try {
            //开启事物
            status = transactionManager.getTransaction(def);
            if(id==null){
                return  false;//库存编号不能为空
            }
            if(i<=0){
                return false;//扣除库存不能小于0 
            }
            //提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
              return false;
        }
        return true;
    }
复制代码

导致问题

线上出现 订单下单 提示用户订单创建成功  但是订单不在了 对应的钱没扣 库存也没扣   看代码没有问题 日志也是正常打印 没抛错没回滚

mysql 部分表出现锁等待 单表操作无并发也出现锁等待

自己的坑自己填

1.day 1

怀疑是数据库的问题 事物提交了 但是数据库没持久  在下单操作打印了日志判断是否回滚并打印日志

if(status.isRollbackOnly()){
            logger.info("======事物已回滚=========");
            return false;
        }

2.day 2

第二天又出现这种情况 同时发现伴随着后台有个事物一一直 在select * from information_schema.innodb_trx; runing 持续半个小时以上

但是发现丢单的地方======事物已回滚========

这个时候很疑惑 因为下单很多逻辑怀疑是某个方法设置了回滚状态但是抛出true  来log4j开启事物管理器debug日志

log4j.logger.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG

开启之后开启事物 设置事物状态合并事物都会打印对应的日志
同时为了定位到runing的事物是在哪个地方 在重写了log4j layout 打印了事物号和线程号

log4j配置log4j.appender.stdout.layout =com.chinayanghe.dms.log4jUtil.MyPatternLayout

复制代码
public class MyPatternLayout extends PatternLayout {
    /**
     * 日志打印
     */
    protected Logger logger = LoggerFactory.getLogger(getClass());
    private static ThreadLocal<String> localTreadIds = new ThreadLocal<String>();

    public static void removeTreadLocalCache() {
        localTreadIds.remove();
    }
    @Override
    public String format(LoggingEvent event) {
        String logStr = super.format(event);
        try {
            if (logStr.indexOf("SELECT trx_id FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID()") < 0
                    &&logStr.indexOf("SELECT trx_mysql_thread_id FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();")<0) {
                //表示是sql打印尝试获得事物号
                if (logStr.indexOf("[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:145)] ==>  Preparing:") >= 0) {

                    WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
                    String treadId = localTreadIds.get();
                    if (StringUtil.isEmpty(treadId)) {
                        JDBCTransactionInfoService jdbcTransactionInfoService = (JDBCTransactionInfoService) wac.getBean("jDBCTransactionInfoServiceImpl");
                        List<String> treadIds = jdbcTransactionInfoService.selectTrxMysqlThreadId();
                        if (CollectionUtils.isNotEmpty(treadIds)) {
                            treadId = treadIds.get(0);
                            localTreadIds.set(treadId);
                        }
                    }
                    if (treadId != null) {
                        logStr = logStr.replace("[DEBUG]", "[DEBUG][事务号" + treadId + "]");
                    }

                } else if (logStr.indexOf("Transactional code has requested rollback") >= 0) {
                    logStr = logStr.replace("Transactional code has requested rollback", "Transactional code has requested rollback,线程id:" + Thread.currentThread().getId());
                }
            } else {
                return "";
            }
            long treadId = Thread.currentThread().getId();
            logStr = logStr.replace("[DEBUG]", "[DEBUG][线程id:" + Thread.currentThread().getId() + "]").replace("[INFO]", "[INFO][线程id:" + treadId + "]");
        } catch (Exception e) {
            logger.info(e.getMessage());
        }
        return logStr;
    }
}
复制代码

3.day3出现丢单根据日志发现 丢单的都是同一个线程id 事物号也相同  同时丢单的地方都没有开启事物日志而是事物已存在合并的日志

根据日志 一个一个看 发现规律 线程id最后一次打印 creating transaction  之后 再也没打印  然后定位到最后一次打印事物 的方法是手动开启事物的方法  有个地方忘记回滚 直接return了

这个时候反应过来 是这个方法没提交一直挂起  spring事物是基于线程缓存 tomcat会回收线程到线程池  下单分配到这个线程  因为手动开启事物还存在 所以公用同一个事物  事物一致挂起 当遇到RR模式锁等待抛出wai lock

statu的回滚状态会设置为true 所以才会出现我的下单日志打印都是回滚 

优化代码

复制代码
 //防止全局配置了 所以这里定义sprnig 不托管事物
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public boolean  deductNumber(Long id,int i){

        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//设置事物传播行为
        TransactionStatus status = null;
        try {
            //开启事物
            status = transactionManager.getTransaction(def);
            if(id==null){
                transactionManager.rollback(status);
                return  false;//库存编号不能为空
            }
            if(i<=0){
                return false;//扣除库存不能小于0
            }
            //提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
              return false;
        }finally {
            //加上此代码 防止忘记
            if(status!=null&&!status.isCompleted()){
                transactionManager.rollback(status);
                return false;
            }
        }

        return true;
    }
复制代码

 总结原因:

            事物一直没有被提交,事物状态是通过线程缓存实现的,当线程回收到线程池,后面再分分派的这个线程使用的同一个连接和事物,事物其实一直挂起

posted @   意犹未尽  阅读(10922)  评论(0编辑  收藏  举报
编辑推荐:
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
点击右上角即可分享
微信分享提示