常识来了
白蓝主题五 · 清爽阅读
首页  > 软件进阶

事务回滚到底是怎么“撤回”的?

你往银行转账,输错账号,点了确认——系统弹出“操作失败”,钱没转走。这背后不是魔法,是数据库在默默执行“事务回滚”。

回滚不是删记录,是“倒带”

很多人以为回滚就是把刚插的数据 DELETE 掉、刚改的字段 UPDATE 回去。其实更准确的说法是:数据库早就在你开始事务时,悄悄记下了每一步操作前的样子——就像手机录屏时同时保存了“操作轨迹”和“原始画面”。

比如执行这条 SQL:

UPDATE accounts SET balance = balance - 100 WHERE user_id = 123;

数据库不会直接改磁盘上的数据页。它先在内存里生成一条“undo log”(回滚日志):记录下 user_id=123 原来的 balance 是 500。接着才把 balance 改成 400。如果之后执行 ROLLBACK,它就翻出那条 undo log,把 balance 再设回 500——不是靠猜,是靠存好的快照。

日志比数据还重要

真正保证回滚可靠的,不是数据文件本身,而是那堆看不见的 undo log。它们被写入专门的日志文件(比如 MySQL 的 ibdata1 或独立 undo 表空间),而且遵循“日志先行”原则:任何数据变更之前,undo log 必须先落盘(或至少进 OS 缓冲)。这样哪怕断电重启,数据库也能从日志里还原出该回滚到哪一步。

你可以把它想象成游戏存档:你打 boss 前手动存了个档;打一半发现打不过,直接读档——读的不是当前残血状态,而是存档那一刻的完整现场。

不是所有操作都能回滚

注意,有些动作天生不支持回滚。比如:

  • DROP TABLE users; —— 表结构丢了,undo log 不会存整个表定义;
  • INSERT INTO logs VALUES ('user_login'); —— 如果这个表用的是非事务引擎(如 MyISAM),压根没 undo log;
  • Sending an email via stored procedure —— 数据库管不了外部系统。

所以写业务逻辑时,别指望一个 ROLLBACK 能撤回发出去的短信、扣掉的库存、或者调用过的第三方支付接口。

实际代码里怎么触发?

以 Python + SQLAlchemy 为例:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine('mysql://user:pass@localhost/db')
Session = sessionmaker(bind=engine)
session = Session()

try:
session.execute("UPDATE orders SET status='shipped' WHERE id=1001")
session.execute("INSERT INTO shipments (order_id) VALUES (1001)")
# 这里突然报错(比如发货单号重复)
raise ValueError("Shipment number conflict")
except Exception:
session.rollback() # ← 这一行,让上面两条 SQL 全部失效
print("已回滚,订单状态和发货单都没变")
finally:
session.close()

关键不在 rollback() 这个函数名,而在于 session 从 begin 到 rollback 之间维护的 undo log 链。它像一根绷紧的橡皮筋,一松手,所有中间态自动弹回原点。

下次看到“事务回滚”,别再想成“删数据”。它是一套精密的时间机器——靠提前记账、靠日志驱动、靠原子性兜底。