首页/新闻资讯/正文详情

Java事务原理与失效排查:@Transactional、传播行为和分布式事务

发布时间:2026/11/3 16:46:26 来源:云帆数科 栏目:资讯中心
Java事务原理与失效排查:@Transactional、传播行为和分布式事务
线上对账任务在一个周五晚上跑挂了日志里干干净净一条报错都没有但数据就是没对上。翻代码才发现那个加了Transactional的方法被同一个类里的另一个方法直接调用注解形同虚设中间某一步抛出的异常又被上层catch掉前面写进去的半截数据就那样留在了库里。这个坑我在不同团队里见过不止五次每次都要重新给新人讲一遍Java 事务到底是怎么跑起来的。很多人对事务的理解停留在“加个注解就完事了”一旦出了问题就只能靠猜。这篇东西想干的事很具体把 Java 事务从数据库底层到 Spring 代理层拆开讲清楚把传播行为、隔离级别、回滚规则这几个天天用却常常配错的开关说明白再把事务失效的十几种现场和分布式事务的选型逻辑一次性摊开。不管你是刚写完第一个 CRUD 接口的新人还是已经在处理跨库一致性问题的老手应该都能从里面挑到能直接抄走的东西。1. 先把事务这件事说透从 ACID 到 Java 里的三条落地路径1.1 四个字母背后到底是谁在兜底很多人背 ACID 背得很熟但一问“原子性是谁保证的”就卡住了。原子性不是 Java 给的也不是 Spring 给的是数据库的 undo log 给的。InnoDB 在修改数据之前会先把旧值记到 undo log 里事务回滚时按相反的顺序把旧值写回去这就是原子性的物理实现。持久性是 redo log 在扛事务提交时先把变更写进 redo log 并刷盘innodb_flush_log_at_trx_commit1时每次提交都刷哪怕此时数据页还没落盘崩溃重启后也能从 redo log 重放出来。隔离性靠的是锁加 MVCC一致性其实不是单独实现的它是前三个加上业务约束共同作用的结果。把这个链路想清楚很多困惑就自动消失了。比如为什么“事务里发了 MQ 消息回滚了消息却发出去了”——因为 MQ 不在数据库的事务边界里它根本不受 undo log 管。再比如为什么大事务会让主从延迟变得很难看——redo log 一路涨从库回放要时间。我在排查问题时有个习惯先判断问题出在“数据库事务边界内”还是“边界外”这一步能砍掉一半的排查方向。提示innodb_flush_log_at_trx_commit和sync_binlog这两个参数决定了“提交”到底有多“实”。前者为 1、后者为 1 是最安全的组合代价是每次提交至少两次刷盘如果业务能容忍极端情况下丢少量最近事务把它们调成 2 或 0 能换来明显的吞吐提升但这个决定必须由业务方拍板不能由开发顺手改。1.2 手写、模板、注解三条路径的取舍Java 里操作事务本质上就三种写法。最原始的是直接玩 JDBCconn.setAutoCommit(false)然后commit()或rollback()finally里把连接还回去。这种写法现在基本只在老代码或者极特殊的场景里出现因为它的模板代码太多而且一旦中间return或者抛异常没处理好连接就会带着未提交的事务回到池子里后果很难看。往上一层是编程式事务Spring 提供的TransactionTemplate是代表。它把“开事务、执行、提交/回滚、释放资源”这套流程包成了一个回调你只需要关心execute里那段业务逻辑。我个人的经验是凡是事务边界需要动态决定的场景优先用TransactionTemplate。比如“根据参数决定这段逻辑要不要单独一个事务”“在一个循环里分批提交”用注解反而别扭用模板几行代码就说清楚了。最上层就是声明式事务也就是Transactional。它的好处是零侵入、可读性好坏处是它依赖代理一旦调用路径绕开了代理注解就是一行注释。三种方式没有高下之分只有场景之分。我的实际分布大概是业务主流程 90% 用注解批处理和数据修复脚本用模板剩下极少数跟框架底层打交道的地方才手写。1.3 为什么绝大多数业务代码只该用注解有人会问既然模板更可控为什么不干脆全用模板因为事务边界本质上是业务语义的一部分不是技术细节。“下单扣库存”这件事要么全成要么全败这个边界应该写在方法签名上让人一眼看到而不是埋在十几行代码中间。注解的价值就在这里它把事务边界声明化了代码评审的时候扫一眼就能判断这个边界划得对不对。但声明化是有代价的代价就是“看不见”。你看不到连接什么时候获取、什么时候提交出问题时就没有抓手。所以用注解的前提是你得知道它背后发生了什么。这也是为什么我坚持让团队里的新人在用Transactional之前先用模板写一遍带事务的批量插入感受一下连接复用和回滚的时机。感受过一次后面看注解就不会再当成魔法。2. Transactional 背后的机制拆解代理、连接、同步器2.1 一个注解是怎么变成一次 commit 的Transactional生效的前提是当前类被 Spring 管理并且调用方拿到的是代理对象。Spring 在启动时扫描到这个注解会给对应的 Bean 创建一个代理有接口时默认 JDK 动态代理没有接口用 CGLIB代理里织入一个TransactionInterceptor。调用进来时拦截器先根据方法上的注解解析出一个TransactionDefinition包含传播行为、隔离级别、超时、是否只读、回滚规则再交给PlatformTransactionManager去开启事务。DataSourceTransactionManager的处理逻辑很朴素从DataSource拿一个连接setAutoCommit(false)把这个连接通过TransactionSynchronizationManager绑定到当前线程的ThreadLocal上然后放行执行业务方法。方法正常返回就commit抛异常就按回滚规则判断要不要rollback最后清理ThreadLocal、恢复连接的autoCommit、归还连接池。整个过程记住了这一条链路后面所有“为什么失效”的问题都能自己推出来。注意代理意味着“调用必须从外面进来”。this.method()这种自调用走的是原始对象压根不经过代理拦截器不会被触发注解写得再全也没用。这是事务失效里出现频率最高的一种没有之一。2.2 连接绑定与事务同步器同一事务里为什么能拿到同一个 ConnectionTransactionSynchronizationManager这个类值得单独拎出来说。它内部维护了一组ThreadLocal分别存当前线程绑定的DataSource到ConnectionHolder的映射、当前事务是否活跃、隔离级别、只读标志、以及一堆同步回调。JdbcTemplate、MyBatis 的SqlSessionTemplate在拿连接时都不是直接向数据源要而是先问这个管理器“当前线程有没有已绑定的连接”有就直接复用没有才去池子里取。这就是为什么在同一个事务方法里连续调十个 DAO用的还是同一个物理连接也才会有“事务里改了数据同事务内查询能看到”的现象。同样是因为这个机制Transactional方法里新起一个线程去查库会拿不到外层事务的连接自然也就看不到未提交的数据——不是数据库不给你看是你换了条线程ThreadLocal是空的。// 事务提交后再执行某个动作避免消息发出去但事务回滚了 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCommit() { // 这里执行缓存删除、消息投递等事务后动作 cacheEvict(orderId); } });这段代码在实际项目里非常有用。缓存删除、异步通知这类动作如果放在事务方法体里直接执行一旦事务回滚动作已经生效了数据和缓存就不一致。挂到afterCommit上才能保证“事务真的提交了才执行”。2.3 事务的挂起与恢复REQUIRES_NEW 到底干了什么当内层方法的传播行为是REQUIRES_NEW时AbstractPlatformTransactionManager会发现当前线程已经有一个活跃事务于是执行“挂起”把当前的ConnectionHolder从ThreadLocal里解绑暂存到一个SuspendedResourcesHolder对象里然后向数据源要一个新的连接开启新事务。内层执行完提交或回滚再把之前暂存的那个 holder 重新绑回ThreadLocal恢复外层事务。这里有两个容易忽略的后果。第一挂起期间外层事务的连接一直被占着不放内层又占一个一次嵌套就同时占用两个连接。如果外层方法在一个循环里调用内层REQUIRES_NEW方法连接池的maximumPoolSize又设得比较小很容易在并发稍高的时候直接把池子打满表现为请求全部卡在获取连接上日志里一堆获取连接超时。第二两个事务是两个独立的连接、独立的事务内层提交了外层回滚内层的数据是回不来的。所以REQUIRES_NEW最适合的场景是“记录操作日志”这类“不管主流程成不成功都要留下痕迹”的旁路逻辑不适合用来做业务数据的分离。NESTED则是另一套机制它不换连接而是在同一个连接上用Savepoint打一个点。内层回滚只回滚到这个点外层不受影响但外层一旦回滚内层所有的变更都会跟着没。它依赖 JDBC 驱动的 savepoint 支持用之前最好确认一下驱动和数据库都支持别想当然。3. 七种传播行为与四种隔离级别选错一个就是事故3.1 传播行为速查与真实取舍传播行为决定了“当调用发生时已经存在一个事务该怎么办”。七种取值我按使用频率排一下顺便把容易混淆的点标出来。传播行为有外层事务时无外层事务时典型用途坑点REQUIRED默认加入外层新建99% 的业务方法内层异常会导致整个外层回滚REQUIRES_NEW挂起外层新建新建日志、审计、独立进度记录双连接占用内层提交不可撤销NESTED建 savepoint新建可部分回滚的子流程依赖驱动支持外层回滚会带走内层SUPPORTS加入外层不加事务纯查询方法单独调用时无事务行为不一致NOT_SUPPORTED挂起外层不加事务耗时查询、大批量导出挂起期间连接被占着MANDATORY加入外层抛异常强制要求上游开事务的底层方法上游忘加就炸NEVER抛异常不加事务明确禁止在事务中执行的逻辑用得极少这里最需要警惕的是REQUIRED的“连坐”效应。内层方法抛了异常如果这个异常一路往外抛外层事务会被标记为 rollback-only最后提交时抛UnexpectedRollbackException提示“事务被标记为只回滚”。很多人第一次遇到这个异常会一头雾水明明代码里已经catch住了为什么还回滚。原因是内层事务拦截器在捕获异常时已经把整个事务标记成了 rollback-only外层即使吞掉异常最后也提交不了。要解决只有两条路要么让内层用REQUIRES_NEW独立出去要么别把可能失败的子逻辑放在同一个事务边界里。3.2 隔离级别和数据库实现的差异隔离级别这块Spring 的默认值是ISOLATION_DEFAULT意思是不指定跟随数据库。MySQL InnoDB 默认是REPEATABLE_READOracle 和 PostgreSQL 默认是READ_COMMITTED。这个差异带来的行为区别非常大同一个应用换了个数据库脏读、不可重复读、幻读的表现可能完全不同。隔离级别脏读不可重复读幻读实现手段READ_UNCOMMITTED可能可能可能基本无隔离READ_COMMITTED否可能可能MVCC 快照按语句生成REPEATABLE_READ否否InnoDB 下基本避免MVCC 快照按事务生成 间隙锁SERIALIZABLE否否否全部读加锁有个细节值得说在REPEATABLE_READ下事务开始后第一次读建立快照后续所有普通查询都读这个快照所以别的会话刚提交的数据你是看不到的。这会导致一种很典型的线上现象——运营在后台改了配置应用读到的还是旧值重启或者事务结束后才生效。看到这种“改了不生效”的反馈先想想是不是被 RR 的快照挡住了。另外Transactional(isolation ...)一旦设置了非DEFAULT的值Spring 在判断能否复用已有连接时会额外检查隔离级别是否一致不一致就触发挂起、重新取连接。所以不要随手在方法上写一个isolation它可能在你没意识到的地方多消耗一个连接。3.3 只读事务、超时、回滚规则这三个开关readOnly true不是摆设。对 MySQL 来说它会提示驱动和数据库这是个只读事务InnoDB 可以减少一些内部开销对某些数据库比如加了只读路由的主从架构它还能帮你把请求路由到从库。但要注意readOnly只是提示不是强制在只读事务里执行写操作多数数据库不会报错只会默默写进去。所以它带来的是性能优化不是安全约束别指望用它来防止误写。timeout的单位是秒默认是数据库的innodb_lock_wait_timeout通常 50 秒。这个默认值在生产里往往太长一个被慢 SQL 卡住的事务能占着连接和锁将近一分钟连锁反应很严重。我给核心业务方法设的超时通常是 3 到 10 秒具体看这条链路最长能容忍多久。rollbackFor是另一个高频坑。Spring 默认只在遇到RuntimeException和Error时回滚遇到受检异常Exception及其子类默认不回滚。如果你的方法签名上写着throws IOException然后内部真的抛了事务会照常提交脏数据就这么留下了。所以我的习惯是统一写Transactional(rollbackFor Exception.class)宁可多回滚也不要少回滚。Transactional( rollbackFor Exception.class, // 受检异常也回滚 timeout 5, // 秒防止慢 SQL 长期占锁 propagation Propagation.REQUIRED ) public void settleOrder(Long orderId) throws IOException { // 业务逻辑 }顺便提一句事务里不该做的事不要在事务方法里调用远程接口、发消息、做文件上传。这些操作耗时不可控一次网络抖动就能让事务从几十毫秒变成几秒锁的持有时间跟着成倍上涨。我见过一个事务里嵌了三次外部 HTTP 调用高峰期直接把数据库连接池打满最后是靠把远程调用全挪出事务边界才解决的。4. 事务失效的十种现场与排查手法4.1 代码层面的坑自调用、权限、异常被吞自调用排第一前面已经说过。判断方法很简单看调用方是不是从代理对象发起的。快速验证的办法是在方法里打印this.getClass()如果是XxxServiceImpl而不是XxxServiceImpl$$EnhancerBySpringCGLIB说明你拿到的根本不是代理。解决办法有三种把方法挪到另一个 Bean 里最干净、注入自身代理Lazy注入自己或者AopContext.currentProxy()需要开exposeProxy、或者干脆用TransactionTemplate。第二个是方法可见性。Spring 5 之后protected和包级可见的方法在某些代理模式下也能被拦截但强烈建议统一用public别去赌代理实现细节。private方法一定不生效final方法在 CGLIB 代理下也不生效因为没法覆写。第三个是异常被吞。这段代码你一定见过Transactional public void doSomething() { try { repository.save(entity); otherService.remoteCall(); // 这里抛了异常 } catch (Exception e) { log.error(记录一下, e); // 吞掉了事务正常提交 } }异常被catch住没有重新抛出拦截器感知不到失败自然就提交了。要么在 catch 里重新抛一个运行时异常要么显式调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。后者我一般只在“确实需要吞掉异常但要回滚”的场景用因为它可读性比较差新人看了容易迷惑。第四个是对象压根不归 Spring 管。new XxxService().doSomething()这种写法代理根本不存在注解等于注释。第五个是调用发生在异步线程里ThreadLocal不传递事务上下文丢失。这两个都属于“看起来在 Spring 里实际上不在”。4.2 数据库与连接池层面的坑引擎、隐式提交、连接超时代码没问题事务还是不生效那就要往数据库层看。最常见的是存储引擎不支持事务。MySQL 5.5 之前的默认引擎是 MyISAM它不支持事务注解加得再标准也白搭。现在虽然默认 InnoDB但历史迁移过来的表、手写建表语句忘了指定引擎的表还是可能踩到。执行show table status where name xxx看一眼Engine字段两秒钟的事。第二种是DDL 隐式提交。MySQL 里执行CREATE TABLE、ALTER TABLE、TRUNCATE这类 DDL 语句会自动提交当前事务不管你在不在事务里。有些数据修复脚本里混着 DDL 和 DML跑完发现部分数据没回滚就是这个原因。第三种是连接池配置与事务超时不匹配。事务超时设了 30 秒但连接池的maxLifetime只有 20 秒连接可能在事务中途被回收表现是偶发的“连接已关闭”异常。反过来connectionTimeout设得太短并发高的时候获取连接直接失败业务方法根本没进去日志里看像“事务没生效”实际是连接没拿到。第四种是手动改了 autoCommit。有些老代码或者工具类会在方法里执行conn.setAutoCommit(true)把事务默默提交掉。这类问题排查起来最费劲建议在代码规范里直接禁止业务代码操作autoCommit。4.3 一张排查速查表我把这些年遇到的情况整理成一张表出问题时按顺序过一遍基本都能定位。现象最可能的原因快速验证方式抛异常了但数据还在受检异常未配rollbackFor或异常被 catch检查注解和 catch 块某个方法完全不起作用自调用、private/final 方法、对象不是 Bean打印getClass()看是否代理外层正常返回却报回滚异常内层事务被标记 rollback-only搜索UnexpectedRollbackException栈批量插入部分成功部分失败循环里用了REQUIRES_NEW或分了批提交看循环里的传播行为配置事务里查不到刚插入的数据查询走了另一个线程或另一个数据源检查线程与数据源路由偶发连接超时、请求卡住事务过长占连接或嵌套传播导致连接翻倍看连接池活跃数与慢 SQL特定表的事务不生效存储引擎是 MyISAMshow table status看 Engine主从延迟越来越严重大事务导致 binlog 堆积看主库事务大小与从库延迟曲线提示排查事务问题第一步永远是打开事务相关的 debug 日志把org.springframework.transaction的日志级别调成 DEBUG它会明确告诉你事务是在哪里开启的、是新建还是加入、提交还是回滚。比在代码里到处打断点快得多。5. 分布式事务先想清楚是不是真的需要5.1 落地之前必须先问的三个问题一上来就选型分布式事务方案是很多团队的通病。我通常会先问三个问题。第一个这两个操作能不能放进同一个库很多所谓的跨库需求其实是早期分库分表拍脑袋导致的业务上根本没必要拆。第二个能不能接受最终一致绝大多数业务发积分、发通知、更新统计都能容忍秒级延迟只要能保证最终对得上就没必要上强一致的方案。第三个失败之后能不能人工兜底或者自动重试如果补偿逻辑清晰、有对账任务那用最轻的方案就够了。这三个问题问完真正需要强一致分布式事务的场景其实少之又少。我参与过的项目里最后落到 2PC/XA 的只有一个资金类的核心链路剩下的全是用最终一致解决的。这个顺序很重要别让技术选型倒推业务设计。5.2 主流方案的对比与适用边界方案一致性性能业务侵入适用场景XA / 2PC强一致差同步阻塞、锁到二阶段低短事务、强一致要求、参与方少TCC最终一致较好高要写三个方法资金、库存等需要显式预留的资源Saga最终一致好中要写补偿长流程、跨多个服务本地消息表最终一致好中有可靠数据库的消息投递事务消息最终一致好低已有消息中间件且支持半消息选型的核心矛盾不在“哪个更先进”而在“你愿意为一致性付出多少可用性和复杂度”。XA 的问题不在于理论不好而在于它在两阶段之间锁着资源不放参与方一多整体可用性就是乘积下降。TCC 看起来美好但空回滚、悬挂、幂等这三个问题不处理干净线上一定会出事Try 没到就来了 Cancel 叫空回滚Cancel 比 Try 先到叫悬挂重复调用就是幂等没做好。这三个点必须在设计阶段就落到接口约定里事后补非常痛苦。5.3 本地消息表我这几年用得最多的落地方式如果要在“足够可靠”和“足够简单”之间选一个平衡点我最推荐本地消息表。核心思路一句话把要发的消息和业务数据写在同一个本地事务里再由一个独立的任务把消息捞出来投递。Transactional(rollbackFor Exception.class) public void createOrder(OrderDTO dto) { // 1. 业务数据落库 orderMapper.insert(dto.toEntity()); // 2. 消息和业务在同一个本地事务里落库 outboxMapper.insert(OutboxMessage.pending(ORDER_CREATED, dto.getOrderId())); // 事务提交两条记录要么都在要么都不在 }然后由一个定时任务扫描outbox表里status PENDING的记录投递成功后置为SENT失败则按退避策略重试。这里有几个实操细节值得强调扫描要用select ... for update skip locked或者按状态加索引 分片避免多个实例重复捞同一批投递必须做幂等消费方用业务唯一键做去重outbox表要定期归档否则数据量涨起来扫描会越来越慢。这套方案的缺点也很明显依赖数据库消息延迟取决于扫描间隔通常是秒级。但对绝大多数场景来说秒级的最终一致完全够用而且它的可观测性特别好——消息状态就摆在那张表里出了问题直接查表比翻中间件的日志快多了。6. 大事务、批量操作与性能调优6.1 大事务到底伤在哪怎么拆“大事务”没有严格的字数定义我的判断标准是执行时间超过 1 秒或者涉及行数超过几千行或者内部有远程调用三条命中任意一条就该拆。它的危害是多维的锁持有时间长其他事务排队连接被长时间占用并发能力被卡死undo log 持续增长回滚时可能比正向执行还慢binlog 一次性写入量过大从库回放出现明显延迟。拆的方向有四个。第一把远程调用移出事务边界先落库再调用或者先调用再落库用状态机记录中间态。第二批量化改成小批次提交一次 500 行而不是一次性 5 万行。第三把非核心的从属操作异步化主流程只做必须同步完成的部分。第四用状态机替代长事务把一个跨多步的业务拆成多个短事务每一步用状态字段标记进度失败后从断点继续。第四点最容易被忽略但它往往是治本的。比如“导入 10 万条数据”如果做成一个大事务中途失败全部回滚重来一遍还是从零开始。改成“先建任务记录然后分批处理每批独立事务 更新进度”失败后可以从上次的进度继续用户体验和系统压力都会好很多。6.2 批量写入的批次大小该怎么算批次大小不是拍脑袋定的它受几个硬约束限制。第一是max_allowed_packet单条 SQL 加上所有参数的总大小不能超过它默认值常见是 4MB 或 16MB。第二是单次事务的 redo log 写入量写得太多会导致一次刷盘时间过长出现明显的毛刺。第三是锁的范围批次太大容易和别的业务产生锁冲突。一个粗略的估算方法假设一行数据序列化后约 1KB批次 500 行就是 500KB 左右的 SQL 负载距离 4MB 有足够余量同时单次事务的执行时间通常在百毫秒级是个比较稳的起点。实际调优时我会用 200、500、1000 三个档位压测看吞吐和 P99 延迟的拐点通常拐点出现在 500 到 1000 之间。// 分批提交每 500 条一个独立事务避免一个巨大事务 public void batchImport(ListItem items) { int batchSize 500; for (int i 0; i items.size(); i batchSize) { ListItem batch items.subList(i, Math.min(i batchSize, items.size())); transactionTemplate.executeWithoutResult(status - itemMapper.batchInsert(batch)); } }顺带说一个容易被忘的点批量插入时如果表上有唯一索引冲突会导致整批失败。这时候要么用insert ignore注意它会把其他错误也一起忽略要么用on duplicate key update要么在应用层先查一遍。三种方式各有代价得看业务对“部分成功”的容忍度。6.3 事务与缓存、消息、异步的先后顺序这几个东西和事务凑在一起顺序错了就是数据不一致的源头。缓存这块公认的做法是先更新数据库事务提交后再删缓存而不是更新缓存。删缓存要挂到afterCommit上避免“缓存删了但事务回滚了”导致缓存被回填成旧值。更稳妥的还有延迟双删和订阅 binlog 变更前者简单但只能降低概率后者最可靠但需要额外组件。消息这块核心原则是不要在事务提交前投递。事务没提交就发消息消费者可能查不到数据出现“消息到了但数据还没有”的经典问题。要么用本地消息表要么用支持事务消息的中间件要么至少在afterCommit里发。异步任务同理Async方法如果在事务方法内部被调用它可能在外层事务提交之前就开始执行读到的还是旧数据。这类问题不会稳定复现往往是压测或者低峰期才偶尔冒出来排查成本极高。注意Async和Transactional写在同一个方法上异步生效但事务不生效——因为异步执行时已经换了线程事务拦截器的顺序也可能让注解失效。这两个注解要分开到两个方法上由外层方法调用内层的异步方法。7. 几个我踩过的坑和几条硬经验说几个印象深的。有一次线上出现偶发的“订单状态更新丢失”日志完全正常重试一次就好了。查了两天才发现更新语句用的是乐观锁版本号而事务的隔离级别是REPEATABLE_READ两个并发请求在同一个快照下读到相同的版本号其中一个更新失败后被上层 catch 掉当成“无需处理”忽略了。修法很简单把更新失败当成异常抛出并触发重试但根因是对隔离级别和乐观锁的交互理解不到位。还有一次是连接池被打满查下来是某个查询方法上标了NOT_SUPPORTED而它被一个循环调用了几百次每次都要挂起外层事务、取新连接、执行、归还。看起来每次都很短但架不住次数多而且挂起期间外层连接一直占着。后来把循环挪到事务外面问题立刻消失。几条我现在的固定习惯注解统一写rollbackFor Exception.class核心方法一定设timeout远程调用一律移出事务批量操作统一用模板分批提交REQUIRES_NEW用之前先数一下连接池够不够任何“事务后”的动作都挂afterCommit出问题先开org.springframework.transaction的 DEBUG 日志再动手查。最后分享一个定位事务问题的笨办法但很管用在事务方法的第一行和最后一行各打一条带线程名和时间的日志同时在数据库侧开general_log或者慢查询日志两边时间一对就能看出事务到底开了多久、期间执行了哪些 SQL、卡在哪一步。比对着代码空想快十倍不止。事务这东西原理就那么多剩下的全是场景经验踩过一次、想明白一次下次就能一眼认出来。

