MySQL 并发访问核心是读写一致性保障,InnoDB 通过 MVCC+ 行级锁 + 可重复读隔离级别协同实现读不阻塞写、写不阻塞读,但写写互斥;需重点关注写冲突、锁范围、事务生命周期及乐观锁重试机制。

MySQL 并发访问 不是“能不能同时连”,而是“多个连接同时读写同一份数据时,MySQL 怎么不搞错、不丢数据、不卡死”。
核心结论:InnoDB 默认用 MVCC + 行级锁 + 事务隔离级别(可重复读)协同工作,让读不阻塞写、写不阻塞读(大部分情况),但写写之间仍会互斥——这才是你实际 编码 时真正要盯住的边界。
什么是“并发访问”?别被术语绕晕
它就等于:你写的代码里 SELECT 和 UPDATE 同时跑在不同线程 / 请求里,还可能操作同一张表、甚至同一行。
- 读 - 读:安全,完全 无锁(比如 100 个人查商品库存,没问题)
- 读 - 写:InnoDB 用 MVCC 隔离版本,通常不加锁(
SELECT不会等UPDATE提交) - 写 - 写:高危区!两个
UPDATE user SET balance = balance - 100 WHERE id = 1可能互相覆盖,必须靠锁或事务兜底
为什么“select for update”不是万能解药?
很多人一遇到并发更新就加 SELECT …… FOR UPDATE,结果发现性能暴跌、死锁频发——因为它本质是“先查再锁”,中间有时间窗口,且锁范围容易失控。
- 它只在当前事务内生效,不能防止其他事务在你
SELECT之前就已持有该行锁 - 如果没走索引,InnoDB 会升级为表锁(
SELECT * FROM user FOR UPDATE→ 整张表卡住) - 嵌套事务或长事务中使用,锁会一直挂着,拖垮整个连接池
- 正确姿势:确保 WHERE 条件命中索引,并尽量缩短事务生命周期
START TRANSACTION; -- 必须走主键或唯一索引,否则可能锁全表 SELECT balance FROM account WHERE user_id = 123 FOR UPDATE; UPDATE account SET balance = balance - 50 WHERE user_id = 123; COMMIT;
乐观锁怎么写才真“乐观”?
用 version 字段做乐观锁,不是加个字段就行——它只在“冲突概率低 + 更新逻辑简单”的场景下有效;一旦失败重试频繁,反而比悲观锁更耗资源。
- 必须在 UPDATE 的 WHERE 子句里校验
version,漏掉就等于没锁 - 应用层需捕获影响行数为 0 的情况,并主动重试(不是抛异常就完事)
- 注意时钟 / 版本号生成方式:用数据库自增
version比用NOW()更可靠
UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 456 AND version = 2;
执行后若 ROW_COUNT() == 0,说明已被别人抢先更新,此时应重新查最新 stock 和 version,再试一次。
最容易被忽略的坑:隔离级别不是全局开关
你设了 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,不代表所有 SQL 都按这个跑——InnoDB 的 MVCC 行为和锁策略,还取决于语句类型、索引、是否在事务块里。
-
SELECT单独执行 → 快照读(不加锁),走 MVCC -
SELECT …… FOR UPDATE / LOCK IN SHARE MODE→ 当前读(加锁),绕过 MVCC - 即使在
READ COMMITTED下,UPDATE依然会对匹配行加 X 锁,且锁到事务结束 - 线上误配成
REPEATABLE READ后又用SELECT …… FOR UPDATE,可能触发间隙锁(Gap Lock),锁住不存在的记录范围,引发隐蔽死锁
真实并发问题从来不在“能不能连”,而在“谁改了什么、什么时候可见、锁住了谁、有没有漏判”。MVCC 是隐形的保护伞,锁是显性的刹车片,而事务边界,是你唯一能亲手划清的防线。






























