summery
- Mysql 默认使用 RR, 在这个状态下面,其他事务线程开启修改中间的数据不能被看到,看到的是 undo log 里面上一个版本的 read views 这个主要是确保对应的最终一致性,中间数据可以快速读取,不需要像 serilizable 这样导致强一致性读取速度减慢,同时也避免了 RU 的脏读问题,和 RC 的不可重复读问题
- Mysql 中间有两种读操作,一种是快照读 直接 select 是查看当前事务开始之前的状态 使用 MVCC 实现对应的版本 undo log,第二种就是当前读 select ... for update / update / delete 获取的就是最新的读取数据同时加锁
- 锁等待只是对应的锁的修改等待前一个事务释放,死锁是 当前线程持有锁同时请求另一个锁,另一个线程持有另一个锁请求当前线程的锁,这样导致循环出现,就是构成了死锁
- 没有索引的where 条件就是扫全表,给全表上锁,一切 当前读的where 条件必须检查是否添加索引
index & lock
- 唯一索引 + 唯一条件 只会锁一条记录 || 普通索引 + 范围查询 锁住扫描范
> 原因是 唯一索引数据库中间最多只有一个数据 || 但是范围查询中间有多少个数据,同时无法确定是否这个区间会被插入和添加多少个条件数据,之后会导致直接锁整个范围,如果不加索引全表扫描,并发起来之后就会导致对应的 RT 飙高
- 只锁住一条记录的锁是 Record Lock || 锁住一个扫描范围
生产应对方案
1. 上线前强制规范(第一道防线)
"我们团队强制要求:所有
DML语句(Update/Delete)的WHERE条件,必须先用Explain查看执行计划,确保type是ref或range,且rows扫描行数控制在1000以内。如果type=ALL(全表扫描),禁止上线。"
2. 线上监控(第二道防线)
"我们在
performance_schema和慢查询日志里开启了lock_time(锁等待时长)监控。一旦发现锁等待超过1秒,立即触发告警,DBA会强制kill该会话。"
3. 索引降级策略(第三道防线)
"如果确实存在
范围查询且扫描行数较多,我们会使用force index强制走特定索引,或者将范围查询拆分为分批循环(例如每次查1000条去更新),避免单次大事务锁住所有数据。"
4. 终极兜底(架构层面)
"对于核心业务表(如订单、库存),我们会采用
读写分离,将查询流量引到从库;对于Update操作,直接通过分布式ID+消息队列异步落库,避免在高峰期产生大事务锁。"
生产方案
- 如果业务能够通过订单号唯一索引或主键定位订单,我会优先使用唯一索引等值条件,让 InnoDB 精确锁定目标记录,减少无关事务之间的锁冲突。
- 对高并发场景中的
UPDATE、DELETE、SELECT FOR UPDATE,必须通过EXPLAIN检查是否命中了合适索引,避免扫描并锁住大量无关记录。 - 不是“没有索引就不允许执行”,而是:没有合适索引的当前读具有很高的并发风险,上线前必须重点检查。
开发准则
- 不是“有索引就行”,而是要命中合适索引
- 唯一索引的等值查询通常锁范围最小
- 普通索引 + 范围查询 还是会出现 锁范围
面试回答
MySQL InnoDB 的并发控制主要靠 MVCC 和锁两套机制。普通 SELECT 通常是快照读,通过 Read View 读取事务可见版本,不加锁,所以不会阻塞其他事务修改。 但是 UPDATE、DELETE、SELECT FOR UPDATE 属于当前读,需要读取最新已提交版本,并且会加锁来保证并发修改的安全。 在 RR 隔离级别下,普通 SELECT 通常复用同一个 Read View,所以同一事务里可能一直看到旧值;但当前读仍然会看到最新版本。 锁的范围和索引访问路径有关,唯一索引等值查询通常只锁一条记录,范围查询或普通索引可能触发 next-key lock,没有合适索引可能导致扫描和锁住更多记录。 如果两个事务更新同一行,后来的事务会锁等待;如果多个事务互相等待对方持有的锁,就会形成死锁。工程上要缩短事务、保证访问顺序一致、让 SQL 命中索引,并在应用层做好幂等和重试。
隔离级别决定可见性,MVCC 负责快照读,锁负责当前读,索引决定锁范围。