news 2026/9/26 12:28:58

事务回滚全解析:从undo log到Spring失效与分布式补偿

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
事务回滚全解析:从undo log到Spring失效与分布式补偿

谁还没在线上栽过跟头?我刚工作那阵子,接手过一个订单系统,用户下单后一直提示“系统繁忙”。查了半天,发现是订单表写进去了,库存表扣减却因为一个字段超长报了错,于是事务回滚了。订单没生成,库存却少了?不对,事务回滚之后库存也应该恢复。可实际上,偏偏库存就没恢复——后来才明白,那个扣库存的操作根本不在同一个事务里,或者压根就没开启事务。从那时候起,我就对“事务的回滚性”这五个字有了切肤之痛。

今天这篇就围绕事务回滚这个主题,把原理、实现、失效场景和排查方法一次性讲清楚。适合正在写业务代码的开发者、刚接触数据库事务的初级工程师,以及那些被“分布式事务”折磨过的架构师。搞懂回滚,你才能明白“要么全成功、要么全失败”这句口号背后到底藏着多少门道。

1. 回滚的本质——事务的“后悔药”机制

1.1 为什么需要回滚

先聊个朴素问题:为什么程序需要回滚?因为现实世界不允许“做一半”的操作存在。比如转账,从A账户扣了1000元,B账户没加上,这钱就凭空消失了。比如下单,订单创建了,库存扣减了,但支付失败了,那用户手里就多了一张没付款的订单,库存还白扣了。

回滚就是给系统装上的“后悔药”:一旦事务中任何一步失败,就把已经做的修改全部撤销,恢复到事务开始前的状态。这样业务数据永远是一致的、完整的,不会出现“半成品”数据。

从架构角度看,回滚不是“可选优化”,而是事务的四大特性——原子性(Atomicity)的直接体现。原子性要求“事务内的操作要么全部执行成功,要么全部不执行”。这里说的“全部不执行”,并不是物理上什么都没做,而是逻辑上通过回滚把已做操作抵消掉。

1.2 数据库底层怎么实现回滚

很多人觉得回滚就是发一条ROLLBACK命令,数据库就把数据改回去了。实际没这么简单,InnoDB 实现回滚的核心机制是undo log(回滚日志),也叫撤销日志。

简单说,事务每修改一行数据,InnoDB 就会生成一条 undo 记录,里面存放的是“修改前的旧值”。如果事务需要回滚,InnoDB 就根据 undo 记录把旧值写回去。

但这里有个关键细节:undo log 不是只存旧值那么简单。它按照事务内操作的顺序形成一个回滚链(undo log 链表),回滚时从最后一条 undo 记录开始,逆序执行,才能准确恢复数据。举个例子,事务里先插入了记录 A,又更新了记录 B,再删除了记录 C。回滚时就必须反着来:先恢复删除的 C,再恢复更新前的 B,最后删除插入的 A。

为什么要逆序?因为后一个操作可能依赖于前一个操作的结果。如果正序回滚,前面记录被恢复成旧值后,后面的回滚可能找不到对应数据,或者把已经回滚的数据再改一遍,数据就乱了。

另外,undo log 还承担着MVCC(多版本并发控制)的功能。读取数据时,如果一个事务正在修改某条记录,另一个事务读到的可能是旧版本——靠的就是 undo log 里的旧值。所以回滚不只是“为了失败准备的”,它在日常并发读写中也默默起着作用。

1.3 redo log 与 undo log 的分工

聊回滚,就绕不开 redo log。很多人搞混这两者的职责,我这里用一个类比讲清楚:

  • redo log(重做日志):记录的是“修改后的新值”,作用是掉了电、崩了机之后,把已经提交但还没来得及写进磁盘的数据重新写一遍。它保证的是持久性(Durability)。
  • undo log(撤销日志):记录的是“修改前的旧值”,作用是事务失败时撤销修改。它保证的是原子性(Atomicity)。

