你往银行转账,输错账号,点了确认——系统弹出“操作失败”,钱没转走。这背后不是魔法,是数据库在默默执行“事务回滚”。
回滚不是删记录,是“倒带”
很多人以为回滚就是把刚插的数据 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 链。它像一根绷紧的橡皮筋,一松手,所有中间态自动弹回原点。
下次看到“事务回滚”,别再想成“删数据”。它是一套精密的时间机器——靠提前记账、靠日志驱动、靠原子性兜底。