数据库事务原理与回滚机制

一、什么是数据库事务

事务是一组被当作单个逻辑工作单元执行的数据库操作序列------这组操作要么全部成功,要么全部失败回滚,不存在"执行了一半"的中间状态。

最经典的例子是银行转账:A 给 B 转 100 元,包含两步:① A 扣 100,② B 加 100。如果第 1 步成功后系统崩溃、第 2 步没执行,A 的钱就凭空消失了。事务的作用就是保证这两步要么都成功,要么都不发生。

sql 复制代码
BEGIN;                                              -- 开启事务
UPDATE account SET balance = balance - 100 WHERE id = 'A';
UPDATE account SET balance = balance + 100 WHERE id = 'B';
COMMIT;                                             -- 提交(全部生效)
-- 出错则 ROLLBACK(全部撤销)

二、ACID 四大特性

特性 名称 含义 底层机制
A 原子性 操作不可分割,全成功或全失败 undo log 回滚日志
C 一致性 事务前后满足业务规则/约束(最终目的) 其他三性 + 约束共同保证
I 隔离性 并发事务互不干扰 锁 + MVCC
D 持久性 提交后数据永久保存,断电不丢 redo log 重做日志

三、为什么需要事务

  1. 故障恢复:异常/断电/崩溃时,保证数据不停在错误的中间状态。
  2. 并发控制:多用户同时读写同一数据时避免相互干扰(否则会超卖、重复扣款、账目不平)。

四、常见使用方式

原生 SQL:

sql 复制代码
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 'A';
UPDATE account SET balance = balance + 100 WHERE id = 'B';
COMMIT;   -- 或 ROLLBACK

Spring 声明式事务(Java 最常用):

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) {
    accountMapper.decrease(from, amount);
    accountMapper.increase(to, amount);   // 抛异常时上面也会回滚
}

Node.js(Sequelize 托管事务):

javascript 复制代码
await sequelize.transaction(async (t) => {
  await Account.decrement('balance', { by: amount, where: { id: fromId }, transaction: t });
  await Account.increment('balance', { by: amount, where: { id: toId }, transaction: t });
  // 正常返回自动 commit,抛异常自动 rollback
});

(Python SQLAlchemy 用 with session.begin():,JDBC 用 conn.setAutoCommit(false) + commit/rollback,原理一致。)

五、隔离级别与并发问题

三类并发读问题:

  • 脏读:读到其他事务未提交的数据(对方回滚后就是脏数据)
  • 不可重复读:同一事务两次读同一行,值不同(针对 UPDATE)
  • 幻读:同一事务两次范围查询,行数不同(针对 INSERT/DELETE)

四种隔离级别(越高越安全、越慢):

隔离级别 脏读 不可重复读 幻读
读未提交 Read Uncommitted 可能 可能 可能
读已提交 Read Committed(Oracle 默认) 避免 可能 可能
可重复读 Repeatable Read(MySQL 默认) 避免 避免 可能*
串行化 Serializable 避免 避免 避免

MySQL InnoDB 靠 MVCC + 间隙锁,可重复读级别下大多能避免幻读。

六、实践避坑

  1. 事务要短小:内部别放 RPC、发消息等耗时操作,会长期占用连接和锁。
  2. Spring @Transactional 失效场景:非 public 方法、同类内部自调用、异常被 try-catch 吞掉、默认只回滚 RuntimeException(需加 rollbackFor)。
  3. 高并发扣库存:优先用乐观锁替代长事务:
sql 复制代码
UPDATE account SET balance = balance - 100
WHERE id = 'A' AND balance >= 100;   -- 靠 WHERE 防超卖,判断影响行数

七、回滚是自动行为吗

回滚不是数据库"自动"帮你兜底的行为,而是需要有人(数据库自己或应用程序)显式或隐式地触发它。

数据库会自动回滚的情况:

  1. 语句执行出错 / 违反约束(主键冲突、外键约束、字段超长等)。注意:MySQL 默认只回滚出错的那一条语句,而不是整个事务,事务仍处于打开状态。
  2. 死锁:数据库检测到死锁时,自动选一个事务作为牺牲者强制回滚。
  3. 连接中断 / 会话超时 / 崩溃恢复:未提交的事务会被自动回滚(靠 undo log 恢复)。
  4. 锁等待超时。