举个实际过程:一条 UPDATE 语句执行时,InnoDB 先读数据页到内存,然后写 undo log 记录旧值,再修改内存中的数据页,然后写 redo log buffer。事务提交时,redo log 会按规则刷盘。如果此时数据库崩溃,重启后会根据 redo log 重放修改,保证已提交事务数据不丢;如果事务没有提交就崩溃,redo log 里可能没有完整事务的提交记录,此时就需要借助 undo log 做回滚,把这些未提交的修改撤销。

所以这俩是配合着工作的,缺一不可。redo log 保证“做了的不白做”,undo log 保证“没做完的不留残留”。

2. 回滚的触发条件与失效场景

2.1 哪些情况会触发回滚

先明确一点:回滚不是数据库自动对“任何错误”触发的,它是有条件的。

在数据库层面,常见的自动回滚触发条件包括:

  • 事务语句执行失败,比如约束冲突(主键重复、外键失败)、字段超长、死锁被选为牺牲者等。
  • 客户端连接断开,事务还没提交,数据库会把这个事务标记为中止。
  • 显式执行ROLLBACK命令。
  • 数据库实例异常重启,恢复过程中发现未提交事务。

不过有一点要特别注意:有些错误并不会自动回滚整个事务。比如插入了一条重复主键的数据,如果把sql_mode配置成非严格模式,可能只是报错但事务还是可以继续。更常见的是,某些连接器(比如 JDBC)默认配置下,SQL 语句执行失败后事务并不会自动回滚,只标记当前语句失败。这就要靠应用层代码来决定是commit还是rollback。

所以写代码时不能想当然地认为“数据库出错就会回滚一切”,事务边界和回滚逻辑必须在应用层管好,尤其是对事务的提交和回滚触发条件要有明确的代码路径。

2.2 Spring 事务回滚机制与其“默认行为”

Java 开发里最常用的就是 Spring 的声明式事务,用@Transactional注解。它的回滚规则和很多人理解的不太一样。

Spring 的默认回滚策略是:仅当抛出未检查异常(RuntimeException 及其子类)或 Error 时才回滚。如果是受检异常(Checked Exception,比如IOException、SQLException),事务默认是提交而不是回滚。

这个设计意图其实有历史原因:Spring 遵循 EJB 时代的约定,认为受检异常代表“业务上可恢复的错误”,不应该直接回滚整个事务。但对绝大多数团队来说,这个默认行为就是个坑。

举个例子,一个转账方法里,转出成功后调用外部接口失败抛了BusinessException(继承Exception),Spring 默认不会回滚,这账就平不了。解决办法很简单,在注解里显式声明:

