3.1 事务与锁机制
在单用户、单请求的工作流中,数据库的写操作是安全的。然而,而在高并发的多人在线环境下(例如电商秒杀、银行转账、座位预订),多位用户可能会同时读取和修改相同的数据行。如果不加以约束,数据的正确性将瞬间崩塌。
数据库通过事务 (Transaction) 与 锁 (Lock) 两把坚盾,来保障并发状况下数据的强一致性。
什么是事务?
简单来说,事务是将多条 SQL 修改整合在一起的、不可分割的工作逻辑单元 (Logical Unit of Work)。
例如典型的银行划账流程:
1. 第一步:扣除账户 A 的余额 $100 (UPDATE accounts SET balance = balance - 100 WHERE id = A;)
2. 第二步:将 $100 转入账户 B 的余额 (UPDATE accounts SET balance = balance + 100 WHERE id = B;)
如果第一步成功,但刚好由于网线被拔掉、系统崩溃或者限额触发导致第二步失败,这 $100 将离奇消失。所以,我们要么让它们全部生根落地,要么发生任何意外时彻底擦除修改痕迹:
START TRANSACTION; -- 或者 BEGIN; 开启事务,提供一个私密沙盒环境
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 'A';
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 'B';
-- 检查上面所有命令是否均被数据库校验接收,确认无误时
COMMIT; -- 提交:将沙盒里的所有结果强力刷盘,转化为不可磨灭的永久物理现实如果在执行过程中某一行检查出负余额、额度超标或者外键冲突,则应立即全盘回退:
ROLLBACK; -- 回滚:撤销当前事务中所有已完成的内存修改,让数据无损重现 BEGIN 之前的相貌BEGIN;MySQL 推荐高阶的 START TRANSACTION,但也支持别名 BEGIN;而 SQL Server 则独创一派,写作为 BEGIN TRANSACTION。深刻理解 ACID 黄金原则
事务的运作受到极高信仰的规范,即著名的 ACID 原子法则:
1. Atomicity (原子性):事务就如一颗核分裂原子,要么完美全部提交,要么因任何错失退回,归为全无 (All or Nothing)。物理修改没有中间状态。
2. Consistency (一致性):数据库必须从一个合法状态跃迁到另一个合法状态。在事务开始前和结束后,所有的校验规则(外键、主键唯一、CHECK 触发器约束)必须完全过关。
3. Isolation (隔离性):两个并发的、同时运作的事务不能互相染指对方的临时修改快照。隔离级别保证了多个事物看起来像是“串行”一个接一个执行的。
4. Durability (持久性):一旦 COMMIT 宣告成功并由数据库回应,修改就彻底定盘。即便在写盘反馈后的 0.01 秒系统由于断电瘫痪,重启后新修改也必须要通过重做日志(Write-Ahead Log, WAL)可靠地重现。
并发冲突与四大隔离级别
由于每个人都要争抢高速度,数据库如果完全实行“串行排队”会极度拖慢响应性能。为此,SQL 规范将读写隔离划分了四个等级,由弱到强,并折射出了完全不同的底层并发隐患:
| 隔离级别 (Isolation Level) | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) | 底层常见防护原理 |
|---|---|---|---|---|
| Read Uncommitted (未提交读) | 允许 ⚠️ | 允许 ⚠️ | 允许 ⚠️ | 几乎不加锁,Session 直接读取其他事务在内存里的临时草稿修改。 |
| Read Committed (已提交读) | 防止 🟢 | 允许 ⚠️ | 允许 ⚠️ | 只能读取对方已提交的数据。在每次 SELECT 时获取一个最新的局部读一致性快照。 |
| Repeatable Read (可重复读) | 防止 🟢 | 防止 🟢 | 允许 ⚠️ (有些如MySQL支持防止) | 在事务启动首个 SELECT 时锁定数据视图快照,整个事务内怎么多遍读,数据均保持静止。 |
| Serializable (可串行化) | 防止 🟢 | 防止 🟢 | 防止 🟢 | 强制所有的读写操作依次排队、排他加锁执行。效率最低。 |
数据库锁:并发下的交通指挥灯
要保持数据不被改崩,底层的奥秘就是 加锁机制 (Locking)。锁主要分成两个方向:
1. 共享锁 (S锁 / Shared Lock / 读锁):允许其他事务也来读取,但没人可以改。大家可以共享读,就像图书馆里的一本开架书,谁都可以翻,谁都不准剪。
2. 排他锁 (X锁 / Exclusive Lock / 写锁):一旦被某人获取,不让别人写,甚至不让别人读。就像私人日历,一旦被写作者锁定,外部无法触碰。
🚨 警惕:让人绝望的死锁 (Deadlock)
当两个不同的事务彼此拿了对方想要加锁的物品时,就会形成彼此无限期等候的死循环。
【事务 A】 【事务 B】
持有 checking 行的写锁 持有 savings 行的写锁
想要对 savings 行进行加锁 ... 想要对 checking 行进行加锁 ...
(等待中 ⏳) (等待中 ⏳)这就是死锁!在真实高负荷项目中非常常见:
- 死锁解除:大多数数据库都有“死锁检测机制”(Deadlock Detector),当发现死循环时,会自动强制把其中一个代价较小的事务“牺牲”杀掉,使其
ROLLBACK,并抛出重试异常,让另一个事务能够顺利完成。
工程必懂:事务设计的好习惯
设计安全的商业逻辑事务,切记遵守以下生存法则:
1. 事务要极其短小 (Keep it Short):不要在 BEGIN 与 COMMIT 之间夹杂任何耗时的网络请求、计算模型甚至文件读写!事务越长,持有的锁时间越长,高并发项目立马就会死锁超时或锁等待爆炸崩溃。
2. 严格固定资源访问顺序:如果所有的事务转账逻辑中都是固定“先锁定 A 账户,再锁定 B 账户”,那锁由于都是单向行进,死锁的概率就可以降到微乎其微。
3. 不要滥用高隔离级别:尽可能维持在产品默认的隔离级别(如大多数数据库默认的 READ COMMITTED 或者是 MySQL InnoDB 默认的 REPEATABLE READ),依靠 MVCC (多版本并发控制) 的快照读优势,把排他 Serializable 重锁降到最低。
4. 事务失败机制必须完备:捕获所有错误并编写周全的 ROLLBACK 清洗操作。