CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
SQL 与关系型数据库/课时/事务基础
RawBlame
课时目录10 节

SQL 与关系型数据库

  1. 01关系型数据库与 SQL 是什么
  2. 02SELECT 基本查询
  3. 03WHERE 过滤与排序
  4. 04JOIN 多表联结
  5. 05聚合与 GROUP BY
  6. 06INSERT、UPDATE、DELETE
  7. 07表设计与主键外键
  8. 08索引与查询性能
  9. 09事务基础
  10. 10综合练习与本站数据模型

事务基础

ACID、BEGIN/COMMIT/ROLLBACK。

25 分钟

事务基础

这一课你将能做到

  1. 用 START TRANSACTION / COMMIT / ROLLBACK 手动管理事务,并用转账例子演示「要么全成、要么全不做」
  2. 用自己的话解释 ACID 四性和 MySQL 的四个隔离级别(默认 REPEATABLE READ)
  3. 把 SQL 事务映射到 Spring Boot 的 @Transactional,说出至少三个注解失效场景

前置:第一课建库脚本已跑(前面课改过余额的话先重跑复原,保证对得上输出)。

为什么要学

真实业务的一条逻辑往往要连续改好几张表:买课 = 扣余额 + 插订单 + 生成学习记录。三步做到一半服务崩了怎么办?余额扣了订单没生成,钱就凭空消失。事务就是数据库给的答案:把多条 SQL 捆成一个「全做或全不做」的单元。这也是 InnoDB 压倒 MyISAM 的核心优势,Java 后端面试的必考题(ACID、隔离级别、@Transactional 失效场景三连问)。

先建立图像

转账 = 两步 UPDATE,必须同生共死

START TRANSACTION;
  老陈 balance 128 → 78   (步骤①)
  小林 balance  39 → 89   (步骤②)
  ┌─ 一切正常 ──────────────→ COMMIT:两步对外同时生效
  └─ 中间任何环节出错 ──────→ ROLLBACK:两步一起当没发生过

COMMIT 之前,改动只有当前连接自己看得见;COMMIT 之后才对所有人生效、落盘持久。

跟着做

步骤 1:认识 autocommit

SELECT @@autocommit;   -- 1

MySQL 默认开启自动提交:每条语句自带一个隐式事务,执行完立即生效。前面课里的 INSERT/UPDATE 所以「一敲就生效」。显式事务就是把多条语句从「各自为政」改成「打包结算」。

步骤 2:ROLLBACK——全没发生过

START TRANSACTION;
UPDATE student SET balance = balance - 50 WHERE id = 1;   -- 老陈 128 → 78
UPDATE student SET balance = balance + 50 WHERE id = 2;   -- 小林  39 → 89

SELECT id, name, balance FROM student WHERE id IN (1, 2);
-- 当前事务里看到:78 / 89

ROLLBACK;

SELECT id, name, balance FROM student WHERE id IN (1, 2);
-- 回到 128 / 39,两步 UPDATE 一起作废

步骤 3:COMMIT——一起生效

START TRANSACTION;
UPDATE student SET balance = balance - 50 WHERE id = 1;
UPDATE student SET balance = balance + 50 WHERE id = 2;
COMMIT;   -- 结算,改动对外生效并持久化

SELECT id, name, balance FROM student WHERE id IN (1, 2);   -- 78 / 89

把老陈和小林改回初始值,方便后面课对照:

START TRANSACTION;
UPDATE student SET balance = balance + 50 WHERE id = 1;
UPDATE student SET balance = balance - 50 WHERE id = 2;
COMMIT;

步骤 4:应用层异常 → 自动回滚

先在 SQL 里看「扣钱前检查余额」的防御式写法(这条语句本身就该这么写):

-- 余额不够就什么都不发生:affected rows = 0,应用层据此中断流程
UPDATE student SET balance = balance - 500
WHERE id = 1 AND balance >= 500;
-- 老陈只有 128,影响 0 行

再到 Java 侧,@Transactional 会在抛异常时替你执行 ROLLBACK(完整示例见步骤 6)。

步骤 5:隔离级别初体验(开两个 mysql 客户端窗口)

先看默认级别:

