搞本地数据读写的时候,事务的原子性听着好像是个简单概念,但真到线上出问题,你才会发现一堆细节其实没搞透。我最早接手一个订单系统,用户下单后偶尔出现订单表写了、库存扣减却失败的情况,后端日志里一行 transaction rolled back,但订单还是落库了。查了很久才发现是事务根本没有包住整个业务链路。那次之后我把事务机制完整研究了一遍,换过编程式事务、折腾过传播行为,也踩过锁等待超时,很多知识点都是靠生产故障逼出来的。
这篇内容我打算用一套真实可复现的本地业务场景,把事务从原理、配置、代码实验,一直讲到并发锁、超时排查和分布式事务边界。无论你是后端开发还是刚接触数据库的初学者,只要你需要确保一批写操作要么全部成功、要么全部不生效,这篇就能给你一套能直接参考的完整思路。
1. 先搞明白:事务的原子性到底在解决什么问题
很多人一说事务就背四大特性 ACID,原子性排第一位,代码里加个 @Transactional 就以为万事大吉。但问题恰恰出在“以为”这两个字上,理解不透彻,后面连排查方向都会走偏。
1.1 一个让数据出现“半成品”的经典场景
拿我项目里最典型的转账场景举例。用户 A 给用户 B 转 100 元,数据库里要做两步操作:
- 从 A 账户扣减 100 元
- 给 B 账户增加 100 元
只要第一步成功、第二步失败,A 的钱少了,B 的钱没多,账就平不了。放在电商里也是一样,下单动作可能同时涉及订单表、库存表、账户流水表,哪怕只是本地单库,多个写操作之间也没有天然保证“同生共死”。
更隐蔽的是文件类本地写入。比如一个应用启动时写配置,先删旧文件再写新文件,中途进程崩溃,文件就处于半删除半写入状态,下次启动可能直接读到一个损坏配置。这就是没有原子性的典型症状:数据出现“中间态”并暴露给外部。
1.2 原子性的本质:要么全部提交,要么全部回滚
事务机制给出的原子性定义其实很朴素:一个事务内的所有操作,在数据库视角里是一个不可分割的执行单位。事务提交(commit)后,所有修改才对外可见;事务回滚(rollback)时,所有修改都被撤销,数据恢复到事务开始前的状态。
我常跟团队同事说,原子性不是“操作不能中断”,而是“中断了也不会留下半截结果”。数据库通过 undo log 或事务日志记录修改前的状态,一旦需要回滚,就把这些数据倒回去。从这个意义上讲,原子性是数据库用记录和日志换来的强保证。
在本地读写中,这个保证是比较便宜的。单库、单连接、确定的几张表,数据库自己能管理事务边界。而一旦数据分布到不同库、不同服务,就需要靠分布式事务补位,那也是后话了。
1.3 没有事务时,本地文件/数据库读写会发生什么
构建印象最好的方式是自己动手模拟。先看一个不用事务的伪代码:
java复制public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountDao.decreaseBalance(fromId, amount); // 第一步
accountDao.increaseBalance(toId, amount); // 第二步
}
第二步一旦抛异常,第一步的扣款已经被写进数据库,没有机制撤销。程序日志里会记录异常,但数据已经是脏的。此时必须靠人工补数据或者引入对账任务来纠偏。
如果换成事务包裹,情况完全不同:
java复制@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountDao.decreaseBalance(fromId, amount);
accountDao.increaseBalance(toId, amount);
}
第二步抛异常,事务拦截器捕获到异常后主动发出回滚指令,第一步的扣款被撤销,调用方看到的效果就是什么都没有发生过。这就是原子性最直接的价值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 本地事务机制的核心组成与关键配置
原子性不是凭空实现的,需要一套底层的机制配合。理解数据库连接、提交、回滚、事务注解、事务传播行为和隔离级别之间的关系,才能真正用好事务。
2.1 事务、连接与 Commit/Rollback 的关系
一个基础概念要先讲透:事务是绑定在数据库连接上的。
数据库连接有一个默认的自动提交开关,很多数据库驱动初始情况下默认 autoCommit = true,意味着每一条 SQL 执行完立即提交。要让多条 SQL 共享同一个事务,你需要先关闭连接的自动提交,接着执行所有业务 SQL,最后统一提交或回滚。
看一段最朴素的 JDBC 代码:
java复制Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false); // 1. 关闭自动提交
preparedStatement1.executeUpdate(); // 2. 业务操作
preparedStatement2.executeUpdate(); // 3. 业务操作
conn.commit(); // 4. 全部成功则提交
} catch (Exception e) {
conn.rollback(); // 5. 任何异常则回滚
throw e;
} finally {
conn.setAutoCommit(true); // 6. 恢复默认
conn.close();
}
这套流程就是所有事务框架的底层模板。你日常用的 `@Transactiona