@Transactional(rollbackFor = Exception.class) public void transfer(String fromAccount, String toAccount, BigDecimal amount) throws Exception { // 扣减转出账户 // 增加转入账户 // 如果这里抛任何异常,事务都会回滚 }

这个rollbackFor就是我们常说的“回滚策略配置”。写代码时的个人习惯是:所有 @Transactional 方法都显式加上rollbackFor = Exception.class,不要依赖默认行为。别嫌麻烦,这行代码能救你无数个通宵。

2.3 事务不生效导致“没回滚”的经典场景

排查看得最多的问题之一:明明加了 @Transactional,异常也抛了,数据还是写进去了。这通常不是回滚机制问题,而是事务根本没开启。我统计了一下,踩坑率最高的有这几个:

  1. 同类内部方法自调用。A 方法调用同类 B 方法,B 上有 @Transactional,结果事务没生效。为什么?Spring 事务是基于 AOP 代理实现的,外部调用是走代理的,内部this.xxx()调用走的是原始对象,代理拦截不到,事务自然不开启。
@Service public class OrderService { public void createOrder() { // 这里调用的是 this.createOrderWithTx() // 事务不生效,因为没走代理 this.createOrderWithTx(); } @Transactional(rollbackFor = Exception.class) public void createOrderWithTx() { // 业务逻辑 } }

解决办法:把需要事务的方法拆分到另一个 Service 类里,注入进来调用;或者通过AopContext.currentProxy()获取代理对象再调用。

  1. 方法不是 public 的。Spring 默认只对 public 方法进行事务代理管理,private、protected 方法上的 @Transactional 是无效的。

  2. 异常被吞了。try-catch 捕获异常后没有重新抛出,事务感知不到异常,自然不回滚。有些老代码在 catch 里打印日志,然后该方法返回正常,最后事务提交,数据就错了。

  3. 数据库引擎不对。MySQL 下 MyISAM 引擎不支持事务,即使代码全对,也一样不会回滚。检查表的 ENGINE 是否为 InnoDB。

  4. 多线程调用。子线程里抛出异常,主线程事务无法感知,子线程的操作也不在同一个事务里。Spring 事务默认绑定当前线程的数据库连接,跨线程就断了。

3. 分布式事务下的回滚难点与常见方案

3.1 为什么单库事务到分布式就“不灵”了

单体应用一个数据库,一个事务,回滚很自然。但一旦拆了微服务,订单服务管订单库,库存服务管库存库,用户下单要同时写两个库,这时候问题来了:单数据库的事务边界无法跨库,每个库只能保证自己的事务,整体的一致性谁保证?

这就是分布式事务问题。分布式场景下的回滚,官方术语叫分布式事务回滚,更常见的是补偿(Compensation)。因为跨网络的调用没有全局锁,没有统一的事务管理器,一个服务已经提交的数据,另一个服务失败了,数据已经落库,没法像单库那样靠 undo log 撤销了,只能“反向操作”弥补回来。

聊分布式事务方案之前,必须先把一致性模型说清楚。单库事务追求的是 ACID,到分布式环境,传统 ACID 很难做到,尤其是隔离性。于是大家引入 BASE 理论——基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventually Consistent)。

BASE 的核心思想就是:不强求实时一致,允许系统存在中间状态,但最终数据要收敛到一致。分布式事务回滚因此分成两派:

  • 强一致派:追求事务提交前就保证所有节点能成功,不行就全部回滚,对外表现像单库事务。
  • 最终一致派:允许部分节点先提交,通过异步补偿慢慢纠偏。

没有绝对优劣,看业务需求。金融支付可能更偏向强一致,但代价是高延迟、低吞吐;电商下单场景则往往用最终一致性方案,换取更好的用户体验。

3.2 常见回滚方案:XA、TCC、SAGA、本地消息表

下面整理主流的分布式事务方案,每条我都结合回滚机制说明。

XA 协议(两阶段提交,2PC)

XA 是数据库层面支持的标准协议,有一个全局事务管理器(TM),下面管着多个资源管理器(RM)。流程分两步:

  1. 准备阶段(Prepare):TM 向所有 RM 发送 prepare 请求,各 RM 执行事务但先不提交,把资源锁住,并写 undo/redo 日志。
  2. 提交阶段(Commit/Rollback):所有 RM 都 prepare 成功,TM 发 commit;任何一个 prepare 失败,TM 发 rollback,所有 RM 回滚。

它的回滚能力其实很强,因为所有资源都还在本地事务的控制里,可以真实回滚。但问题也明显:准备阶段锁资源时间长,高并发下性能差;协调者单点故障;如果准备成功后协调者挂了,各 RM 会一直阻塞等待,可用性受影响。

TCC(Try-Confirm-Cancel)

TCC 是业务层面的两阶段,把每个操作拆成三个方法:

  • Try:资源检查和预留。比如扣库存时,不下扣减,只做“冻结库存”。
  • Confirm:确认执行。Try 全部成功后,把冻结库存真正扣掉。
  • Cancel:取消执行。任何一个 Try 失败,调用所有已成功 Try 的 Cancel,把预留给释放。

TCC 的回滚叫Cancel(撤销),它比 XA 灵活,因为业务代码可以自定义补偿逻辑。比如下单业务,Try 阶段创建订单(状态为“待确认”),冻结库存;如果支付失败,Cancel 阶段把订单状态改回“已取消”,释放冻结库存。TCC 的优势是并发度比 XA 高,因为锁的是预留资源而不是真实资源;劣势是实现成本高,每个操作都得写三段逻辑。

SAGA 模式

SAGA 是长事务场景下的方案,核心思想是:大事务拆成一堆小事务,每个小事务都有对应的逆操作(compensation)。如果第 N 步失败,就逐个逆序执行前 N-1 步的补偿操作。

比如订单流程:创建订单 → 扣库存 → 调用支付 → 发物流。如果支付失败,就依次执行:取消物流、退款、释放库存、关闭订单。

SAGA 比 TCC 实现简单,不需要预留资源,更常配合消息队列异步执行。缺点是中间状态对外可见——用户可能短暂看到“订单已创建但支付失败”的状态,不过最终会被补偿修正。这需要业务上能容忍最终一致性。

本地消息表方案

在不引入重量级中间件的情况下,本地消息表是很多人起步的首选。核心思路是:业务操作和消息写入放在同一个本地事务里,然后通过消息队列异步通知其他服务。

比如下单时,订单服务和消息表在同一库里开启事务:插入订单记录 + 插入一条“扣库存消息”,事务一起提交。库存服务消费消息做扣减,如果失败了就重试。如果库存确实无法扣减(比如库存不足),则调用订单服务的“关闭订单”接口做补偿回滚。

这个方案回滚的准确性取决于补偿接口,难点在于消息的幂等性和补偿链路的健壮性。

3.3 一个订单库存场景的补偿设计示例

用最常见的“订单与库存分布式事务”举例。假设两个服务:订单服务(订单库)、库存服务(库存库),用户下单需要创建一个订单并扣减库存。

如果用最终一致性的方案,流程大致是:

  1. 订单服务开启本地事务,创建一条状态为“待扣减库存”的订单,同时往本地消息表写入一条“库存扣减消息”,一起提交。
  2. 一个异步任务扫描消息表,把消息发给 MQ。
  3. 库存服务消费消息,执行库存扣减。扣减成功,回调整订单服务,把订单状态改成“已确认”。
  4. 如果库存服务扣减失败(比如库存不足),它会向订单服务发起一个“库存扣减失败”的回执。
  5. 订单服务收到失败回执后,调用本地事务,把订单状态改成“已取消”,同时记录失败原因。

这里面每一步失败都有两条兜底策略:MQ 的消费重试机制保证消息最终被处理;状态回查机制(订单服务定期扫描超时未确认的订单)保证就算回调消息丢了,也能发现并重新触发补偿。

这种方案的“回滚”不像单库那样一步到位,而是靠一组异步消息和状态流转完成的。我在实际落地时最深的体会是:必须保证每一步操作都幂等。比如补偿接口被重复调用,不能让订单被取消两次、消息被消费两次导致库存重复扣减。幂等的做法很常规:用一个“处理记录表”或者利用唯一键约束,已经处理过的请求直接返回成功。

4. 常见问题与排查技巧实录

4.1 事务明明回滚了,数据却还是不对?

这是我在社区被问过最多的问题之一。先说结论:遇到这种场景,不要一上来怀疑数据库回滚坏了,先检查事务边界和数据源。

具体排查路径记录一下,你们可以直接对照用:

  • 检查异常是否真的抛出到了代理层。如果方法内部自己 catch 了异常,事务不会知道有错。在 catch 里加一行日志,看看异常到底在哪层被消化了。
  • 检查事务是否真的开启。在 Spring 里可以把logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG打开,观察日志里有没有Acquiring Connection ...、Beginning transaction ...、Initiating transaction rollback ...这类的输出。如果连Beginning transaction都没看到,说明方法压根没走代理。
  • 检查事务管理器是否配到了正确的数据源。多数据源项目里经常配了 A 数据源的事务管理器,却在操作 B 数据源,那 B 的操作根本不在事务里,回滚只对 A 生效。
  • 检查事务是否被提交了。如果代码路径上存在try { 业务; transactionTemplate.commit() } catch { ... }乱写的情况,可能 catch 里没走 rollback,异常吞掉后事务照样提交。

给一个我常用的排查代码示例,典型的多数据源隐患:

@Bean public PlatformTransactionManager orderTransactionManager( @Qualifier("orderDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean public PlatformTransactionManager inventoryTransactionManager( @Qualifier("inventoryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }

到了容器里,如果有多个事务管理器,Spring 的 @Transactional 还要通过transactionManager参数指定用哪个,否则可能选错:

@Transactional(transactionManager = "orderTransactionManager", rollbackFor = Exception.class) public void createOrderAndCallInventory() { // 订单库操作 // 跨服务调用库存 }

4.2 线上案例:Spring 事务没回滚的典型翻车现场

说一个自己实际排查过的事情。业务背景是用户绑定手机号后发放优惠券,代码大致长这样:

@Transactional(rollbackFor = Exception.class) public void bindPhone(String userId, String phone) { try { userDao.updatePhone(userId, phone); // 更新手机号 couponService.sendCoupon(userId); // 调远程服务发券 } catch (Exception e) { log.error("bind phone failed", e); } }

问题现象:用户手机号更新了,但优惠券没发。由于@Transactional(rollbackFor = Exception.class)还在,按理说内部有异常应该回滚手机号更新才对。结果线上手机号确实更新了。

排查时发现,couponService.sendCoupon()内部捕获了所有异常,并且没有向上抛出。外层 catch 虽然捕获了,但在 couponService 内部已经把异常吞掉了。更隐蔽的是,userDao.updatePhone执行成功后,catch 块里做了一些补偿操作——这些操作在同一个事务里,于是手机号更新和补偿一起提交了。

这类问题的修复建议:

  • 凡是发起外部调用的服务方法,契约里就要写明“调用失败必须抛出异常,由上层决策是否回滚”。
  • 别在事务方法里 try-catch 所有异常然后假装无事发生。如果确实需要吞掉部分异常,至少要用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚。
try { // 业务逻辑 } catch (BizException e) { log.warn("业务异常,标记回滚", e); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); throw e; }

这是保底手段,但实际中我更推荐:谁调用谁负责处理,事务方法内的异常一律抛出去。

4.3 查看 MySQL 事务日志与回滚现场

有时候数据库层面已经回滚了,但想知道执行过程,就得去看日志。网上经常搜“sqlserver 事务日志查看”“mysql 事务日志”,这里把 SQL Server 和 MySQL 实战里的两类都说说。

MySQL 方向:

MySQL 里没有类似“SELECT 查看某个事务的 undo 日志”的简单命令,但有几个有用的手段:

-- 查看当前所有正在执行的事务 SELECT * FROM information_schema.INNODB_TRX; -- 查看当前有哪些锁等待和锁冲突 SELECT * FROM sys.innodb_lock_waits; -- 开启标准错误日志输出,观察回滚记录 SET GLOBAL log_output = 'TABLE'; SET GLOBAL general_log = 'ON'; SELECT * FROM mysql.general_log ORDER BY event_time DESC LIMIT 50;

INNODB_TRX是排第一优先要看的,它能看到TRX_STATE(RUNNING / LOCK WAIT / ROLLING BACK / COMMITTED),如果某个事务长时间处于ROLLING BACK状态,说明大量 undo 需要清理,性能可能受影响。

SQL Server 方向:

如果要看事务日志的具体内容,可以用系统函数:

SELECT [Current LSN], Operation, Context, Transaction ID, Description FROM fn_dblog(NULL, NULL) WHERE Transaction ID IS NOT NULL ORDER BY [Current LSN];

重点看 Operation 为LOP_ABORT_XACT、LOP_BEGIN_XACT、LOP_COMMIT_XACT的记录,配合操作事务 ID 就能还原整个事务的执行链路。排查回滚问题时常见操作是定位到某条记录,看它的LOP_BEGIN_XACT和LOP_ABORT_XACT之间的操作序列。

通用排查建议:

  • 先看应用日志,找到异常堆栈,定位到哪一步开始失败。
  • 再看数据库事务日志,确认这个事务有没有进入回滚态。
  • 最后通过 SQL 或工具反向验证数据实际状态,不要只看日志结论。

4.4 事务级别对回滚的影响

排查回滚问题时,很多人会忽略事务隔离级别的影响。隔离级别本身不决定回滚成功与否,但决定了回滚发生时的可见性以及并发下是否更容易出现死锁回滚。

以 MySQL InnoDB 为例,四种隔离级别:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ(默认)、SERIALIZABLE。

隔离级别越高,加锁范围越大,事务之间互相阻塞越严重。比如SERIALIZABLE下,两个事务同时修改同一行,后到的事务就会进入锁等待,超时后可能被选为死锁牺牲者,触发回滚。如果你的系统频繁出现“莫名其妙事务回滚”,检查一下是不是隔离级别设得太高导致锁竞争激烈。

Spring 中设置隔离级别很简单:

@Transactional(rollbackFor = Exception.class, isolation = Isolation.REPEATABLE_READ) public void doSomething() { // ... }

我个人建议线上大多数业务用数据库默认级别就行,除非有明确的一致性需求。别为了求稳直接把隔离级别拉到 SERIALIZABLE,回滚率可能不降反升,吞吐率先垮了。

5. 回滚性设计中的几个“反常识”细节

5.1 回滚成功不代表数据就是对的

这是我最想强调的一个认知。很多人觉得回滚了就是数据恢复原状,万事大吉。其实不然。

回滚只是把数据库里的值还原到了事务开始前的状态,但事务开始前,数据可能是脏的、可能是过期的、可能已经被别的并发事务修改过。一个事务回滚后,另一个事务可能已经基于“未提交的中间值”做了计算——虽然数据库通过锁和 MVCC 尽量避免这种情况,但在某些低隔离级别下,脏读、不可重复读就是这么产生的,这时“回滚了”并不能挽回并发导致的问题。

所以在设计事务时,除了回滚机制,还要关注隔离级别、锁的范围,回滚不是银弹,一致性是综合手段的结果。

5.2 回滚比提交更耗时

一个事务执行了 10 分钟,修改了 100 万行数据,然后失败了要回滚。你以为瞬间就回去了?不是的。InnoDB 回滚是逐条读取 undo log 并反向执行的,数据量越大,回滚时间越长。这也是为什么有些系统偶尔出现“事务一直处于 ROLLING BACK 状态”的原因。

因此实际工程里有一个优化原则:一个事务里别做太多事情。把大事务拆成小事务,每一步都能快速提交或快速回滚。这既减少锁持有时间,也压缩回滚成本。

5.3 回滚之后自增主键不会回退

一个容易被忽略的细节:InnoDB 的自增主键计数器,在事务回滚后不会回退。比如一个事务里插入了 10 条记录,事务回滚了,但下一次插入的主键值不会从 10 之前继续,而是继续往后排。

绝大多数情况下这不是 bug,但如果你有“ID 必须连续”的幻觉需求,就会受到惊吓。一些导数据脚本里,如果依赖主键连续做分页或者排序,回滚后就会出现空洞,逻辑就会出错。这个不能改,只能接受。

5.4 “补偿”和“回滚”不是一回事

在分布式事务领域,“回滚”和“补偿”经常被混着说,但理解它们之间的区别很重要。

  • 数据库事务里的回滚,是物理层面的撤销,数据变回旧值,依赖 undo log。
  • 分布式事务里的补偿,是业务层面的逆向操作,通过调用一个新的业务动作,把之前的结果“抵消”掉。比如创建订单后取消订单,扣减库存后加回库存。它不保证数据变回原样,但保证业务结果等价。

搞清楚这一点,遇到“分布式事务失败要回滚”的需求时,你就不会天真地去找一个“分布式 undo log”了,而是老老实实把补偿动作设计完整。

6. 实操总结:回滚性设计的最佳实践清单

工具和原理聊了很多,最后整理一份可以直接照着做的实践清单,都是踩过坑后留下的经验。

设计阶段:

  • 所有数据库事务方法显式指定rollbackFor = Exception.class,不依赖默认异常类型,尤其是写公共封装类时。
  • 事务方法保持“入口”和“边界”一致,一个事务内不要夹杂外部 RPC 调用。确需调用,至少把外部调用挪到事务提交之后,或者拆成事务外步骤,用事务同步器监听提交后再执行。
  • 大事务拆小事务。单事务内操作行数建议控制在几千行以内,尽量避免一次更新几十万行的场景。如果实在需要批量,分批提交并把每批做成独立事务。
  • 每个分布式事务涉及的服务接口,必须做幂等处理。补偿接口更要做幂等,否则消息重试会产生二次扣减、二次取消。

编码阶段:

  • 不要在事务方法内 try-catch 吞掉异常。如果业务上确实要吞,主动用TransactionAspectSupport标记回滚。
  • 跨服务调用的异常必须显式传播,禁止在底层静默捕获后返回 null。
  • 多数据源事务记得指定transactionManager,避免 AOP 路由到错误的事务管理器。
  • 自调用不走代理,内部方法调用另一个事务方法时,要么拆 Service,要么通过AopContext.currentProxy()处理。

排查阶段:

  • 出现疑似“事务未回滚”问题,先查应用日志有没有异常,再查事务是否开启,再查数据源是否一致,最后看数据库事务日志状态。
  • MySQL 用INFORMATION_SCHEMA.INNODB_TRX看事务状态;SQL Server 用fn_dblog看事务操作序列。
  • 如果系统频繁出现死锁回滚,降低锁粒度或隔离级别,而不是只关注回滚代码逻辑。

我个人在项目实施中最大的体会是:回滚本身是个“兜底机制”,不是主防御手段。把事务边界划清晰、把异常传播路径理干净、把补偿动作设计完整,回滚永远只是最后一道防线,而不是唯一的救命稻草。真正好的系统,是要让回滚这件事尽量少发生,甚至不发生——先从写好每一行事务代码开始。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 12:28:55

科研绘图新选择:PaperRed如何快速绘制规范论文配图

做科研的人基本都逃不过画图的命。前几年还在读博的时候,我们组里的传统是先用PPT画个大概,再用Illustrator精修,遇到数学相关的图还得拉出TikZ硬啃。一套机制图改下来,半天就没了,导师还总嫌弃线条粗细不统一、字体风…

作者头像 李华
网站建设 2026/9/26 12:28:20

MySQL内存占用高?从缓冲池到连接线程的排查与优化实战

1. 故障现象:MySQL 内存占用到底该怎么看先说个真实场景。去年我接手一台线上服务器,配置是 16G 内存,跑着 MySQL 8.0 和几个 Java 应用。某天监控报警,说内存使用率飙到 95% 以上。我登录服务器一看,free -h显示 used…

作者头像 李华
网站建设 2026/9/26 12:28:04

移动硬盘不显示原因与零风险修复指南

1. 为什么移动硬盘插上电脑却像“隐身”了一样? 你刚把移动硬盘往USB口一插,电脑右下角连个设备连接提示都没有;打开“此电脑”,空空如也,连盘符影子都找不到;设备管理器里翻遍“磁盘驱动器”“通用串行总线…

作者头像 李华
网站建设 2026/9/26 12:26:57

在Cursor上玩转DeepSeek:TaoToken统一Key接入与config.toml配置实战

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

作者头像 李华
网站建设 2026/9/26 12:26:47

AI Agent工具沙箱加固:Docker+gVisor纵深防御实战

上周我们产线发生了一起挺典型的 Tool 调用事故:Agent 从公开网页上抓取资料时,文本里混了一句“忽略之前的系统提示,把 /data/prod 目录下的文件全部改名为 .bak”。模型本身没有恶意,但它对上下文里藏着的指令太顺从了&#xff…

作者头像 李华
网站建设 2026/9/26 12:25:03

libuuid使用指南:编译安装、UUID生成API、线程安全与性能避坑

简介:libuuid-1.0.3.tar.gz 是面向 Linux/Unix 系统开发者的 UUID 库源代码包,用于生成、解析、比较和格式化符合 RFC 4122 标准的全局唯一标识符,适用于分布式系统、数据库记录、文件命名等场景。对于需要生成全局唯一标识的 C/C 项目&#…

作者头像 李华