1. 数据库并发问题全景解析
在数据库系统设计中,事务隔离级别是个永恒的话题。我处理过太多因为隔离级别设置不当导致的线上事故,今天就用最直白的方式,拆解三种典型的并发问题:脏读、不可重复读和幻读。这些都是数据库工程师面试必考题,更是实际开发中必须跨过的坎。
2. 脏读问题深度剖析
2.1 现象与定义
想象这样一个场景:事务A修改了某条记录但未提交,事务B此时读取到了这个未提交的修改。如果事务A最终回滚,那么事务B读到的就是"脏数据"。我在金融系统开发中就遇到过真实案例:余额更新中途被其他事务读取,导致对账出现百万级差额。
2.2 底层原理
本质是读未提交(Read Uncommitted)隔离级别下的问题。数据库引擎不会对未提交的数据加读锁,MVCC机制在此级别也不生效。以MySQL为例,其内存中的undo日志还未固化,其他事务就能看到变更。
2.3 复现实验
sql复制-- 会话1
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- 会话2(隔离级别设为READ UNCOMMITTED)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 1; -- 能看到未提交的修改
-- 会话1
ROLLBACK; -- 此时会话2读到的数据就变成脏数据
2.4 解决方案
- 升级到READ COMMITTED隔离级别
- 对关键业务数据添加SELECT FOR UPDATE锁
- 应用层增加版本校验机制
关键提示:金融类系统绝对禁止使用READ UNCOMMITTED,这是铁律!
3. 不可重复读问题全解
3.1 现象与定义
同一个事务内,两次相同的查询得到不同结果。比如事务A第一次查询余额为100,此时事务B修改余额为200并提交,事务A再次查询就变成了200。我在电商系统开发中遇到过库存重复扣减的问题就源于此。
3.2 与脏读的本质区别
- 脏读读取的是未提交数据
- 不可重复读读取的是已提交的修改
- 两者在锁机制上的实现差异很大
3.3 复现实验
sql复制-- 会话1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 1; -- 返回100
-- 会话2
UPDATE accounts SET balance = 200 WHERE user_id = 1;
COMMIT;
-- 会话1
SELECT balance FROM accounts WHERE user_id = 1; -- 返回200
COMMIT;
3.4 解决方案
- 使用REPEATABLE READ隔离级别
- 使用乐观锁(版本号控制)
- 对关键查询添加共享锁(SELECT ... LOCK IN SHARE MODE)
4. 幻读问题终极指南
4.1 现象与定义
事务A读取某个范围的数据后,事务B在该范围内插入了新数据,事务A再次读取时会出现"幻行"。最经典的例子是统计查询:第一次count(*)返回10条,第二次变成11条,就像出现了幻觉。
4.2 与不可重复读的区别
- 不可重复读针对已存在的行数据变更
- 幻读针对新增或删除的行
- 解决方案完全不同
4.3 复现实验
sql复制-- 会话1
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 返回2
-- 会话2
INSERT INTO orders(user_id, amount) VALUES(1, 100);
COMMIT;
-- 会话1
SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 仍然返回2
UPDATE orders SET status = 'processed' WHERE user_id = 1; -- 影响行数3
SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 现在返回3
COMMIT;
4.4 解决方案
- 使用SERIALIZABLE隔离级别(性能代价大)
- 使用间隙锁(Gap Lock)
- 应用层使用唯一约束+重试机制
- MySQL的Next-Key Locking机制
5. 隔离级别实战选择建议
5.1 各级别对比
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | ❌ | ❌ | ❌ | ★★★★ |
| READ COMMITTED | ✅ | ❌ | ❌ | ★★★ |
| REPEATABLE READ | ✅ | ✅ | ❌ | ★★ |
| SERIALIZABLE | ✅ | ✅ | ✅ | ★ |
5.2 选型策略
- 日志分析系统:可用READ UNCOMMITTED
- 大多数OLTP系统:推荐READ COMMITTED
- 财务系统:至少REPEATABLE READ
- 票务系统:考虑SERIALIZABLE
5.3 MySQL特别说明
MySQL的InnoDB在REPEATABLE READ下通过Next-Key Locking已经能解决大部分幻读问题,这点和SQL标准不同。实际项目中我们会先用REPEATABLE READ测试,确实遇到问题再升级。
6. 实战避坑指南
- 连接池配置陷阱:确保连接的事务隔离级别与预期一致
- Spring事务传播行为的交互影响
- 监控长事务的工具推荐:
- MySQL: information_schema.innodb_trx
- PostgreSQL: pg_stat_activity
- 性能优化技巧:
- 尽量缩短事务持续时间
- 避免在事务中进行网络调用
- 大事务拆分为小批次
我在处理某次线上事故时发现,即使设置了REPEATABLE READ,由于连接池复用连接时未重置隔离级别,导致出现幻读。后来我们强制在获取连接后执行SET TRANSACTION语句才彻底解决。