在MySQL的世界里,多个用户同时操作数据是家常便饭。就像图书馆里既有读者在看书,又有管理员在更新藏书,若没有合理的规则,混乱便会随之而来——有人看到没定稿的内容,有人发现刚看过的书内容变了,甚至有人统计的人数突然多了几个“幽灵”。而MVCC(多版本并发控制)、间隙锁等机制,就是MySQL的“图书馆管理规则”,专门解决这些并发难题。今天我们就用最通俗的方式,把这些核心概念讲明白。
在讲解决方案前,我们得先清楚问题的本质。当多个事务同时操作数据时,容易出现三类典型问题:脏读、不可重复读和幻读。我们用生活场景一一对应,就像看故事一样简单。
小张写了封辞职信,中途保存了草稿但没提交给老板,同事小李偷偷看到了这封草稿,立马传开“小张要走了”。结果小张反悔删了草稿,小李的消息就成了“假新闻”——这就是生活中的“脏读”。
对应到数据库中:事务A修改了一条数据(比如把工资从1万改成8千),但还没提交;事务B这时候读到了这个未提交的修改结果。如果事务A最终反悔回滚,事务B读到的就是“根本不存在”的脏数据,基于这个数据做决策必然出错。
你借了本《MySQL实战》,第一次翻到第10页看到“主键不能为NULL”,喝水回来再翻同一页,内容却变成“主键可以为NULL”——原来管理员趁你离开换了新版。这种“同一内容前后不一致”的情况,就是不可重复读。
数据库场景中:事务A在同一个事务里两次查询同一行数据,第一次查到工资1万,中途事务B修改工资为8千并提交,事务A第二次查询时,数据就变了。这会让事务A对数据的一致性产生困惑。
班主任统计不及格人数,第一次查全班都及格,刚要汇报时再查,却多了1个不及格的学生——原来是教务系统刚录入了一个转学生。这个“凭空出现”的记录,就像幽灵一样,这就是幻读。
在数据库中:事务A执行范围查询(比如“查询余额大于500的账户”),第一次得到1条结果;事务B插入一条余额800的新记录并提交,事务A再次执行相同查询,结果变成了2条。幻读的核心是“结果集行数变化”,和不可重复读的“单行内容变化”有本质区别。
面对这些并发问题,MySQL的InnoDB引擎祭出了“MVCC(多版本并发控制)”这个大杀器。它的核心思想很简单:不直接覆盖旧数据,而是为数据保留多个版本,让不同事务看到自己“该看”的版本。
图书馆里有本《MySQL入门》,最新版是第3版。小明上周借走了第2版开始阅读,你今天把书更新到了第3版。如果图书馆只留最新版,小明的阅读就会被打断;而MVCC相当于图书馆同时保存第2版和第3版,小明继续看自己借的版本,你用你的新版本,互不干扰。
InnoDB会给每行数据偷偷加两个“隐藏字段”:
当你开启一个事务执行查询时,MySQL会根据事务开始时间生成一个“一致性视图(Read View)”,然后通过这个视图去匹配数据版本——只返回“在事务开始前已提交”或“本事务修改”的版本。这样一来,即使其他事务在修改数据,你看到的始终是自己事务开始时的快照,自然不会被干扰。
MVCC能解决脏读和不可重复读,但对付幻读还不够——因为幻读是“行数变化”,MVCC的快照读只能屏蔽已提交的修改,却挡不住新插入的记录。这时候,“锁”就要登场了,其中最关键的就是间隙锁(Gap Lock)和临键锁(Next-Key Lock)。
InnoDB的读操作分两类,处理幻读的方式完全不同:
读类型 | 示例 | 是否加锁 | 能否看到新数据 |
|---|---|---|---|
快照读 | 普通SELECT | 否 | 看不到(靠MVCC快照) |
当前读 | SELECT ... FOR UPDATE、UPDATE、DELETE | 是 | 能看到(需最新数据) |
简单说:快照读是“看历史”,用MVCC防幻读;当前读是“看现在”,必须用锁防幻读。 |
间隙锁不是锁具体的某一行,而是锁住索引之间的“空白区域”。比如表中主键id有5、10、20这三个值,那么索引间隙就包括(-∞,5)、(5,10)、(10,20)、(20,+∞)。这些“空隙”就是可能插入新记录的地方,锁住它们就能从根源上杜绝幻读。
举个例子:执行SELECT * FROM t WHERE id >10 AND id <25 FOR UPDATE,InnoDB会:
临键锁是InnoDB的默认锁类型,本质是“间隙锁+记录锁”的组合,锁定的区间是“左开右闭”。比如对id=10加临键锁,锁定的是(5,10]区间——既包括id=10这条记录,也包括它前面的间隙(5,10)。这样一来,既保护了现有记录不被修改,又防止了新记录插入到间隙中,是防幻读的核心武器。
有个特殊情况要注意:如果用唯一索引(比如主键)做等值查询,且查询的值存在,InnoDB会自动把临键锁“降级”为记录锁——因为唯一索引能保证不会有重复值插入,没必要锁间隙,这样能提高并发性能。但如果查询的值不存在(比如查id=25,而表中最大id是20),则仍会锁住(20,+∞)这个间隙。
遇到锁等待或死锁时,我们需要查看锁的状态来排查问题。MySQL提供了几个实用工具,新手也能轻松上手:
执行以下命令,能看到最新的死锁信息和活跃事务的锁情况:
SHOW ENGINE INNODB STATUS\G
在输出结果的“LATEST DETECTED DEADLOCK”部分,能清晰看到死锁双方的事务、持有的锁和等待的锁,帮你快速定位问题。
先开启监控开关:
-- 开启锁监控
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES'
WHERE NAME LIKE '%wait/lock%';
-- 开启锁消费器
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME LIKE '%locks%';
然后查询锁信息:
-- 查看当前持有的所有锁
SELECT * FROM performance_schema.data_locks;
-- 查看锁等待关系
SELECT * FROM performance_schema.data_lock_waits;
其中“LOCK_MODE”字段能显示锁的类型,比如“X,GAP”表示排他间隙锁,“X”表示排他记录锁,通过这些信息能快速判断锁的作用范围。
最后用一张表总结核心内容,帮你加深记忆:
概念 | 核心作用 | 关键特点 |
|---|---|---|
MVCC | 解决脏读、不可重复读 | 数据多版本,读不加锁 |
间隙锁 | 阻止间隙插入新记录 | 锁“空隙”不锁具体行 |
临键锁 | 防幻读的默认锁 | 记录锁+间隙锁,左开右闭区间 |
快照读 | 读取历史快照 | 普通SELECT,靠MVCC实现 |
当前读 | 读取最新数据 | 加锁操作,靠间隙锁防幻读 |
其实MySQL的并发控制机制没那么可怕,记住“MVCC负责看不见变化,间隙锁负责插不进新数据”这个核心逻辑,很多问题就迎刃而解了。
最后再推荐下不会前端纯靠靠ai手工做的小程序。
推荐👍 :Java高并发编程基础三大利器之CyclicBarrier
推荐👍 :Java高并发编程基础三大利器之CountDownLatch
推荐👍 :Java高并发编程基础三大利器之Semaphore
推荐👍 :Java高并发编程基础之AQS
推荐👍 :可恶的爬虫直接把生产6台机器爬挂了!
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构、等等。获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。