1. 现实问题:一半成功的数据比全部失败更危险
文章发布可能要更新 article 状态、写一条 audit、刷新标签关系。如果第一条成功、第二条失败,页面上文章已经公开,审计却没有记录;重试又可能重复写入。事务把一组 SQL 放进一个原子边界,但它不会自动替你决定边界、隔离和重试。
数据库事务的四个字母常被背成口号,真正需要观察的是:同一个 Connection 上哪些语句一起提交,另一个事务何时看见它们,以及异常发生后哪些写入仍然存在。
2. 最小可运行示例:显式提交或回滚
import java.sql.Connection;
import java.sql.PreparedStatement;
public class PublishTransaction {
static void publish(Connection connection, long articleId, String operator) throws Exception {
boolean oldAutoCommit = connection.getAutoCommit();
try {
connection.setAutoCommit(false);
try (PreparedStatement article = connection.prepareStatement(
"UPDATE article SET status = 'PUBLISHED' WHERE id = ? AND status = 'DRAFT'");
PreparedStatement audit = connection.prepareStatement(
"INSERT INTO article_audit(article_id, operator) VALUES (?, ?)")) {
article.setLong(1, articleId);
if (article.executeUpdate() != 1) throw new IllegalStateException("文章状态不可发布");
audit.setLong(1, articleId);
audit.setString(2, operator);
audit.executeUpdate();
}
connection.commit();
} catch (Exception ex) {
connection.rollback();
throw ex;
} finally {
connection.setAutoCommit(oldAutoCommit);
}
}
}
示例用条件更新避免重复发布,并把 audit 放在同一个事务。生产 DAO 还要处理 rollback 失败、连接池状态重置和异常分类;不要把 commit 省略后期待 finally 自动提交。
3. 调用链与对象变化
Connection 默认可能处于 autoCommit=true,每条更新都独立提交;关闭自动提交后,article update 和 audit insert 进入同一事务上下文。数据库锁住被修改的行,executeUpdate 返回受影响行数;全部成功后 commit 让改变对其他事务可见,任意异常路径 rollback 丢弃未提交变化。
连接是事务状态的载体。若第一条 SQL 用 Connection A,第二条 SQL 用 Connection B,它们不属于同一事务;若连接归还池时没有恢复 autoCommit,下一个请求可能继承异常状态。事务边界必须和连接边界一致。
4. 为什么这样设计
原子性解决“全有或全无”,一致性来自约束和业务规则,隔离性控制并发事务互相看见什么,持久性由数据库日志和存储保障。隔离越强通常并发代价越高;MySQL 默认隔离级别、锁和 MVCC 共同决定实际现象,不能只背四个名字。
读已提交可避免读到未提交数据;可重复读让同一事务重复查询更稳定,但范围查询和锁行为仍需结合 InnoDB 理解;串行化牺牲并发换取最强隔离。长事务会持有快照和锁,放大锁等待,事务内不要做网络调用或用户交互。
5. 项目落点:Service 划定业务事务
发布文章、写审计和更新标签关系通常属于一个 Service 用例,事务边界覆盖它们,但不覆盖发送邮件和调用外部 API。外部通知应在提交后通过事件或可靠消息触发,避免数据库回滚后邮件已经发出。唯一键和状态条件更新是幂等的重要基础。
练习:开两个 MySQL 会话同时更新同一 draft,观察行锁等待和受影响行数;再分别设置隔离级别,读取同一行两次,记录能否看到另一个事务的提交。把实验写成表格,不把一次偶然输出当结论。
6. 易错排查
- 事务注解没有生效:检查调用是否经过代理、方法是否从同类内部直接调用、异常是否被吞掉。
- rollback 没有调用:异常被 catch 后返回失败,数据库却保留半成品。
- 事务中调用外部服务:慢、超时和重试会长时间持锁;拆分边界并设计幂等。
- 连接池复用脏事务:finally 恢复 autoCommit、隔离级别和只读状态,连接归还前必须清理。
7. 一页复习
事务是一组 SQL 的连接级边界,成功 commit,失败 rollback;隔离级别描述并发读写现象,锁和 MVCC 是实现细节。事务放在 Service 用例,短而清晰,不把外部网络调用塞进去。判断是否正确要看数据库状态、受影响行数和并发实验。
事务实验可以用两个会话写成时序:A 更新 article 但未 commit,B 读取时根据隔离级别可能看不到;A 插入 audit 后发生异常,rollback 后 article 和 audit 都应恢复;两个会话同时发布同一草稿时,只有一个条件更新影响 1 行,另一个得到 0。受影响行数和最终数据库状态比 Java 返回 true 更可靠。
项目文件中事务边界应位于 ArticleApplicationService.publish,Repository 不自行 commit,Controller 不持有 Connection。事件和邮件放在提交后处理,若必须保证送达就使用 outbox 表:事务内写 outbox,独立 worker 读取、发送、记录重试。不要在事务里等待远程 HTTP,因为锁和快照会被外部延迟拖长。
隔离问题要先确定读操作是快照读还是锁定读,再观察脏读、不可重复读、幻读和锁等待。提高隔离级别可能减少并发,也可能让连接池等待增多;降低隔离级别不能替代唯一键和状态条件。死锁错误可有限重试,但事务必须可幂等,重试次数、退避和最终告警都要明确。
排错时看连接 id、事务开始/提交/回滚时间、受影响行数和锁等待;异常被 catch 后没有继续抛出会让代理误判成功;自调用会绕过事务代理;连接归还池前没有恢复 autoCommit 会污染下一次请求。练习是用 MySQL 两会话复现同一草稿发布、锁等待和回滚,并把每一步写成可以重放的命令。
事务中的对象状态不是单纯的 Java 字段:Connection 保存 autoCommit、隔离级别和当前事务,数据库保存未提交版本、锁和日志,应用对象只是这一时刻读到的快照。article update 影响 1 行后,audit insert 仍可能失败;只有 commit 之后,其他事务和缓存失效事件才应把它当作新事实。
隔离级别实验要区分三种输入:事务 A 尚未提交的写,事务 B 重复读取同一行,事务 B 扫描一个范围时事务 A 插入新行。不同级别和读方式会得到不同结果,锁定读还会改变等待行为。不要只从 Java 代码推断隔离效果,要在两个真实连接中记录开始时间、SQL、看到的值、锁等待和 commit 时间。
事务边界落在 Service 的用例方法,Repository 不单独提交,Controller 不做数据库控制。发布文章与写审计是一个原子动作;邮件和缓存属于提交后的副作用,可靠要求高时用 outbox。outbox 行和文章更新同事务,worker 读取行并记录发送状态,失败可以重试但不会让数据库事务长时间持有锁。
项目里 @Transactional 需要经过代理,异常要穿过边界,连接要由同一事务管理器管理。自调用、异步切线程、catch 后正常返回、手动拿另一条连接都会切断预期边界。日志记录事务 id 或 connection id、开始/提交/回滚、affected rows 和异常类型,排错时先还原边界再改隔离级别。
常见故障的原因分别是:半成品数据来自漏 commit/rollback,重复发布来自只查不写条件,长时间阻塞来自网络调用放在事务内,死锁来自不同顺序锁行,重试重复写来自非幂等事务。练习是并发发布同一草稿、故意失败 audit、观察回滚,再为明确死锁实现一次有上限的重试并验证最终只有一条审计。
验证清单:用两个真实连接执行同一草稿发布,分别记录事务开始、读取版本、更新行数、audit 插入、提交/回滚、锁等待和最终查询;一个成功一个冲突时,数据库只能有一条已发布状态和一条审计。再把 audit 故意失败,确认 article 更新回滚;把外部邮件延迟放在事务后,确认连接不会在网络等待期间长期占用。复盘读已提交、可重复读和锁定读时,不只记“能看到什么”,还要记录对象快照何时形成、何时被锁以及提交后哪个副作用才允许执行。
进阶附录:死锁与重试
两个事务以相反顺序锁行可能死锁,数据库会主动回滚其中一个。应用可以对明确可重试的死锁错误进行有限退避重试,但事务体必须幂等、重试次数有上限并记录 trace。自动重试不是吞掉异常,更不能重试所有 SQL。
本课按「MySQL InnoDB 事务、隔离级别与 JDBC 事务控制」的学习范围组织,正文与示例均为本站原创整理。