需要程序主动回滚的情况:

  • 业务规则失败(如"余额不足"):SQL 本身没报错,数据库不知道这是"失败",得靠代码判断并调用 ROLLBACK。
  • 框架帮你做的"自动"回滚(Spring @Transactional、Sequelize 托管事务):本质是框架在捕获到异常时替你调用了 rollback,仍是"程序触发"。
触发方 场景
数据库自动回滚 死锁牺牲者、崩溃/断连恢复未提交事务、(部分)约束错误
程序/框架触发回滚 业务校验失败、捕获到异常、主动调用 ROLLBACK

八、rollback 的逻辑定义在哪

应用层调用的 rollback() 只是一个"发起指令",真正的回滚逻辑不在应用代码里,而在数据库引擎内部。

调用链:

复制代码
你的代码 conn.rollback()
   │  (JDBC/驱动层:只是发一条命令)
   ▼
数据库客户端驱动  →  把 "ROLLBACK" 命令通过网络协议发给数据库服务器
   │
   ▼
数据库服务器(真正的回滚逻辑在这里!)
   │  读取 undo log,逆向撤销本事务所有已改动的数据
   ▼
数据恢复到事务开始前的状态

真正的回滚逻辑靠 undo log(撤销日志)实现(以 MySQL InnoDB 为例):

  • 事务每做一次修改,引擎会先记录一条"反向操作"到 undo log:
    • INSERT 一行 → undo log 记录一条对应的 DELETE
    • UPDATE 把 A 改成 B → undo log 记录"把 B 改回 A"
    • DELETE 一行 → undo log 记录一条对应的 INSERT
  • 收到 ROLLBACK 命令时,引擎从后往前依次执行 undo log 里的反向操作,把数据还原到事务开始前。
  • 这段逻辑由数据库引擎内核源码实现(如 InnoDB 的 trx_rollback 相关函数),应用层看不到也改不了。

各层"rollback 定义"对照表:

层次 "rollback" 是什么 定义/实现在哪
业务代码 try { ... } catch { conn.rollback(); } 你自己写,决定"何时"回滚
框架层 Spring @Transactional 捕获异常后调用回滚 框架源码帮你调 rollback()
JDBC/ORM 接口 Connection.rollback() / session.rollback() JDBC 规范定义接口,数据库驱动实现:发送 ROLLBACK 命令
数据库协议 一条 ROLLBACK 命令 驱动按数据库通信协议发送
数据库引擎 真正撤销数据的逻辑 数据库内核源码,基于 undo log 逆向回放

一句话概括:你(或框架)负责"喊一声回滚",数据库引擎负责"真正把数据倒回去",而它能倒回去是因为提前把每一步的反悔操作都记在了 undo log 里。

相关推荐
CodexDave2 小时前
PostgreSQL 明明有索引却选了 Nested Loop:从行数误判修正执行计划
数据库·postgresql·执行计划·扩展统计·nestedloop
czhaii4 小时前
STC ai助手单片机工程项目创建实例
数据库·mongodb
山峰哥4 小时前
数据库性能救星:Explain执行计划深度拆解
服务器·开发语言·数据库·sql·启发式算法
oradh5 小时前
Oracle 11g rac IP地址修改(public ip、vip、scan ip、priviate ip)
数据库·tcp/ip·oracle·rac ip地址修改
YOU OU5 小时前
Redis哨兵 & 集群
数据库·redis·sentinel
小罗水5 小时前
第8章 文档解析与文本切片
数据库·spring·spring cloud·微服务
瞬间&永恒~5 小时前
【MySQL】 主从复制多拓扑搭建实验
运维·数据库·mysql·云原生
han_hanker6 小时前
sql语法 DECODE, CASE ... WHEN
数据库·sql·oracle
井川廊咏6 小时前
vi 删除指定范围的行,不用再反复按 dd
数据库·mysql