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 @ 2016-05-07 11:27  意犹未尽  阅读(10300)  评论(0编辑  收藏  举报