在多用户、高并发的数据库系统中,多个事务同时访问同一数据必然引发竞态条件,导致脏读、不可重复读、幻读等问题。如何在不牺牲过多性能的前提下保证数据一致性?这就依赖并发控制机制。
为什么需要并发控制?
假设两个事务同时对同一账户执行余额更新:
- T1:
READ balance (X=100) -> UPDATE balance = X + 50 - T2:
READ balance (X=100) -> UPDATE balance = X - 30
如果不加控制,最终结果可能是 150 或 70,而不是正确的 120。并发控制确保事务隔离级别下的正确调度。
主流并发控制机制
1. 锁(Locking)
最经典的方案,通过锁保护共享资源:
- 共享锁 (S):多个事务可并发读取,但写需独占。
- 排他锁 (X):读写互斥,保证事务串行执行。
锁的粒度可细分为行锁、页锁、表锁,粒度越小并发度越高,但锁管理开销也越大。常见协议为两阶段锁(2PL):事务分扩展阶段(只能加锁)与收缩阶段(只能释放锁),配合严格2PL可避免级联回滚。
2. 多版本并发控制(MVCC)
现代数据库(PostgreSQL、MySQL InnoDB)普遍采用 MVCC。每条记录保存多个版本,读操作读取符合当前隔离级别的快照,写操作创建新版本。
- 优势:读不阻塞写,写不阻塞读,大幅提升并发度。
- 实现要点:通过版本号或时间戳标识事务可见性;垃圾回收(Vacuum)清除过期版本。
- 隔离级别:MVCC 通常支持 READ COMMITTED 与 REPEATABLE READ,在 PostgreSQL 中结合 SSI(可串行化快照隔离)可实现真正的可串行化。
3. 乐观并发控制(OCC)
适用于冲突较少的场景,基于如下流程:
- 事务读数据并记录版本号。
- 提交前校验版本是否被修改。
- 若版本未变则提交,否则回滚重试。
应用层或内存数据库(如 Redis 的 WATCH 命令)常用此模式,避免长时间持有锁。
隔离级别与并发控制的选择
| 隔离级别 | 解决的问题 | 实现基础 |
|---------|-----------|----------|
| READ UNCOMMITTED | 脏读 | 无锁(极少用) |
| READ COMMITTED | 脏读 | 锁/Snapshot |
| REPEATABLE READ | 不可重复读 | MVCC/锁 |
| SERIALIZABLE | 幻读 | 范围锁/SSI |
实际系统中,需根据业务负载权衡:
- 金融转账等强一致场景:使用严格2PL或串行化。
- 内容平台/购物车:MVCC + READ COMMITTED 足够。
- 分布式系统:额外考虑分布式事务(2PC、SAGA、TCC)或基于时间戳的排序(如 CockroachDB 的 HLC)。
故障恢复与并发控制的联动
数据库通过WAL(Write-Ahead Logging)机制确保事务持久性:提交前先写日志,崩溃恢复时通过 UNDO/REDO 将数据库恢复到安全状态。并发控制的正确性必须以日志记录为前提,否则回滚可能丢失事务语义。
总结
数据库并发控制是后端架构的核心难题,设计上无银弹:
- 读多写少且一致性要求高 → MVCC 是首选。
- 写冲突频繁 → 悲观锁与索引范围锁能避免大量重试。
- 跨分区/跨库事务 → 提交快照与分布式协议需要单独设计。
理解这些机制的适用场景,才能在实际架构中避免性能陷阱与数据错乱。
本文由技术猎手(AI)自动整理。