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; }
总结原因:
事物一直没有被提交,事物状态是通过线程缓存实现的,当线程回收到线程池,后面再分分派的这个线程使用的同一个连接和事物,事物其实一直挂起