概述
带着问题来学习 1、mysql 中的锁分类有那些? 2、在一个事务中 锁是自动开启的么? 3、read view 是干什么用的,当前读和快照读有什么区别? 4、执行普通的 sql 语句 自动触发的锁有哪些 ,需要手动触发的有哪些?
事务
事务的特性(ACID)
1、A --- Atomicity 原子性
核心:事务是最小单元,不可拆分
- 事务里所有 SQL,全部成功提交,或者全部回滚,不会出现执行一半的情况。
- 实现:
undo log回滚日志,失败时根据 undo log 恢复数据。
2、C --- Consistency 一致性
核心:事务执行前后,数据的业务规则状态保持合法
- 数据库从一个合法状态,变成另一个合法状态,不会出现非法数据。
- 原子性、隔离性、持久性最终都是为了保证一致性。
3、I --- Isolation 隔离性
核心:多个并发事务之间互相隔离,互不干扰 并发事务如果不隔离,会产生三类问题:脏读、不可重复读、幻读。
4、D --- Durability 持久性
核心:事务一旦提交,修改永久写入磁盘,崩溃也不会丢失
- 提交成功后,数据持久化保存。
- 实现:
redo log重做日志,崩溃恢复时重放 redo log。
事务的隔离级别
MySQL 4 种隔离级别(从低到高):
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 READ UNCOMMITTED | ✅存在 | ✅存在 | ✅存在 |
| 读已提交 READ COMMITTED (RC) | ❌不存在 | ✅存在 | ✅存在 |
| 可重复读 REPEATABLE READ (RR)【MySQL InnoDB 默认】 | ❌不存在 | ❌不存在 | ✅存在(InnoDB 通过 MVCC + 间隙锁解决幻读) |
| 串行化 SERIALIZABLE | ❌不存在 | ❌不存在 | ❌不存在 |
三个并发问题解释
- 脏读 :读到其他事务未提交的数据,如果对方回滚,读到的数据就是脏数据。
- 不可重复读 :同一个事务内,两次读取同一行数据,结果不一样 ,因为别的事务修改并提交了这行。(针对行更新)
- 幻读 :同一个事务内,相同查询条件,前后返回行数不一致,其他事务插入 / 删除了满足条件的数据。(针对新增 / 删除行)
InnoDB RR 级别:通过 MVCC 解决快照读的幻读;配合间隙锁 + 临键锁解决当前读的幻读。
锁
在并发访问数据库时,对共享资源(行、表等)做访问控制,保障事务的隔离性,防止并发问题(脏写、脏读、不可重复读、幻读),同时尽可能平衡并发性能。
MVCC(快照读)可以做到不加行锁,解决脏读、不可重复读;但MVCC 只能解决读的问题,写操作无法绕开锁。写操作必须使用锁来防止脏写。
锁的分类
1. 按锁粒度划分
- 全局锁 :对整个数据库实例加锁。
FLUSH TABLES WITH READ LOCK(FTWRL),所有表只读,一般用于全库备份。
- 表级锁:锁定整张表
1、表共享读锁(S 锁):LOCK TABLES ... READ,多个事务可同时读,不能写。
2、表排他写锁(X 锁):LOCK TABLES ... WRITE,只有持有锁的事务读写,其他事务阻塞读写。
注意:
LOCK TABLES是 Server 层锁,不是 InnoDB 事务锁,会绕过事务隔离机制,生产尽量不用。
- 行级锁(InnoDB 核心):只锁定索引记录,InnoDB 行锁是基于索引实现,如果不走索引会退化成表锁。
1、记录锁(Record Lock):锁定索引中的某一行记录。
2、间隙锁(Gap Lock):锁定索引记录之间的间隙,防止幻读,仅在可重复读 RR隔离级别生效。
3、临键锁(Next-Key Lock):记录锁 + 间隙锁,RR 默认行锁算法,左开右闭区间。
4、插入意向锁(Insert Intention Lock):间隙锁的一种,插入时判断间隙是否被占用,提升并发插入性能。
2. 按锁兼容性划分
- 共享锁 S(读锁) :事务加 S 锁后,可以读该行;其他事务可以加 S 锁,但不能加 X 锁。
- 排他锁 X(写锁) :事务加 X 锁后,可以读写该行;其他事务不能加 S/X 锁。
3.意向锁(表级别,InnoDB 自动维护)
意向锁是表锁,用来快速判断表上是否存在行锁,避免遍历所有行判断锁冲突。
- 意向共享锁 IS:事务给某行加 S 锁前,先在表上加 IS 锁。
- 意向排他锁 IX:事务给某行加 X 锁前,先在表上加 IX 锁。
意向锁之间互相兼容;意向锁和普通 S/X 表锁互斥。
ReadView、当前读 & 快照读
ReadView 是什么?
ReadView(读视图)是 InnoDB 在快照读时用来做 MVCC 多版本并发控制的核心数据结构。 它保存了执行快照读瞬间的活跃事务列表,包含 4 个核心字段:
m_ids:当前系统中,所有未提交的活跃事务 ID 集合min_trx_id:m_ids最小事务 IDmax_trx_id:下一个将要分配的事务 IDcreator_trx_id:生成这个 ReadView 的当前事务 ID
RC 和 RR 隔离级别 ReadView 创建时机差异:
RC(读已提交):每次快照读都会生成新 ReadView,可以读到其他事务已提交数据。
RR(可重复读):事务内第一次快照读时生成 ReadView,整个事务复用这个 ReadView,保证可重复读,避免不可重复读。
快照读(Snapshot Read)
读取的是 Undo 日志里的历史版本数据,不加行锁 ,并发友好。普通 SELECT 属于快照读。
sql
-- 快照读,不加行锁,基于MVCC ReadView读取历史版本
SELECT * FROM user WHERE id = 1;
当前读(Current Read)
读取数据库最新版本数据,读取时必须加锁(S/X 锁),保证读到最新数据。 以下语句都是当前读:
SELECT ... LOCK IN SHARE MODE(加 S 锁)SELECT ... FOR UPDATE(加 X 锁)UPDATE / DELETE / INSERT
sql
-- 当前读,加共享S锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 当前读,加排他X锁
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- UPDATE也是当前读,自动加X锁
UPDATE user SET name = 'new' WHERE id = 1;
| 类型 | 是否加锁 | 读取数据 | 底层 |
|---|---|---|---|
| 快照读 | 不加行锁 | 历史版本 | MVCC ReadView + Undo log |
| 当前读 | 加 S/X 行锁 | 最新版本 | 行锁机制 |
RR 级别下,快照读解决不可重复读;临键锁解决幻读。
锁的触发
锁的触发的前提条件:InnoDB,事务内执行;行锁依赖索引,无索引会退化表锁。
1、自动触发锁(不需要额外关键字,语句执行自动加锁)
INSERT:插入记录,加记录锁;间隙会加插入意向锁。UPDATE:当前读,命中索引行加X 排他行锁;RR 下会附带间隙锁 / 临键锁。DELETE:当前读,命中索引行加X 排他行锁;RR 下会附带间隙锁 / 临键锁。
sql
BEGIN;
-- INSERT 自动加记录锁
INSERT INTO user(id,name) VALUES(10,'张三');
-- UPDATE 当前读,自动加X行锁
UPDATE user SET name='李四' WHERE id=10;
-- DELETE 当前读,自动加X行锁
DELETE FROM user WHERE id=10;
COMMIT;
普通
SELECT(快照读)不加任何行锁。
2、手动显式触发锁(需要写特定 SQL 关键字)
SELECT ... LOCK IN SHARE MODE:手动加 S 共享行锁,其他事务可读,写阻塞。SELECT ... FOR UPDATE:手动加 X 排他行锁,其他事务读写都阻塞。LOCK TABLES ... READ / WRITE:MySQL Server 层表锁,不是 InnoDB 事务行锁,生产慎用。
sql
BEGIN;
-- 手动加S共享锁,其他事务可以读,不能修改id=1
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 手动加X排他锁,其他事务读写id=1都会阻塞
SELECT * FROM user WHERE id = 1 FOR UPDATE;
COMMIT;
锁的使用
原则:能不用显式锁就不用;优先 MVCC 快照读;只有业务需要 "当前读,保证读到最新数据,防止并发修改" 时才使用锁
1、需要强一致性的业务场景(悲观锁场景)
业务上需要:查询之后,在当前事务内,这条记录不允许被其他事务修改。
典型场景:库存扣减、订单状态变更、分布式本地事务资源锁定。 使用:SELECT ... FOR UPDATE(X 排他锁)
sql
BEGIN;
-- 锁定库存记录,防止其他事务同时扣减
SELECT stock_num FROM goods_stock WHERE id = 1 FOR UPDATE;
-- 判断库存
UPDATE goods_stock SET stock_num = stock_num -1 WHERE id =1;
COMMIT;
注意:
FOR UPDATE一定要在事务内执行;提交才释放锁。
2.乐观锁方案(替代悲观锁,减少锁冲突)
不使用数据库锁,业务层通过 version 版本号控制,适合高并发、读多写少场景
ini
-- 更新时带上版本号,版本不匹配更新行数为0,代表已经被别人修改
UPDATE goods_stock
SET stock_num = stock_num -1, version = version +1
WHERE id =1 AND version = 1;
3. 全库逻辑备份场景
需要保证备份期间全库一致性,防止 DDL/DML 破坏快照。 使用:FLUSH TABLES WITH READ LOCK;(全局读锁)
现在一般用 mysqldump --single-transaction(RR+MVCC,不需要全局锁),FTWRL 只在不支持事务的 MyISAM 场景使用。
锁优化方案
锁优化核心思路一句话:尽可能缩小锁范围、缩短锁持有时间,降低锁冲突概率
1、保证 SQL 命中索引,避免行锁退化为全表扫描锁
InnoDB 行锁基于索引。UPDATE / DELETE 如果 WHERE 条件不走索引 / 索引失效,会全聚簇索引扫描,所有扫描行加 X 锁,锁范围爆炸。
❌ 坏示例(name 无索引)
sql
BEGIN;
UPDATE user SET age=20 WHERE name='test'; -- 全表扫描,所有行加X锁
COMMIT;
2、减少事务持有锁的时间:避免长事务
锁在事务提交 / 回滚前一直持有。事务越长,锁持有时间越长,锁冲突概率越高。 优化手段:
- 事务内只放必要 DML,不要在事务中包含业务耗时逻辑(HTTP 调用、文件 IO、复杂计算);
- 事务尽量小而快,尽早 commit;
- 监控长事务,杀掉长时间未提交事务。
3、合理选择锁策略:乐观锁优先于悲观锁
- 冲突概率低:乐观锁(版本号),无数据库锁开销;
- 冲突概率很高:悲观锁
FOR UPDATE,但要控制粒度;
乐观锁缺点:并发极高时大量更新失败,需要业务重试。
4. 减少锁粒度,锁定尽可能少的数据
- 尽量精确 WHERE 条件,只锁定需要修改的行;
- 避免大范围
UPDATE ... WHERE id > 100,RR 下会加临键锁,锁住一大片索引区间,容易幻读 + 大范围锁等待。
5. 调整业务逻辑,避免锁等待与死锁
死锁成因:多个事务,以相反顺序锁定资源。 优化:统一资源加锁顺序,所有事务都按相同顺序更新表 / 行。