一、先搞懂什么是幻读?别被名字吓住,其实就是“假的重复读”
很多开发者用达梦数据库时,偶尔会碰到“明明没改核心数据,前后查询结果却不一样”的情况,大概率就是幻读——听起来玄乎,其实用日常小事就能讲清:你每周三固定去小区菜店买青菜,老板用记账本记了剩余蔬菜数量。第一次你看账本写着“剩5斤”,你选了3斤;10分钟后再去,账本写着“剩8斤”,老板说刚才有人补了3斤青菜。你会觉得“不对,我上次看的是5斤,怎么这次变多了?”——这就是幻读:同一个事务范围内,两次查“同条件的结果集合”,因为中间插了新数据,看起来像出现了幻觉。
1.1 幻读和不可重复读的区别
不可重复读是同一事务里,两次查同一行数据结果变了;幻读是同一事务里,两次查**同一范围(多行数据)**结果变了——比如查“所有青菜的总剩余”,中间多了一行青菜数据,总数值就变了,这和“改一行数据”的不可重复读是两码事。
二、MVCC是啥?大白话就是“给数据套上临时外套,各玩各的”
很多人听到MVCC就头疼,其实它是数据库用来处理并发的“软锁”:不用硬锁行或表,而是给每个事务开始时的所有数据拍一个“快照”(临时拷贝),事务读数据时只看快照内容,完全不管其他事务后来对数据的修改。
2.1 MVCC在达梦里的具体逻辑
达梦的MVCC有个简单规则:每个事务启动时,都会生成一个唯一的“事务ID”,数据的每一行都会附带“创建它的事务ID”和“删除它的事务ID”。事务读数据时,只看“快照范围内存在的行”——也就是那些创建ID在当前事务ID之前,删除ID还没到当前事务ID的行。还是用菜店举例:你买青菜的事务启动时,快照是“只有5斤青菜的行”,不管老板后来补了多少青菜,你这次买青菜的事务里,始终只会看到5斤的记录,不会被新增的3斤干扰,完美避开幻读。
2.2 为什么有些场景还是会出幻读?
核心原因是隔离级别没选对。达梦的隔离级别分四个:读未提交、读提交、可重复读、串行化。如果选了“读提交”,事务每次读数据都会去查最新的实时数据,不是快照,这就会出现幻读——就像你第二次去菜店时,刚好老板补了青菜,你就看到了新数据;只有选“可重复读”,配合MVCC,才能让事务全程用同一快照,避免幻读。
三、隔离级别选错引发的幻读:达梦实际代码示例
以下示例用单一技术栈(达梦数据库V8),完整演示幻读的出现和避免:
-- 技术栈:达梦数据库 V8
-- 场景:对比读提交和可重复读隔离级别下的幻读问题
-- 步骤1:创建测试表并初始化数据
CREATE TABLE vegetable_stock (
id INT PRIMARY KEY,
name VARCHAR(20), -- 蔬菜名称
stock INT -- 当前剩余数量
);
INSERT INTO vegetable_stock VALUES (1, '青菜',5); -- 初始青菜库存5斤
COMMIT;
-- 步骤2:会话A(用户买青菜)、会话B(老板补青菜)的操作
-- 会话A:设置隔离级别为读提交,启动事务
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 第一次查询所有青菜的总库存
SELECT SUM(stock) FROM vegetable_stock WHERE name='青菜'; -- 结果:5
-- 此时暂停会话A,切换到会话B操作
-- 会话B:设置隔离级别为读提交,启动事务并补青菜
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
INSERT INTO vegetable_stock VALUES (2, '青菜',3); -- 新增一行青菜库存3斤
COMMIT; -- 老板提交补菜操作
-- 回到会话A,第二次查询所有青菜的总库存
SELECT SUM(stock) FROM vegetable_stock WHERE name='青菜'; -- 结果:8(和第一次结果不同,这就是幻读)
COMMIT; -- 会话A结束
3.1 用可重复读避免幻读的调整
把两个会话的隔离级别改成“可重复读”,就能避开幻读:
-- 调整后的隔离级别(会话A和B都执行)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
-- 会话A第一次查询,结果仍为5(快照里的初始数据)
SELECT SUM(stock) FROM vegetable_stock WHERE name='青菜';
-- 会话B插入新数据并提交后,会话A第二次查询
SELECT SUM(stock) FROM vegetable_stock WHERE name='青菜'; -- 结果还是5(快照未变,无幻读)
COMMIT;
四、并发控制的最佳实践:怎么选隔离级别?
4.1 对应业务场景的隔离级别选择
- 普通查询/更新(如商品浏览、订单提交):选「读提交」,性能开销小,足够处理大多数场景,不会有明显并发问题;
- 一致性要求高的场景(如报表统计、用户余额查询):选「可重复读」,靠MVCC保证同一事务内多次查询结果一致,避免幻读;
- 高并发核心场景(如支付转账):选「串行化」,虽然性能略低,但完全隔离并发事务,彻底避免各类并发问题;
- 避免过度使用可重复读:每个可重复读事务都会保留快照,长时间运行的事务会占用更多内存/磁盘空间,后台需要定时清理垃圾快照。
4.2 技术优缺点分析
MVCC的优点:不用硬锁行,减少锁等待,并发性能比传统锁机制高很多;缺点:快照的存储会有轻微空间开销,长时间事务可能导致快照垃圾堆积。 隔离级别选错的弊端:读提交会出现幻读,可重复读有空间开销,串行化性能下降明显,高并发场景下可能扛不住流量。
4.3 注意事项
- 不要为了避免幻读盲目用串行化,除非是极端核心场景,否则会把并发变成串行,性能下降一个数量级;
- 达梦的只读事务对MVCC优化最好,尽量把业务拆成只读和读写事务,只读事务用可重复读没问题;
- 事务执行时间尽量短,长时间运行的事务(如批量数据处理)要避免用可重复读,防止快照垃圾过多。
五、总结
幻读本质是事务隔离级别和并发控制机制不匹配导致的问题,达梦的MVCC在可重复读级别下能很好地解决这个问题。实际开发中,不需要追求最高的隔离级别,要根据业务的一致性要求和并发压力选最合适的隔离级别:普通业务用读提交,一致性要求高的用可重复读,极端核心场景用串行化,这样既能避免幻读,又能保证数据库的并发性能。
Comments