相关推荐

无线电能传输技术:SLSPC拓扑与高阶参数调谐优化
无线电能传输技术:SLSPC拓扑与高阶参数调谐优化

1. 项目背景与核心价值无线电能传输(WPT)技术正在彻底改变传统电力传输方式。作为该领域的前沿研究方向,高阶参数调谐(PT-WPT)系统通过精确控制谐振网络参数,实现了在复杂工况下的高效能量传输。这项技术特… · 2026/11/3 16:46:26

生成式召回实战:从负采样到物品编码的联合调优指南
生成式召回实战:从负采样到物品编码的联合调优指南

1. 生成式召回到底在解决什么问题推荐系统里的召回环节,过去很多年一直是多路召回打天下:协同过滤一路、热度一路、标签一路、向量一路,最后把各路结果合并送去粗排。这套架构能跑,但问题也很明显——每一路都要单独维护索引、单独… · 2026/11/3 16:45:24

AMD ROCm统一内存管理实战:多模型并发下的UM调度优化
AMD ROCm统一内存管理实战:多模型并发下的UM调度优化

1. 这不是内存扩容,是统一内存调度的硬核实战“128G 统一内存管 5 个模型”——看到这个标题,很多人第一反应是:“又一个吹牛的?显存内存加起来128G?那不就是堆料?”但真正动手做过 ROCm AMD Strix Halo 平… · 2026/11/3 16:45:24

