MySQL 锁机制详解+示例
MySQL 锁机制是保证数据库并发控制的核心手段,用于解决多用户同时操作数据库时可能出现的脏读、不可重复读、幻读等问题。合理使用锁可以平衡数据库的并发性能与数据一致性。
一、锁的基本概念与分类
MySQL 锁根据不同维度可分为多种类型:
- 按锁的粒度划分:行级锁、表级锁、页级锁
- 按锁的模式划分:共享锁(S 锁)、排他锁(X 锁)
- 按锁的使用方式划分:自动锁、显式锁
- 特殊锁类型:意向锁、间隙锁、临键锁、记录锁等
不同的存储引擎支持的锁机制不同,其中 InnoDB 是 MySQL 中唯一支持行级锁的存储引擎,也是目前最常用的存储引擎。
二、表级锁
表级锁是粒度最大的一种锁,对整张表加锁,实现简单,开销小,加锁快,但并发度低。
1. 表级锁的类型
(1)表共享读锁(Table Read Lock)
- 多个会话可以同时获取表的读锁
- 持有读锁的会话只能读表,不能写表
- 其他会话不能获取写锁,直到所有读锁释放
(2)表独占写锁(Table Write Lock)
- 只有一个会话可以获取表的写锁
- 持有写锁的会话可以读写表
- 其他会话不能获取读锁和写锁,直到写锁释放
2. 表级锁示例
使用 LOCK TABLES
命令手动获取表级锁:
-- 会话1:获取表的读锁
LOCK TABLES users READ;-- 可以读取数据
SELECT * FROM users WHERE id = 1;-- 不能写入数据,会报错
UPDATE users SET name = 'test' WHERE id = 1;-- 会话2:尝试获取写锁会被阻塞,直到会话1释放锁
LOCK TABLES users WRITE;-- 释放锁
UNLOCK TABLES;
3. 表级锁的适用场景
表级锁适合以下场景:
- 全表数据迁移或批量更新
- 读写比例严重失衡,读远多于写的场景
- MyISAM 存储引擎(只支持表级锁)
三、行级锁
行级锁是粒度最小的锁,只对指定的记录加锁,并发度高,但实现复杂,开销大,加锁慢。InnoDB 支持行级锁,这也是其广泛应用的重要原因。
1. 行级锁的类型
(1)共享锁(S 锁,Shared Locks)
- 允许事务读取一行数据
- 多个事务可以同时对同一行加 S 锁
- 加了 S 锁的行,不能被加 X 锁,直到所有 S 锁释放
(2)排他锁(X 锁,Exclusive Locks)
- 允许事务更新或删除一行数据
- 同一行只能有一个 X 锁
- 加了 X 锁的行,不能被加 S 锁或 X 锁,直到 X 锁释放
2. 行级锁示例
InnoDB 默认在 SELECT ... FOR UPDATE
时加排他锁
在 SELECT ... LOCK IN SHARE MODE
时加共享锁:
共享锁示例:
-- 会话1:获取行的共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;-- 会话2:可以获取同一行的共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;-- 会话2:尝试更新会被阻塞,因为会话1持有S锁
UPDATE users SET name = 'test' WHERE id = 1;-- 会话1:释放锁(提交事务)
COMMIT;-- 会话2:此时更新操作会执行
排他锁示例:
-- 会话1:获取行的排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;-- 会话2:尝试获取共享锁会被阻塞
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;-- 会话2:尝试获取排他锁会被阻塞
SELECT * FROM users WHERE id = 1 FOR UPDATE;-- 会话1:释放锁
COMMIT;-- 会话2:此时查询会执行
四、意向锁
意向锁(Intention Locks)是表级锁,用于指示事务稍后会对表中的行加哪种类型的锁(共享锁或排他锁),是 InnoDB 为了支持多粒度锁而引入的。
1. 意向锁的类型
- 意向共享锁(IS 锁):事务意图在表中的某些行上加共享锁
- 意向排他锁(IX 锁):事务意图在表中的某些行上加排他锁
2. 意向锁的兼容性
锁类型 | 共享锁 (S) | 排他锁 (X) | 意向共享锁 (IS) | 意向排他锁 (IX) |
---|---|---|---|---|
共享锁 (S) | 兼容 | 冲突 | 兼容 | 冲突 |
排他锁 (X) | 冲突 | 冲突 | 冲突 | 冲突 |
意向共享锁 (IS) | 兼容 | 冲突 | 兼容 | 兼容 |
意向排他锁 (IX) | 冲突 | 冲突 | 兼容 | 兼容 |
3. 意向锁示例
-- 当执行以下语句时,InnoDB会自动先加意向共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;-- 当执行以下语句时,InnoDB会自动先加意向排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
五、记录锁、间隙锁与临键锁
这三种锁是 InnoDB 在处理行级锁时使用的具体锁类型,尤其在使用索引时发挥作用。
1. 记录锁(Record Locks)
记录锁是对索引记录加的锁,锁定单行记录。
-- 对id=1的记录加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
2. 间隙锁(Gap Locks)
间隙锁锁定索引记录之间的间隙,或索引记录之前 / 之后的间隙,防止其他事务在间隙中插入数据,用于解决幻读问题。
-- users表中存在id=1,3,5的记录
-- 会话1:锁定id在1到5之间的间隙
SELECT * FROM users WHERE id BETWEEN 1 AND 5 FOR UPDATE;-- 会话2:尝试在间隙中插入数据会被阻塞
INSERT INTO users (id, name) VALUES (2, 'test');
INSERT INTO users (id, name) VALUES (4, 'test');
3. 临键锁(Next-Key Locks)
临键锁是记录锁和间隙锁的组合,锁定索引记录及其前面的间隙,是 InnoDB 默认的行级锁类型(当使用范围条件而非相等条件检索数据时)。
-- users表中存在id=1,3,5的记录
-- 会话1:使用临键锁
SELECT * FROM users WHERE id > 2 AND id < 5 FOR UPDATE;-- 这会锁定id=3的记录,以及(2,3)和(3,5)的间隙
-- 会话2:以下操作会被阻塞
INSERT INTO users (id, name) VALUES (2, 'test'); -- 插入到(2,3)间隙
INSERT INTO users (id, name) VALUES (4, 'test'); -- 插入到(3,5)间隙
UPDATE users SET name = 'test' WHERE id = 3; -- 更新id=3的记录
六、自增锁
自增锁(Auto-inc Locks)是一种特殊的表级锁,用于事务插入带有自增列(AUTO_INCREMENT)的表时使用。
自增锁示例
-- 创建带有自增列的表
CREATE TABLE test (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(10)
);-- 会话1:获取自增锁
INSERT INTO test (name) VALUES ('a');-- 会话2:插入会被阻塞,直到会话1释放自增锁
INSERT INTO test (name) VALUES ('b');-- 会话1:提交事务,释放自增锁
COMMIT;
在 MySQL 8.0 中,默认使用 innodb_autoinc_lock_mode = 2
(连续模式),优化了自增锁的性能,对于简单插入语句不会使用表级自增锁,而是通过轻量级锁实现。
七、元数据锁
元数据锁(MDL,Metadata Locks)用于保护表的元数据信息,在事务中访问表时自动获取,确保读写一致性。
MDL 锁示例
-- 会话1:开始事务,访问表,获取MDL读锁
START TRANSACTION;
SELECT * FROM users LIMIT 1;-- 会话2:尝试修改表结构会被阻塞,因为会话1持有MDL读锁
ALTER TABLE users ADD COLUMN age INT;-- 会话1:提交事务,释放MDL读锁
COMMIT;-- 会话2:此时ALTER操作会执行
长时间运行的事务会持有 MDL 锁,可能导致表结构修改操作阻塞,在实际应用中应避免长事务。
八、数据库锁的常用实战场景
1. 秒杀 / 高并发库存扣减
场景:商品秒杀时,多个用户同时抢购同一商品,需确保库存不超卖。
锁策略:
- 使用行级锁(如 MySQL 的
FOR UPDATE
)锁定特定商品的库存记录 - 示例:
BEGIN; -- 锁定id=1001的商品记录 SELECT stock FROM products WHERE id=1001 FOR UPDATE; -- 检查库存并扣减 UPDATE products SET stock=stock-1 WHERE id=1001 AND stock>0; COMMIT;
- 避免使用表锁,防止并发性能过低
2. 订单状态更新
场景:订单可能被多个进程同时操作(支付、取消、超时等),需保证状态转换的一致性。
锁策略:
- 采用悲观锁确保状态更新的原子性
- 或使用乐观锁(版本号机制)实现无锁并发控制:
-- 乐观锁实现:只有版本号匹配时才更新 UPDATE orders SET status='paid', version=version+1 WHERE order_id='O12345' AND version=3;
3. 分布式事务数据一致性
场景:跨库事务中(如订单创建同时扣减库存),需保证多库操作的一致性。
锁策略:
- 使用分布式锁(如基于 Redis 或 ZooKeeper 实现)
- 结合本地数据库锁,形成 "分布式锁 + 本地锁" 的双层锁机制
- 示例:用 Redis 的
SETNX
获取分布式锁,再操作本地数据库
4. 数据迁移 / 批量更新
场景:后台任务批量更新大量数据时,需避免与正常业务操作冲突。
锁策略:
- 使用表级锁(如 MySQL 的
LOCK TABLES
)或分区锁 - 对数据分片处理,使用范围锁减少锁定范围:
-- 锁定id在1000-2000范围的记录 SELECT * FROM user_data WHERE id BETWEEN 1000 AND 2000 FOR UPDATE;
5. 防止重复提交
场景:用户重复点击提交按钮,导致重复创建数据(如重复订单)。
锁策略:
- 基于唯一索引的隐式锁:在唯一字段上创建唯一索引
- 结合分布式锁,以业务唯一标识作为锁键:
// 伪代码:使用订单号作为锁键 if (redisLock.tryLock(orderNo, 3000)) {try {// 检查订单是否已存在,不存在则创建} finally {redisLock.unlock();} }
6. 热点数据保护
场景:热门商品、首页推荐等高频访问的数据,避免并发更新导致的数据不一致。
锁策略:
- 采用读写锁(如 PostgreSQL 的
SELECT ... FOR SHARE
) - 读多写少场景:读操作加共享锁,写操作加排他锁
- 示例:
-- 读操作加共享锁 SELECT * FROM hot_products WHERE id=5 FOR SHARE;-- 写操作加排他锁 SELECT * FROM hot_products WHERE id=5 FOR UPDATE;
7. 定时任务并发控制
场景:多个服务器节点的定时任务同时执行,导致重复处理。
锁策略:
- 使用分布式锁控制任务执行权
- 结合过期时间防止锁永久占用:
// 伪代码:定时任务获取锁 if (distributedLock.tryLock("daily_task", 60000)) {try {// 执行定时任务} finally {distributedLock.unlock();} }
九、锁的优化建议
- 尽量使用行级锁:减少锁冲突,提高并发性能
- 合理设计索引:确保锁能精确到所需的行,避免因无索引导致的表锁
- 控制事务大小:缩短事务持有锁的时间
- 避免死锁:
- 以相同顺序访问表和行
- 尽量使用较低的隔离级别
- 设置合理的锁等待超时时间(
innodb_lock_wait_timeout
)
- 使用
FOR UPDATE SKIP LOCKED
:在 MySQL 8.0 中,可以跳过被锁定的行,避免等待
九、总结
MySQL 提供了丰富的锁机制,从表级锁到行级锁,从共享锁到排他锁,每种锁都有其适用场景。理解并正确使用这些锁机制,是保证数据库高并发和数据一致性的关键。
在实际应用中,应根据业务场景选择合适的锁类型和隔离级别,同时通过合理的索引设计和事务管理来优化锁的使用,避免不必要的锁冲突和性能问题。特别是 InnoDB 存储引擎的行级锁机制,为高并发场景提供了强大的支持,是现代 MySQL 应用的首选。