SELECT @@transaction_isolation;   -- REPEATABLE READ(MySQL 默认)

实验:窗口 A 开事务并读一次余额;窗口 B 在事务外把老陈余额加 100 并提交;窗口 A 再读——

-- 窗口 A
START TRANSACTION;
SELECT balance FROM student WHERE id = 1;   -- 128
-- (切到窗口 B 执行 UPDATE + COMMIT,再回来)
SELECT balance FROM student WHERE id = 1;   -- 仍然是 128:可重复读
COMMIT;
SELECT balance FROM student WHERE id = 1;   -- 228:本事务结束后才能看到新值

这就是「可重复读」:同一事务内多次读同一行结果一致,不受别人已提交修改的影响。四个隔离级别与三类读问题:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED避免可能可能
REPEATABLE READ(MySQL 默认)避免避免基本避免(MVCC + 间隙锁)
SERIALIZABLE避免避免避免

名词解释:脏读=读到别人还没提交的数据;不可重复读=同一事务两次读同一行值不同;幻读=同一事务两次范围查询行数不同。

步骤 6:Spring Boot 对照(Java 后端视角)

@Service
public class CourseOrderService {

    private final JdbcTemplate jdbc;

    public CourseOrderService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    @Transactional   // 方法内所有 SQL 共用一个事务
    public void buyCourse(long studentId, long courseId) {
        BigDecimal price = jdbc.queryForObject(
                "SELECT price FROM course WHERE id = ?",
                BigDecimal.class, courseId);

        // 扣款:余额不足则影响 0 行
        int updated = jdbc.update(
                "UPDATE student SET balance = balance - ? WHERE id = ? AND balance >= ?",
                price, studentId, price);
        if (updated != 1) {
            throw new IllegalStateException("余额不足");   // 抛 RuntimeException → 整个事务回滚
        }

        jdbc.update(
                "INSERT INTO pay_order (student_id, course_id, amount, status) VALUES (?, ?, ?, 'PAID')",
                studentId, courseId, price);
        // 方法正常返回 → Spring 提交事务
    }
}

三件事对上号:@Transactional ≈ 自动 START TRANSACTION;正常返回 ≈ COMMIT;抛 RuntimeException ≈ ROLLBACK。SQL 里 AND balance >= ? 的兜底条件仍然保留——并发下两笔扣款同时通过「先查余额」的校验,只有带条件的 UPDATE 能挡住超扣。

讲清楚要点

  1. 事务 = 多条 SQL 的打包结算单元:COMMIT 前随时可整体作废,COMMIT 后永久生效。只有 InnoDB 支持事务,MyISAM 表上 BEGIN 是摆设。
  2. ACID 人话版:原子性(全做或全不做,靠 undo log 回滚)、一致性(前后都处于合法状态,是前三者的结果)、隔离性(并发事务互不干扰,靠锁 + MVCC)、持久性(COMMIT 后断电不丢,靠 redo log)。
  3. autocommit=1 时每条语句隐式提交;显式事务用 START TRANSACTION(或 BEGIN)开启,COMMIT/ROLLBACK 结束。应用连接池里事务边界在方法/请求级别,别把 START TRANSACTION 留在手里不管。
  4. 隔离级别四档,MySQL 默认 REPEATABLE READ,靠 MVCC 让读不加锁、写不阻塞读,并用间隙锁抑制幻读;多数业务默认档就够,改级别要给出明确理由。
  5. 事务要短:长事务持有 undo 和锁不放,拖垮并发、撑大回滚段。事务里绝不调 HTTP/RPC/发消息/等用户输入——先算好参数,事务内只做纯数据库操作。
  6. DDL 会隐式提交:事务中途执行 ALTER/TRUNCATE,之前未提交的修改全部定死,ROLLBACK 救不回来。事务内只放 DML。

