手头正好攒了一批跟“事务(Transaction)”相关的实战记录,包括本地事务、Spring声明式事务、分布式事务一致性、事务日志排查这些内容。从几个真实的故障现场聊起,把事务背后的原理、常见坑和排查思路一次讲透,希望能给正在跟事务打交道的同学一些参考。
1. 事务到底是什么,为什么每次聊到它都绕不开一致性
先从一个最直观的问题说起:为什么系统一复杂,凡是牵扯到钱、库存、订单这类核心数据的地方,几乎都会被“事务”这个词支配?原因很简单,事务就是用来保证数据在并发、失败、重试这些乱七八糟的情况下,依然能保持一致性的唯一可靠手段。
1.1 从一个订单库存的故障现场说起
早前有一个订单系统,下单时要做三件事:写订单表、扣库存、给用户账户加积分。最初实现是在代码里按顺序执行这三段SQL,每一段都单独提交。结果某次线上促销,流量一上来,扣库存成功了,写订单表却因为字段溢出报错,用户这边收到了“下单失败”,但库存却实实在在少了一件。更麻烦的是,积分那边还照常加了,用户来投诉说积分不对,复盘的时候核对半天才发现是这三步没有放在同一个事务里。
这类问题在开发初期特别容易忽视,因为单机本地开发时,三步执行完基本不会出错,但线上并发一高,任何一步都可能异常。而事务的作用,就是把这若干步操作绑成一个不可分割的整体:要么全部成功,要么全部回滚到执行之前的状态。这就是ACID里原子性(Atomicity)最直白的解释。
1.2 隔离级别为什么是事务里最容易出幺蛾子的地方
除了原子性,事务还有一致性(Consistency)、隔离性(Isolation)、持久性(Durability),合起来就是常说的ACID。其中隔离性是最容易被误解、也最容易引发线上问题的一个维度。
SQL标准定义了四个隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 典型场景 |
|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | 几乎不用,风险太大 |
| 读已提交 | 避免 | 可能 | 可能 | Oracle默认级别 |
| 可重复读 | 避免 | 避免 | 可能(InnoDB可避免) | MySQL InnoDB默认级别 |
| 可串行化 | 避免 | 避免 | 避免 | 并发极低,性能牺牲大 |
实际开发中,90%的团队用的是数据库默认级别,也就是MySQL的“可重复读”。但要注意,隔离级别只是定义了“这个事务能看到什么数据”,它不决定“这个数据是不是最新”。遇到过不少同事,在可重复读级别下,A事务先查了一条订单金额,B事务随后把金额改了并提交,A事务再去查,发现金额还是老样子,于是代码里直接拿着旧金额去计算,最终数据对不上。这不是事务的Bug,而是没有理解隔离级别对数据可见性的约束。
1.3 事务的生命周期,回滚不是简单的undo
理解事务,还要知道它的生命周期:开始、执行、提交、回滚。很多框架把“开始”封装好了,开发者基本感知不到,反而是“回滚”这个动作,在实际线上环境中远比想象中复杂。
MySQL InnoDB的回滚依赖undo log,它记录的是“变更前的数据”。如果事务更新了一行数据,数据库会先把旧值写入undo log,再修改实际行数据。一旦事务回滚,就用undo log把数据还原。但有一个关键点常被忽略:undo log本身也是要持久化的,如果事务执行了很久、改了上万行,回滚也同样要花很久,期间数据可能表现为短暂的不一致状态。
所以,事务不是越大越好,很多人以为把读写操作全包在一个事务里就万无一失,结果遇到大批量数据操作时,事务过大不仅锁范围大,回滚代价也高,反而成了性能瓶颈和故障源。
2. Spring事务的注解为何经常“失效”,以及自调用问题的深层原理
Java后端开发,十有八九要用Spring管理事务。上手很简单,一个@Transactional就完事,但线上出了事务没回滚的问题,排查起来往往比想象中费劲得多。这里面的坑,多数出在“事务是如何被Spring生效的”这一层。
2.1 @Transactional的本质是AOP代理
Spring的声明式事务,本质上是AOP。它在Bean初始化时生成一个代理对象。当外部调用某个带@Transactional的方法时,实际调用的是代理对象,代理会先开启事务,再执行目标方法,最后根据是否有异常决定提交还是回滚。
这就是为什么“同类内部方法调用”会让事务失效。比如你写了一个UserService,
@Service public class UserService { @Transactional public void createUser() { insertUser(); insertLog(); } public void insertLog() { // 这里也有insert操作 } }表面上createUser方法有事务,如果insertLog失败,理论上应该回滚。但如果在UserService内部有一个方法调用了createUser,比如:
public void register() { this.createUser(); }此时register方法通过this直接调用createUser,绕过了Spring的代理对象,事务根本不会开启。这就是网上常说的“自调用事务失效”。解决办法也很简单,把内部调用拆到另一个Bean里,或者通过ApplicationContext拿到代理对象再调用,最省事的方式是自己注入自己:
@Autowired private UserService self; public void register() { self.createUser(); }2.2 异常被吞掉和异常类型是两座大山
事务不回滚的头号原因,不是自调用,而是异常被try-catch吃掉了。下面的写法在代码评审里出现的频率极高:
@Transactional public void doBiz() { try { insertOrder(); deductStock(); } catch (Exception e) { log.error("error", e); } }事务拿到的结果是“方法正常返回”,它并不知道内部有异常,当然不可能回滚。另一种情况是异常类型不对:Spring默认只对RuntimeException和Error回滚,检查异常(Exception的子类,比如IOException)默认不回滚。初次接触Spring事务的人最容易在这里翻车,以为任何异常都会触发回滚。
个人建议,凡是核心写操作,事务方法内不要自己捕获异常,要么让异常抛出去,要么在catch里手动标记回滚:
@Transactional public void doBiz() { try { insertOrder(); deductStock(); } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); log.error("error", e); } }2.3 事务注解贴在接口还是实现类,以及私有方法问题
再补充两个细节。第一,@Transactional可以放在接口上,也可以放在实现类上,但Spring官方推荐放在实现类的方法上,因为JDK动态代理基于接口,如果用了CGLIB代理,接口上的注解在某些边界情况下不会被解析。第二,@Transactional标记在private方法上,事务不会生效,因为代理类无法增强私有方法。这个在编译期不会报错,属于“看起来正常,实际上无效”的典型案例。
另外,还有一个常被忽略的点:同一个类里的两个@Transactional方法互相调用,比如methodA调用methodB,如果methodB挂了,methodA内部又没有异常抛出(比如methodB自己catch了),整个事务同样不会回滚,因为这两个方法处于同一个事务切面中,代理只拦截了最外层调用。
2.4 传播行为,REQUIRED和REQUIRES_NEW到底怎么选
Spring事务的传播行为,是一套比较容易上头的内容。最常用的两个是REQUIRED和REQUIRES_NEW。
REQUIRED是默认值,意思是如果当前已经存在事务,就直接加入;如果没有,就新建一个。这适合绝大多数场景,比如服务A调用服务B,B的方法加了REQUIRED,A的事务如果已经开启,B就直接挂在A的事务里,共用同一个事务。好处是数据一致性强,坏处是一旦B出异常,A也跟着回滚。
REQUIRES_NEW则是挂起当前事务,新建一个独立事务。典型使用场景是:订单主流程失败要回滚,但操作日志必须记录下来,哪怕主流程失败,日志也不能丢。它的代价是数据库连接会被占用更多,两个事务之间的数据隔离也要考虑清楚,别以为用了REQUIRES_NEW就万事大吉。
传播行为选型时,我的经验是:默认无脑用REQUIRED,只有明确要求“局部失败不影响整体”时才用REQUIRES_NEW。REQUIRES_NEW用多了,事务边界混乱,排查问题时会非常痛苦。
3. 分布式事务,订单与库存跨库时的一致性怎么保
单机事务再复杂,也只在同一个数据库实例内有效。一旦拆分到微服务,订单在订单库,库存在库存库,积分在积分库,本地事务就彻底无能为力了,这就是分布式事务的诞生背景。
3.1 分布式事务的核心矛盾
分布式事务要解决的问题,和本地事务本质相同,都是原子性和一致性,但难在参与方分散在不同进程、不同数据库中。没有办法用单一的数据库锁机制协调多方。更现实的问题是:网络会超时、服务会宕机、消息可能丢失,所有这些不确定性叠加起来,一致性变得极为困难。
面对这个矛盾,业界有一个基本共识:没有万能的分布式事务方案,只有根据业务场景选择合适强度的一致性模型。
3.2 强一致路线:两阶段提交(2PC)与Seata AT模式
两阶段提交是最经典的强一致方案,分准备阶段和提交阶段。协调者先让所有参与者执行准备操作并锁定资源,全部准备成功后,再统一发出提交指令。听起来很美好,但它的痛点也很明显:准备阶段锁住资源的时间长,性能差;协调者如果挂了,整个事务可能卡死;参与者如果在提交阶段失败了,数据一致性依然会被破坏。
在实际项目中,直接裸写2PC很少见,更多是借助Seata这样的开源方案。Seata AT模式可以理解成对2PC的改良:第一阶段直接执行业务SQL并生成undo log,第二阶段根据全局状态决定提交还是回滚,回滚时通过undo log反向补偿。
用Seata时有一个容易踩的坑:AT模式需要额外的全局锁,如果多个事务并发操作同一行数据,锁冲突会导致大量重试等待。高并发下,Seata的吞吐量下降明显,不适合写入极度密集的场景。我见过有团队把实时交易核心放在Seata上,压测时发现全局锁等待比业务执行还慢,最后不得不改造成异步消息方案。
3.3 最终一致路线:事务消息、本地消息表与可靠消息
大多数互联网业务,其实并不需要强一致。下单后库存扣减晚几百毫秒,用户无感知;但下单必须成功,积分可以稍后到账。这类场景,最终一致性是更务实的选择。
事务消息是目前比较流行的方式。它的核心思路是:先发一条“半消息”到消息中间件,消息对消费者不可见;然后执行本地事务;本地事务成功后,提交确认消息,消费者才能看到并处理;如果本地事务失败,则回滚消息。
RocketMQ的事务消息实现的就是这一套。要注意的是,事务消息本身需要保证消息与业务操作的原子性,中间件通常提供反查机制,也就是本地事务提交后但消息确认丢失时,Broker会反向调用生产者接口去查事务状态。生产者的反查接口必须实现得可靠,否则消息就会一直处于半消息状态,下游感知不到变化。
本地消息表方案更朴素:业务操作和写消息表放在同一个本地事务里,由额外的异步任务轮询消息表,把消息发到MQ或直接调用下游接口,成功后标记消息为已发送。它的优点是不依赖MQ的事务消息特性,缺点是消息表业务侵入性强、轮询可能存在延迟。
3.4 分布式事务排查的心得
在分布式事务场景里,单纯看应用日志往往无法定位数据不一致的根因,因为涉及多个服务、多个数据库,必须把链路串起来。查问题时我一般这样做:
- 先确认事务上下文ID是否贯穿整个调用链,没有TraceID先补上。
- 再核对每个参与方的执行顺序和最终状态,比如订单状态、日志状态、消息消费状态。
- 最后用对账任务扫描数据,找出不一致的脏数据,再反查是哪个环节漏了补偿。
有一点必须提醒:任何分布式事务方案都必须有兜底的对账和补偿机制。哪怕用了Seata或者事务消息,也不能保证100%不出现极端异常,对账任务就是最后一道防线。不少团队忽略了对账,出了事只能人工改库,风险极大。
4. 事务日志,数据库事务日志与SQL Server报错实战
事务日志这个话题,除了概念层面的理解,它更多是运维排查时绕不开的硬骨头。这里结合SQL Server的一次真实故障来说,印象很深。
4.1 那次数据库事务日志已满的报错
某天线上突然报警,业务接口大面积超时,数据库错误日志抛出了这样一条消息:
消息 9002,级别 17,状态 2,第 1 行
数据库 'ais20221123194008' 的事务日志已满。
这个报错的意思是:数据库的事务日志文件(.ldf)增长到了配置上限,或者磁盘空间已经被日志文件占满,事务无法继续写入日志,数据库被迫拒绝新的写操作。报错里的“级别 17”属于数据库资源不足类错误,这类错误通常不会导致数据库实例崩溃,但会让业务写操作无法执行。
我当时的处理步骤:
- 先用SQL确认日志文件大小和增长限制:
SELECT name, size, growth, max_size, is_percent_growth FROM sys.database_files WHERE type = 1;- 确认确属日志空间不足后,检查日志的虚拟日志文件(VLF)分布和日志空间的复用情况:
DBCC SQLPERF(LOGSPACE);- 再查看当前事务、最早的活跃事务,确认是不是有长事务阻碍了日志截断:
DBCC OPENTRAN;查下来发现,有同事在凌晨跑的批处理脚本一直没有提交事务,导致日志持续累积无法截断。处理方式是先跟业务方确认该事务是否可以回滚,确认后执行回滚并补充作业的异常提交逻辑,随后手动收缩日志:
DBCC SHRINKFILE(logical_log_file_name, target_size);4.2 事务日志满的常见诱因
从这次故障以及后来排查过的多次类似问题来看,事务日志满的诱因基本集中在四点:
| 诱因 | 说明 | 预防方案 |
|---|---|---|
| 长事务未提交 | 事务长时间不提交,日志无法截断 | 严格审查批处理作业,增加超时机制 |
| 日志增长限制 | 日志文件设了max_size,触顶后报9002 | 合理设置自动增长比例与最大限制 |
| 磁盘空间不足 | .ldf文件撑满磁盘,无剩余空间 | 监控磁盘使用率,提前扩容或归档 |
| 索引维护/大批量导入 | 大操作产生海量日志,瞬时写爆 | 分批次提交、区分简单恢复模式/大容量日志恢复模式 |
4.3 SQL Server日志查看与恢复模式的底层逻辑
查看事务日志内容,我常用DBCC LOG:
DBCC LOG(database_name, 2);它会输出大量日志记录,包含操作类型、事务ID、时间戳等,对定位特定事务很有帮助。但这个命令读的是原始日志结构,可读性一般,日常排查更多是确认日志增长区间内是否有异常大批量操作。
比日志内容更值得关注的是恢复模式。SQL Server的恢复模式有三种,事务日志的行为差异很大:
- 完整恢复模式:日志记录所有操作,支持任意时间点恢复,代价是日志可能快速膨胀。
- 简单恢复模式:仅保留最近的日志,用于崩溃恢复时的必要信息,检查点之后会截断日志,空间回收快,但无法做时间点恢复。
- 大容量日志恢复模式:适合大批量导入,日志量比完整模式小,但也不能做时间点恢复。
多数生产库用完整恢复模式,于是定期备份事务日志就成了一项必须执行的运维任务。如果从不备份日志,日志文件会只增不减,最终就是9002报错。这里再提一个很容易踩的坑:有的人以为把数据库切成简单恢复模式就能解决日志膨胀,切完之后空间确实回收了,但后续所有时间点恢复能力全部丢失,出大事时根本找不到回退点。类似的误操作,我在不同环境碰到过至少三次,每一次都是灾难性的。
4.4 日志排查里几个容易翻车的小细节
排查日志问题时,有几个细节容易被忽略:
第一,SHRINKFILE不能随便执行。在业务高峰或事务活跃期收缩日志,可能造成性能抖动,甚至阻塞。一般建议在低峰期操作,并先确认日志空间确实不再需要。
第二,DBCC OPENTRAN显示“No active open transactions”,并不意味着日志就可以随意收缩,还要检查日志复制的相关配置,比如基于日志的Change Data Capture(CDC)或者复制分发任务,它们也会阻止日志截断。
第三,ALTER DATABASE修改恢复模式时,会触发日志截断,如果库很大,这个操作本身可能耗时较长。生产环境变更前一定先在测试环境验证,并预留窗口。
5. 从JDBC到事务消息,一组容易被忽略的细节
聊到这里,事务的宏观框架基本齐了,但还有几个被高频搜索、实践中又常出问题的点需要单独拿出来说说。
5.1 JDBC事务:最底层的begin、commit与rollback
很多框架把事务封装得太好,以至于不少人已经忘了JDBC原生的样子。其实JDBC事务很简单:用Connection来管理,关闭自动提交后手动控制提交和回滚。
Connection conn = dataSource.getConnection(); try { conn.setAutoCommit(false); // 执行若干SQL conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.close(); }这里容易被忽略的是setAutoCommit(false)的时机。如果先执行了查询才发现忘了关闭自动提交,那前期的子操作已经被自动提交了,再回滚也回不到最初状态。另一个细节是:同一个Connection不能在多个线程间共享,否则事务边界会乱成一锅粥,比如两个请求交替提交,互相影响。
5.2 MySQL行锁与事务并发,间隙锁造成的“幻觉”
MySQL InnoDB在可重复读隔离级别下,为了解决幻读,引入了间隙锁(Gap Lock)和临键锁(Next-Key Lock)。这意味着,事务里的一次范围查询不只是锁定查到的行,还会锁定查询条件范围内的“间隙”,防止其他事务在这个间隙里插入数据。
很多并发问题就挂在间隙锁上。比如订单表里有一个根据状态统计的查询,一个事务在可重复读级别下执行了范围查询,另一个事务想往这个范围内插入一条新记录,结果被阻塞,产生死锁或超时。表面上看,两个操作互不相关,但实际是在锁层面冲突了。
遇到这种问题,首先考虑降低隔离级别到“读已提交”,因为很多场景根本不需要可重复读;其次,尽量让索引精确命中,减少间隙锁的范围;最后,控制事务执行时间,锁持有越短越好。
5.3 事务消息与普通消息的生产顺序,一个经典的对账陷阱
使用事务消息时,有一个经典的“先写库后发消息”和“先发消息后写库”的顺序之争。正确做法是:先执行本地事务,事务成功后再提交消息确认。RocketMQ事务消息的机制保证了这一点,但前提是半消息要发得足够早,让Broker能感知到,否则本地事务完成后再半消息,就失去了事务消息的意义。
但有些人会在“事务内直接发普通MQ消息”,以为本地事务提交后消息自然就发送了,实际上Spring事务提交和MQ发送之间没有原子性保障。比如订单事务提交后,MQ发送失败,下游永远不知道有新订单。解决方式要么用事务消息,要么采用“本地消息表 + 定时补偿”的老套路。后者虽然不够优雅,但在很多低成本项目里反而是最稳的。
6. 事务与并发的取舍,以及如何正确设置事务级别
事务级别和隔离级别,是两个相关但不同的概念,容易被混在一起。事务级别更偏向“这个事务的隔离强度和保护范围”,而隔离级别只是事务级别的核心维度之一。
6.1 事务级别的选择逻辑
实践中,事务级别影响的是:锁粒度、隔离级别、超时时间、是否只读。核心原则是,在保证数据正确的前提下,让事务尽可能短、锁尽可能少。
经验值大致可以参考:
- 单一写操作,读已提交足够。
- 金融转账、账务调整,可重复读甚至可串行化。
- 报表统计类查询,可以开启只读事务,降低锁开销。
- 大批量定时任务,应分批提交,避免单个事务过大。
6.2 如何避免死锁,这是每一个写事务的人都该懂的
死锁的成因很简单:两个或以上事务,各自持有对方需要的资源,互相等待。排查时一般看数据库死锁日志或系统表的锁等待信息,更直接的做法是把死锁图打开。
规避死锁的操作建议:
- 多个事务以相同顺序访问同一组资源。比如更新订单后更新库存,所有代码路径都按“订单 → 库存”的顺序,不要有的先更新库存。
- 缩短事务时间,减少持有锁的时间窗口。
- 使用合理的索引,避免全表扫描导致大量行锁。
- 不要在一个事务里做外部RPC调用,网络延迟会把锁持住很长时间。
- 设置合理的锁等待超时,宁可让一次快速失败,也不要长时间阻塞。
第4条尤其值得强调。我见过不少线上死锁案例,排查了一圈发现是事务方法里调了外部HTTP接口,接口响应慢,锁一直不释放,其他事务全部堆起来了。把RPC调用挪到事务外面,问题立刻消失。
6.3 事务与缓存的双写一致性,也是一个烫手山芋
严格说,事务本身不解决缓存一致性问题,但因为很多人用事务时也顺手更新Redis,这里必须提一下。最典型的错误是有事务保证的情况下,先更新数据库,再更新Redis,结果Redis更新失败,缓存里还是旧值,读到旧数据的接口持续返回错误结果。
常见方案是先更新数据库,再删缓存,这样即使删失败,最多出现一次缓存穿透,不会出现长时间脏读。更稳妥的是引入延迟双删:事务提交后删一次Redis,过几百毫秒再删一次。这个方案在并发偏高场景里实测有效,代价是实现复杂了一点点。比这更简单的是每次读Redis时校验数据版本,或者干脆让缓存短TTL自动过期,很多场景下也够用。
7. 事务调优与问题排查的个人工具箱
事情聊到最后,一个现实的问题摆在所有人面前:事务的坑这么多,线上出了问题从哪下手?这里整理了一套自己平时反复用的排查路径和工具清单,也算是对前面所有内容的一个串线。
7.1 排查事务问题的四个步骤
第一步,把日志链路拉通。事务相关的问题往往不是单一服务的事,要能顺着订单号、用户ID或自定义的TraceID把整个调用链串起来。没有链路信息,排查分布式事务问题基本靠猜。
第二步,看数据库实时的锁和事务状态。MySQL可以查:
SELECT * FROM information_schema.INNODB_TRX; SELECT * FROM performance_schema.data_locks;SQL Server就查sys.dm_tran_locks和sys.dm_exec_sessions等动态管理视图。这一步主要确定有没有长事务、锁阻塞、事务久未提交的异常。
第三步,打开死锁日志和慢查询日志。死锁频繁出现时,先把死锁信息捞出来,分析事务的加锁顺序。慢查询日志用来补充确认是不是有个别SQL在事务内拖慢整体提交。
第四步,看代码里的事务边界。事务方法内是否做了耗时操作、是否有RPC调用、异常是否被吞、自调用是否绕过了代理。这一层的问题在日志里通常不明显,只能靠代码走查。
7.2 一些拿得出手的排查脚本
信息量比较全的即时排除脚本,MySQL端我常用这一套:
-- 查看正在运行的事务 SELECT trx_id, trx_state, trx_started, trx_rows_locked, trx_rows_modified, trx_mysql_thread_id FROM information_schema.INNODB_TRX; -- 查看当前会话 SHOW PROCESSLIST; -- 查看锁等待关系(MySQL 8.0用performance_schema) SELECT * FROM sys.innodb_lock_waits;SQL Server端则依赖DMV:
SELECT request_session_id, resource_type, resource_description, request_mode, request_status FROM sys.dm_tran_locks WHERE request_status = 'wait'; SELECT session_id, status, login_name, host_name, last_request_end_time FROM sys.dm_exec_sessions WHERE session_id IN (SELECT DISTINCT request_session_id FROM sys.dm_tran_locks);脚本本身不难写,关键是要能读懂输出。比如看到某个事务持续几十分钟没提交,基本可以认定它是日志膨胀或锁阻塞的源头,接下来就是要联系业务方确认能否终止它。
7.3 事务调优时最容易被低估的参数
很多人调优事务只知道改隔离级别,却忽略了事务超时和连接池大小的联动关系。比如数据库连接池maxActive设置得特别小,而每个连接上又挂着长事务,其他请求拿不到连接只能排队,表现是系统响应变慢但数据库CPU又不高。
MySQL的innodb_lock_wait_timeout和Spring的@Transactional(timeout)要配合着看。Spring事务超时是客户端层面的,数据库锁等待超时是服务端层面的,两者取最短的那个作为真实超时时间。还有一点:事务内如果有多个数据源操作,比如多数据源项目里,一个事务挂两个连接,连接池很容易被提前打满,这类项目建议能不跨库就不跨库,或者明确拆分事务边界。
8. 事务的未来和实际项目里的选型心得
事务方案没有银弹,这句话听了很多遍,但每次遇到新的业务模块,还是有人会踩一遍老坑。这里想分享几个踩过坑之后的个人体会,不算结论,更多是参考。
一是不要一上来就上分布式事务框架。很多业务虽然拆了微服务,但数据一致性完全可以通过业务流程设计来规避,比如把强一致的操作收拢到同一个服务里,用本地事务搞定;跨服务的弱一致场景用事务消息 + 对账任务解决。引入Seata这类框架,意味着全局锁、性能损耗、运维成本都会接踵而来,必须评估值不值得。
二是事务边界要在设计阶段就画清楚,而不是写代码的时候随缘。一个事务里放多少个操作、涉及哪些表、是否需要调用外部服务,这些问题在方案评审时就要确定。线上见过不少事故,都是后来有人在旧事务里加了一段逻辑,结果把锁范围扩大了,引发连环阻塞。
三是日志和监控要提前配置。事务日志的大小、数据库磁盘空间、活跃事务数量、死锁次数,这些都是可以在问题爆发前提前预警的。很多团队等到9002报错才去查日志,实际上在日志文件涨到80%的时候,监控就应该报警了。
事务这个词,从JDBC的底层Connection,到Spring的AOP切面,再到分布式环境下的消息与补偿,覆盖的层次极广。每一次故障排查、每一次代码走查、每一次数据库优化,本质上都是在跟事务的原子性、隔离性、持久性这些老概念打交道。理解它背后的设计逻辑,比记住某个框架的具体用法重要得多。