2026年软件测试趋势:AI与云原生测试实践指南
2026年软件测试趋势:AI与云原生测试实践指南

1. 2026年软件测试行业趋势前瞻 2026年的软件测试领域正在经历一场由技术变革驱动的深刻转型。作为从业15年的测试架构师,我观察到几个关键趋势正在重塑行业格局:首先是AI赋能的测试自动化工具正在从简单的脚本录制回放,进化到具备自主生成测… · 2026/11/3 19:43:28

冷热电联供与微电网的能源优化技术解析
冷热电联供与微电网的能源优化技术解析

1. 冷热电联供系统与微电网的能源革命在能源结构转型的大背景下,冷热电联供系统(Combined Cooling, Heating and Power, CCHP)与微电网(Microgrid, MG)的协同运行正成为区域能源管理的前沿方向。这种系统通过将发电、供… · 2026/11/3 19:43:28

PVSYST安装与配置避坑指南:中文路径、许可证及数据库常见故障解决
PVSYST安装与配置避坑指南:中文路径、许可证及数据库常见故障解决

简介:本资源是一份面向光伏工程师、新能源专业学生及系统设计初学者的PVSYST软件入门教程PPT,聚焦并网型光伏系统从初步设计到工程落地的全流程实操指导。内容覆盖安装规范(强调路径禁用中文)、界面功能分区(初步设计/… · 2026/11/3 19:43:28

Gamebryo 2.5 零基础入门指南:从手册到第一个示例
Gamebryo 2.5 零基础入门指南:从手册到第一个示例

简介:GameBryo 帮助手册 PDF 版本面向游戏开发初学者与有经验的引擎使用者,针对网络上相关资料稀缺、多以 CHM 格式存在而不便移动阅读的问题,整理成便于随时查阅的文档。资源包共 1 个 PDF 文件,大小约 56.69MB,内容完… · 2026/11/3 19:43:28

车载操作系统架构设计:单系统七层与多系统选型指南
车载操作系统架构设计:单系统七层与多系统选型指南

简介:这份《车载操作系统架构研究报告》面向汽车电子工程师、车载系统研发人员及标准化工作者,系统梳理车载操作系统的架构现状、演进趋势与标准化需求,帮助读者理解从分布式电子架构向集中式、开放化演进过程中的技术要点。资源包共1个PDF文… · 2026/11/3 19:43:28

边缘计算在IoT测试中的架构设计与优化实践
边缘计算在IoT测试中的架构设计与优化实践

1. 边缘计算与IoT测试的现状与挑战物联网设备的爆炸式增长正在重塑测试领域的面貌。根据行业数据,2023年全球活跃IoT设备数量已突破160亿台,预计到2025年将超过270亿台。这种规模的增长使得传统集中式测试方法面临严峻挑战:测试数据回传造成的… · 2026/11/3 19:42:28

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46

STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码