你可能会懵的地方

  • ROLLBACK 能撤销已 COMMIT 的操作吗? 不能。COMMIT 是「结算完成」。所以第 6 课的安全流程是「BEGIN → 执行 → SELECT 复核 → 确认无误才 COMMIT」——复核必须发生在 COMMIT 之前。

  • 不写 BEGIN 就没有事务吗? 有,autocommit 模式下每条语句各自是一个隐式事务,只是粒度小到感觉不到。

  • @Transactional 什么时候会失效? 高频答案:①同类内部 this.method() 自调用,绕过了 Spring 代理;②方法不是 public;③异常被 try-catch 吞掉,事务以为一切正常;④抛的是受检异常(默认只对 RuntimeException 回滚,除非配 rollbackFor);⑤存储引擎不支持事务。

  • 隔离级别越高越好吗? 隔离越严并发越差。SERIALIZABLE 串行化了读写,吞吐大跌;MySQL 默认的 REPEATABLE READ 用 MVCC 在一致性和并发之间取得平衡,无特殊理由不动它。

  • 死锁是怎么回事? 事务 A 锁着行 1 等行 2,事务 B 锁着行 2 等行 1。InnoDB 检测到后会回滚代价小的一方,应用捕获异常重试即可。多表更新时按固定顺序访问资源,能大幅减少死锁。

错法对照

错法问题正解
事务里调外部 HTTP/MQ长事务,连接和锁被占满事务外做 IO,事务内只碰数据库
catch(Exception){} 吞异常Spring 认为成功,不回滚让异常抛出,或手动 setRollbackOnly()
同类里 this.buyCourse() 自调用代理绕过,注解失效拆到另一个 Bean,或注入自身代理
事务里混 DDL隐式提交,回滚失效DDL 独立在事务外执行
用 MyISAM 表跑事务压根没有事务和行锁统一 InnoDB
只靠「先查再改」防超扣并发窗口内双双通过校验UPDATE 带条件 + affected rows 判断

卡点急救

  1. 「我明明改了怎么没生效」:先确认自己的事务 COMMIT 了没有;再确认查的是同一个库的同一个连接;最后确认没有别的会话在事务里改同一行。
  2. 表被锁住、查询卡死:另开窗口 SHOW PROCESSLIST; 看等待中的语句,找到长事务连接 KILL;事务提交后锁即释放。
  3. 遇到死锁报错 1213:业务代码捕获后重试;SHOW ENGINE INNODB STATUS\G 能看到最近一次死锁的双方语句。
  4. ROLLBACK 了但数据还是变了:检查事务里是否混入了 DDL(隐式提交);或者你回滚的连接和改数据的连接不是同一个。

面试常问

  • ACID 分别指什么?靠什么实现? 原子性靠 undo log 回滚;持久性靠 redo log 先写日志后刷盘;隔离性靠锁 + MVCC;一致性是前三者共同保证的业务结果。

  • 四个隔离级别?MySQL 默认哪个?默认档怎么防幻读? 读未提交/读已提交/可重复读/串行化,MySQL 默认可重复读;快照读靠 MVCC 读旧版本,当前读靠临键锁(记录锁+间隙锁)挡住范围插入。

  • Spring @Transactional 的失效场景? 自调用、非 public 方法、异常被吞、受检异常默认不回滚、引擎不支持事务、多线程里另开连接。能展开「代理机制」是加分项。

  • 长事务有什么危害? 占连接、持锁引发阻塞和死锁、undo log 膨胀、MVCC 旧版本堆积拖慢全库读、主从延迟。治理:拆小事务、IO 移出事务、监控 information_schema.innodb_trx。

自测三问

  1. 转账的两个 UPDATE,如果第二步因为断连没执行,会发生什么?谁保证金额对得上?
  2. 窗口 A 未提交的事务里,为什么读不到窗口 B 已提交的修改?
  3. @Transactional 方法里 try-catch 吞了异常,事务会回滚吗?该怎么处理?

小结一句话

事务把多条 SQL 打包成「全做或全不做」:COMMIT 前可整体回滚,InnoDB 用 undo/redo/MVCC 兑现 ACID;应用侧用 @Transactional 划边界,事务短小、异常别吞、条件兜底。

延伸

  • MySQL 事务隔离级别
  • 最后一课:综合练习与本站数据模型——把 9 课本领全部用上,并对照 CodeRoadMap 真实库写查询。
加载本节练习…
← 上一课索引与查询性能下一课 →综合练习与本站数据模型