MySQL幻读问题到底咋解决,教你一招搞定数据库里那些莫名其妙的重复数据情况
- 问答
- 2026-01-12 00:15:03
- 2
说到MySQL数据库,很多人都会遇到一个让人头疼的问题,就是有时候明明写了代码去检查数据不存在才插入,但跑着跑着,数据库里还是出现了莫名其妙的重复数据,你查日志也觉得没问题,但重复数据就像幽灵一样,时不时冒出来,这个问题,很可能就是传说中的“幻读”在作怪,今天咱们就抛开那些复杂难懂的专业术语,用大白话把它讲清楚,并告诉你一个最常用的解决招数。
幻读到底是个啥?
咱们想象一个场景,你开了一个网上书店,有一个“书籍库存表”,你要做一件事:查询所有库存小于10本的书,然后给这些书都补货100本。
你可能会写这样的代码(先别管具体语法):
- 开启一个数据库事务(事务就是你希望被当成一个整体来执行的一系列操作,要么全成功,要么全失败)。
- 执行一条查询语句:
SELECT * FROM books WHERE stock < 10;,假设这时候查出来有2本书:《三国演义》和《水浒传》,库存都是5本。 - 你根据查询结果,生成两条更新语句:
UPDATE books SET stock = stock + 100 WHERE id = 1;(id=1是三国演义)和UPDATE books SET stock = stock + 100 WHERE id = 2;。
在你正执行这个事务的过程中,可能发生这样的事:另外一个用户,他可能下了一个订单,买了一本《红楼梦》,导致《红楼梦》的库存从15本变成了14本,这没事,因为它本来不在你的查询范围里,但巧的是,可能还有另一个管理员,他手动添加了一本新书《西游记》,并且初始库存只设置了5本!而这本书,正好符合“stock < 10”的条件。
这时候,你的第二步更新操作只会更新你当初查到的《三国演义》和《水浒传》,当你提交事务后,你会发现一个问题:库存小于10本的书,除了那两本,现在又多了一本《西游记》没有被补货!对你整个事务来说,在你执行过程中,好像凭空“幻影”般地多出了一行符合你条件的数据,这就是“幻读”,它强调的是在同一个事务中,两次相同的查询,后一次查询看到了前一次查询没有看到的“新”数据行。
这和普通的重复数据有啥关系?
最直接的关系就出现在“防重插入”场景,你要根据用户名保证用户唯一。
你的代码逻辑可能是:
- 开启事务。
- 查询数据库:
SELECT * FROM users WHERE username = ‘张三’;,发现没有记录。 - 执行插入:
INSERT INTO users (username) VALUES (‘张三’);。 - 提交事务。
在并发量高的时候,可能同时有两个请求都要注册“张三”,请求A和请求B几乎同时开启了事务,它们在第二步查询时,都发现没有“张三”,于是都认为可以插入,请求A先插入了张三,然后请求B也插入了张三,数据库里就有了两个“张三”,重复数据就这么产生了,对于请求B的事务来说,它第一次查询没看到张三,第二次插入前(虽然没再查询)或者提交后,却看到了另一个张三,这也是一种幻读现象。
那怎么解决呢?教你最常用的一招:升级锁的力度
MySQL默认的隔离级别是“可重复读”,在这个级别下,普通查询是“快照读”,就像给数据拍了张照片,事务期间一直看这张照片,所以看不到别的事务提交的新数据,但这防不住“当前读”带来的幻读,什么是当前读?像INSERT、UPDATE、DELETE这种会修改数据的操作,以及SELECT ... FOR UPDATE这种查询,都是当前读,它们会读取数据最新的版本。
解决幻读的关键就是:在第一次查询的时候,就用“当前读”的方式把数据锁住,不让别的事务有机会插入符合我条件的新数据。
这招就是 SELECT ... FOR UPDATE。
回到我们用户注册的例子,正确的做法应该是:
- 开启事务。
- 使用当前读查询:
SELECT * FROM users WHERE username = ‘张三’ FOR UPDATE;,这一步非常关键! - 判断查询结果,如果有记录,说明用户名已存在;如果没记录,则执行插入。
- 提交事务。
这个FOR UPDATE是干嘛的?它会给查到的数据(虽然这里没查到)加上一种叫“间隙锁”的锁,在我们这个例子里,它锁住的不是某一条具体的记录(因为张三还不存在),而是锁住了“用户名是张三”这个条件所在的位置(一个数据范围间隙),这个锁会阻止其他事务在这个间隙里插入新的“张三”记录。
当请求A执行了SELECT ... FOR UPDATE后,它已经锁定了“张三”这个位置,此时请求B也来执行同样的语句,它会被卡在第二步,必须等待请求A的事务结束后(无论是提交还是回滚),请求B才能继续执行,这时请求B再查询,就会发现“张三”已经被请求A创建了,从而避免重复插入。
总结一下
解决MySQL幻读(以及由此引发的重复数据问题),尤其是在插入场景下的,最直接有效的一招就是:
- 在事务中,先发制人,使用
SELECT ... FOR UPDATE进行查询。 - 这把锁能够阻止其他事务插入可能导致幻读的新数据,从而保证了数据的一致性。
FOR UPDATE是一种悲观锁,用不好会影响性能,因为它会阻塞其他事务,所以在实际使用时,要确保事务尽可能短小精悍,拿到锁后快速操作然后释放,但对于需要强一致性的核心业务,比如扣减库存、防止唯一键冲突等,这招是非常管用的,根据美团技术团队发布的文章《MySQL幻读问题详解》中的阐述,通过Next-Key Lock(临键锁,行锁+间隙锁)机制,正是MySQL在可重复读隔离级别下解决幻读问题的核心手段,而SELECT ... FOR UPDATE就是主动利用这一机制的方法之一。

本文由水靖荷于2026-01-12发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/78994.html
