数据库并发问题
数据库并发问题是指多个事务同时操作同一批数据时,因隔离不足导致的数据一致性问题。这些问题的根源是事务并发执行时的“干扰”,而数据库通过事务隔离级别来控制这种干扰的程度。以下是常见并发问题的梳理及对应解决方案:
一、核心并发问题(按严重性/常见性排序)
1. 脏读(Dirty Read)
- 定义:一个事务读取到了另一个事务未提交的数据。
- 场景:
事务A:将余额从100修改为200(未提交);
事务B:读取到余额为200;
事务A:因错误回滚,余额恢复为100;
结果:事务B读取的“200”是无效的“脏数据”。 - 本质:读取了未持久化的数据,违背了“读一致性”。
2. 不可重复读(Non-repeatable Read)
- 定义:同一事务内,多次读取同一数据时,结果不一致(因另一个事务的提交修改导致)。
- 场景:
事务A:第一次读取余额为100;
事务B:修改余额为200并提交;
事务A:再次读取余额为200,与第一次结果不同。 - 本质:事务执行中,数据被其他事务“更新”并提交,导致重复读不一致。
3. 幻读(Phantom Read)
- 定义:同一事务内,多次执行相同范围查询时,结果集的行数不一致(因另一个事务的提交新增/删除导致)。
- 场景:
事务A:查询“余额>0的用户”,返回10条记录;
事务B:新增1条“余额>0的用户”并提交;
事务A:再次执行相同查询,返回11条记录,多了一条“幻影”数据。 - 本质:事务执行中,数据被其他事务“插入/删除”并提交,导致范围查询结果集变化。
4. 更新丢失(Lost Update)
- 定义:两个事务同时修改同一数据,后提交的事务覆盖了先提交事务的修改,导致前者的更新“丢失”。
- 细分:
- 第一类:两个事务直接修改同一字段(如A改100→200,B改100→300,最终成300,A的更新丢失);
- 第二类(更隐蔽):事务A读取数据后基于旧值修改,期间事务B修改并提交,A提交时覆盖B的更新(如A读100准备+50,B改100→200,A提交后成150,B的更新丢失)。
二、事务隔离级别(解决并发问题的核心机制)
为解决上述问题,SQL标准定义了4种隔离级别(从低到高,隔离性越强,并发性能越差),不同级别对问题的解决能力不同:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 更新丢失 | 说明 |
|---|---|---|---|---|---|
| 读未提交(RU) | 允许 | 允许 | 允许 | 允许 | 最低隔离级别,几乎无隔离,仅用于极少数对一致性要求极低的场景(如实时日志)。 |
| 读已提交(RC) | 禁止 | 允许 | 允许 | 禁止 | 大多数数据库默认级别(如Oracle、SQL Server),确保读取的是已提交数据。 |
| 可重复读(RR) | 禁止 | 禁止 | 部分禁止 | 禁止 | MySQL InnoDB默认级别,通过MVCC确保同一事务内重复读一致,可解决部分幻读。 |
| 串行化(Serializable) | 禁止 | 禁止 | 禁止 | 禁止 | 最高隔离级别,事务串行执行,完全避免并发问题,但性能极差。 |
三、关键说明
-
隔离级别与性能的权衡:
隔离性越强(如串行化),数据一致性越有保障,但并发能力越弱(锁竞争激烈);
隔离性越弱(如读未提交),性能越好,但一致性风险越高。实际中需根据业务场景选择(如金融场景常用可重复读,日志系统可用读未提交)。 -
数据库的实现差异:
SQL标准仅定义隔离级别的目标,具体实现不同。例如:- MySQL InnoDB的“可重复读”通过MVCC(多版本并发控制)和Next-Key Lock解决了幻读,超出标准定义;
- Oracle默认“读已提交”,如需可重复读需显式设置。
-
解决更新丢失的额外机制:
除隔离级别外,更新丢失可通过乐观锁(版本号控制)或悲观锁(SELECT ... FOR UPDATE)主动规避,尤其适合高并发写场景。
总结
数据库并发问题的本质是“事务间的干扰”,核心问题包括脏读、不可重复读、幻读和更新丢失。通过选择合适的事务隔离级别,可在一致性与性能间找到平衡,同时结合锁机制或版本控制,可进一步优化并发场景下的数据可靠性。
乐观锁和悲观锁是并发编程中解决数据冲突的两种核心思想,广泛应用于数据库、分布式系统等场景。二者的核心区别在于对“并发冲突”的假设不同,进而导致实现方式和适用场景的差异。
一、核心思想对比
-
悲观锁(Pessimistic Lock):
假设“并发冲突会频繁发生”,因此在操作数据前主动加锁,阻止其他事务对数据的修改,直到自己的操作完成并释放锁。
核心逻辑:“先上锁,再操作,防冲突”。 -
乐观锁(Optimistic Lock):
假设“并发冲突很少发生”,因此操作数据时不上锁,仅在提交时通过“版本校验”判断数据是否被其他事务修改过,若未修改则提交成功,否则失败重试。
核心逻辑:“先操作,提交时校验,有冲突则重试”。
二、实现方式
1. 悲观锁的实现
- 数据库层面:
- 行锁/表锁:如InnoDB的
SELECT ... FOR UPDATE(行级排他锁),锁定指定记录,其他事务无法修改或加排他锁; - 读锁/写锁:如
LOCK TABLES ... READ(共享锁,仅允许读)、LOCK TABLES ... WRITE(排他锁,禁止读写)。
- 行锁/表锁:如InnoDB的
- 编程语言层面:
- Java中的
synchronized关键字、ReentrantLock等,通过锁机制阻止多线程并发修改共享资源。
- Java中的
2. 乐观锁的实现
- 核心机制:版本校验(通过额外字段记录数据版本,提交时比对版本是否一致)。
- 常见实现方式:
- 版本号(Version):在表中新增
version字段,初始值为0。- 步骤1:读取数据时,同时获取
version(如SELECT id, name, version FROM user WHERE id=1); - 步骤2:更新数据时,仅当当前
version与读取时一致才执行更新,并将version+1(如UPDATE user SET name='new', version=version+1 WHERE id=1 AND version=old_version); - 步骤3:若更新影响行数为0,说明版本已被其他事务修改,需重试或处理冲突。
- 步骤1:读取数据时,同时获取
- 时间戳(Timestamp):类似版本号,用
update_time字段记录最后修改时间,提交时比对时间戳是否一致。
- 版本号(Version):在表中新增
三、适用场景对比
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 并发冲突频率 | 适合冲突频繁的场景(写多读少) | 适合冲突很少的场景(读多写少) |
| 性能影响 | 加锁会导致阻塞,并发性能较低 | 无锁阻塞,并发性能高(冲突少的时候) |
| 死锁风险 | 可能因锁竞争导致死锁(需谨慎设计) | 无锁,不存在死锁风险 |
| 重试成本 | 无需重试(锁已保证独占) | 冲突时需重试(重试成本可能较高) |
| 典型场景 | 银行转账、库存扣减(写操作频繁) | 商品详情查询、用户资料修改(读多写少) |
四、优缺点总结
悲观锁
- 优点:
能直接阻止冲突,保证数据一致性,无需手动处理重试逻辑。 - 缺点:
锁竞争会导致线程阻塞,降低并发性能;可能引发死锁(如多个事务交叉锁定资源)。
乐观锁
- 优点:
无锁阻塞,并发性能高;不存在死锁风险。 - 缺点:
冲突时需重试(可能多次失败),增加应用层复杂度;仅能在提交时发现冲突,不适合实时性要求高的场景。
五、总结
- 悲观锁是“预防式”策略,适合写操作频繁、冲突概率高的场景(如金融交易),牺牲性能换取一致性。
- 乐观锁是“检测式”策略,适合读操作频繁、冲突概率低的场景(如电商商品详情),牺牲少量一致性处理成本换取高性能。
实际开发中,需根据业务的“读写比例”“冲突频率”选择合适的方案,甚至混合使用(如核心步骤用悲观锁,非核心步骤用乐观锁)。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)