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 字段做乐观锁,不是加个字段就行——它只在“冲突概率低 + 更新逻辑简单”的场景下有效;一旦失败重试频繁,反而比悲观锁更耗资源。
技术上面应用了三层结构,AJAX框架,URL重写等基础的开发。并用了动软的代码生成器及数据访问类,加进了一些自己用到的小功能,算是整理了一些自己的操作类。系统设计上面说不出用什么模式,大体设计是后台分两级分类,设置好一级之后,再设置二级并选择栏目类型,如内容,列表,上传文件,新窗口等。这样就可以生成无限多个二级分类,也就是网站栏目。对于扩展性来说,如果有新的需求可以直接加一个栏目类型并新加功能操作
- 必须在 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 是隐形的保护伞,锁是显性的刹车片,而事务边界,是你唯一能亲手划清的防线。









