锁机制
悲观锁
悲观锁(Pessimistic Locking)是一种并发控制机制,它基于”先锁定,后修改”的保守策略。在数据处理过程中,悲观锁会假设并发操作很可能导致冲突,因此会先将数据置于锁定状态,确保在当前事务完成前其他事务无法修改该数据。
从实现方式来看,悲观锁主要分为以下几种类型:
- 行级锁(Row Lock):锁定单行数据,如MySQL的SELECT…FOR UPDATE语句
- 表级锁(Table Lock):锁定整个数据表
- 读锁(Shared Lock):允许多个事务同时读取数据但禁止修改
- 写锁(Exclusive Lock):只允许一个事务读写数据,其他事务完全禁止访问
典型的应用场景包括:
- 银行账户转账操作
- 库存管理系统中的库存扣减
- 机票预订系统中的座位锁定
以MySQL为例,悲观锁的实现方式如下:
BEGIN;
SELECT * FROM accounts WHERE id=1 FOR UPDATE; -- 获取排他锁
UPDATE accounts SET balance=balance-100 WHERE id=1;
COMMIT;
悲观锁的优缺点:
- 优点:能有效避免数据冲突,保证数据一致性
- 缺点:会降低系统并发性能,可能导致死锁
在分布式系统中,悲观锁的实现会更加复杂,通常需要借助Redis、Zookeeper等中间件来实现跨服务的锁管理。
表级锁
表级锁每次操作都会锁住整张表,并发度最低。 手动增加表锁:
lock table 表 read|write, 表2 read|write;
查看表上加过的锁:
show open tables;
删除表锁:
unlock tables;
MySQL表级锁详解
表级读锁(Table Read Lock)
当对表添加读锁时:
- 当前连接:
- 可以执行SELECT查询操作
- 不能执行INSERT、UPDATE、DELETE等写操作,否则会立即报错
- 其他连接:
- 可以并发执行SELECT查询操作
- 尝试执行写操作(INSERT/UPDATE/DELETE)会被阻塞,直到锁被释放
表级写锁(Table Write Lock)
当对表添加写锁时:
- 当前连接:
- 可以执行所有操作(SELECT/INSERT/UPDATE/DELETE)
- 其他连接:
- 所有操作(包括SELECT查询)都会被阻塞
- 阻塞会持续到锁持有者释放锁(执行UNLOCK TABLES)
锁特性对比
| 特性 | 读锁 | 写锁 |
|---|---|---|
| 当前连接读操作 | 允许 | 允许 |
| 当前连接写操作 | 禁止 | 允许 |
| 其他连接读操作 | 允许 | 阻塞 |
| 其他连接写操作 | 阻塞 | 阻塞 |
| 锁冲突 | 允许多个读锁共存 | 与其他所有锁互斥 |
共享锁(行级锁-读锁)
共享锁又称为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。使用共享锁的方法是:SELECT lock in share mode。
排他锁(行级锁-写锁)
排他锁(Exclusive Lock),又称写锁,简称X锁,是一种基本的数据库锁机制。这种锁具有排他性,即当一个事务获取了某个数据行的排他锁后,其他事务既不能对该行记录进行任何修改操作,也不能获取该行的任何类型的锁。
排他锁的特性
- 排他性:排他锁与其他任何锁(包括共享锁和其他排他锁)互斥,不能共存
- 独占访问:持有排他锁的事务可以读取和修改数据
- 阻塞其他操作:其他事务不能修改数据,也不能通过
for update子句获取新的记录锁
使用方式
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
InnoDB引擎的默认行为
- 在
UPDATE和DELETE语句中,InnoDB引擎会自动为操作的行加上排他锁 - 这些操作不需要显式指定
FOR UPDATE子句
实现机制
排他锁的行级锁实现依赖于表上的索引:
- 使用索引的情况:当操作使用索引进行查询时,InnoDB只会锁定符合条件的行记录
- 不使用索引的情况:如果查询没有使用到索引,InnoDB会退化为锁定整个表的所有记录
实际应用场景
库存扣减(防止超卖):
BEGIN;
SELECT quantity FROM products WHERE product_id = 1001 FOR UPDATE;
UPDATE products SET quantity = quantity - 1 WHERE product_id = 1001;
COMMIT;
账户转账:
BEGIN;
SELECT balance FROM accounts WHERE account_id = 12345 FOR UPDATE;
-- 执行转账操作
COMMIT;
注意事项
- 排他锁会降低系统的并发性能,应尽量缩短持有锁的时间
- 避免在事务中执行耗时的操作时持有锁
- 确保查询使用适当的索引,防止意外锁表
- 在事务结束时(提交或回滚)会自动释放所有排他锁
乐观锁
乐观锁是一种并发控制机制,与悲观锁形成鲜明对比。它不是数据库系统内置的功能,而是需要开发者自行设计实现的。这种机制得名于其乐观的假设:系统认为当前事务不会与其他事务发生冲突,因此在数据操作阶段不会采取任何锁机制。
实现原理
- 版本字段机制:乐观锁通常通过在数据表中添加一个version版本号字段来实现
- 工作流程:
- 读取阶段:事务A读取数据时,同时获取该数据的当前版本号
- 修改阶段:事务A准备更新数据时,会先检查当前版本号是否仍为原值
- 提交阶段:如果版本号匹配,则执行更新并将版本号+1;否则说明数据已被修改,操作失败
具体SQL示例:
-- 读取阶段
SELECT id, name, version FROM products WHERE id = 1;
-- 更新阶段(原子操作)
UPDATE products
SET name = '新商品名称', version = version + 1
WHERE id = 1 AND version = 1;
适用场景
乐观锁适用场景:
- 读多写少的应用(如资讯类网站)
- 并发冲突概率较低的系统
- 需要高吞吐量的业务场景
- 示例:电商系统中的商品浏览
悲观锁适用场景:
- 写操作频繁的应用
- 数据一致性要求极高的系统
- 并发冲突概率高的场景
- 示例:银行系统的账户余额修改
性能考量
乐观锁优势:
- 减少锁开销
- 提高系统吞吐量
- 避免死锁问题
悲观锁优势:
- 保证强一致性
- 实现简单直接
- 适合长事务处理
总结对比
| 特性 | 乐观锁 | 悲观锁 |
|---|---|---|
| 实现方式 | 版本号/时间戳 | 数据库内置锁 |
| 加锁时机 | 提交阶段 | 访问阶段 |
| 适用场景 | 读多写少 | 写多读少 |
| 并发性能 | 较高 | 较低 |
| 失败处理 | 重试机制 | - |
MySQL InnoDB 锁机制详解
锁分类
按粒度区分
MySQL中的锁按操作粒度可分为三类:
表级锁(Table-level Locking)
- 每次操作锁住整张表,粒度最粗
- 开销小、加锁快,但并发性能差
- 冲突概率高,易出现阻塞等待
- 主要用于MyISAM、Memory等非事务型存储引擎
行级锁(Row-level Locking)
- 每次操作只锁住需要访问的行记录
- 粒度最小,开销大,但并发性能最优
- 冲突概率最低,不同事务可同时修改不同行
- InnoDB通过索引项加锁实现,默认锁机制
页级锁(Page-level Locking)
- 每次锁定相邻的一组记录(通常4KB)
- 粒度中等,介于表锁和行锁之间
- BDB存储引擎使用此机制
按类型区分
共享锁(S锁/读锁)
- 允许多个事务同时读取同一数据
- 多个事务可同时持有S锁
- 持有S锁期间,其他事务不能获取X锁
排他锁(X锁/写锁)
- 保证数据修改时的独占性
- 同一时间只有一个事务可持有X锁
- 持有X锁期间,其他事务无法获取S锁或X锁
意向锁(IS、IX锁)
意向锁是表级锁,用于提高锁检测效率:
- IS锁(意向共享锁):在添加S锁前先获取表的IS锁
- IX锁(意向排他锁):在添加X锁前先获取表的IX锁
锁兼容矩阵:
| 请求\持有 | 无锁 | IS | IX | S | X |
|---|---|---|---|---|---|
| IS | ✓ | ✓ | ✓ | ✓ | ✗ |
| IX | ✓ | ✓ | ✓ | ✗ | ✗ |
| S | ✓ | ✓ | ✗ | ✓ | ✗ |
| X | ✓ | ✗ | ✗ | ✗ | ✗ |
乐观锁与悲观锁
乐观锁(Optimistic Locking)
- 假设并发冲突发生概率较低
- 不加锁直接操作,提交时检测冲突
- 常见实现:版本号机制、时间戳机制
示例:
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 5
适用于读多写少、冲突率低的场景。
悲观锁(Pessimistic Locking)
- 假设并发冲突必然发生
- 执行操作前先获取锁
- 包括共享锁(S锁)和排他锁(X锁)
示例:
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
适用于写操作频繁、冲突概率高的场景。
行锁原理
InnoDB行锁通过索引数据页上的记录加锁实现,主要有三种算法:
1. Record Lock(记录锁)
- 锁定单个行记录,仅锁住索引记录本身
- 支持RC和RR隔离级别
- 使用唯一索引精确查询时使用
- 示例:锁住id=1这一行记录
2. Gap Lock(间隙锁)
- 锁定索引记录之间的间隙,防止插入新记录
- 仅支持RR隔离级别
- 防止幻读问题
- 示例:表中id为1,3,5,锁定(1,3)和(3,5)间隙
3. Next-Key Lock(临键锁)
- Record Lock和Gap Lock的组合
- 锁住记录本身和记录之前的间隙
- InnoDB默认行锁算法
- 仅支持RR隔离级别
- 示例:锁定(1,3]区间
注意:在RR隔离级别,InnoDB对记录加锁先采用Next-Key Lock,但使用唯一索引时会降级为RecordLock。
不同SQL语句的锁行为
SELECT(普通查询)
- 使用MVCC机制实现非阻塞读
- 不施加任何锁,通过读取事务开始时的快照数据
SELECT … LOCK IN SHARE MODE
- 追加共享锁(S锁)
- 使用Next-Key Lock处理
- 唯一索引时降级为RecordLock
SELECT … FOR UPDATE
- 追加排他锁(X锁)
- 默认使用Next-Key Lock
- 唯一索引时降级为RecordLock
UPDATE … WHERE
- 使用NextKey Lock对符合条件记录加锁
- 唯一索引时降级为RecordLock
DELETE … WHERE
- 与UPDATE类似
- 同时锁定主键索引和所有二级索引
INSERT
- 对新行设置排他RecordLock
- 唯一键冲突时会短暂获取共享锁检查冲突
不同索引的加锁示例(RR隔离级别)
主键加锁:仅在主键索引记录上加X锁
唯一键加锁:在唯一索引上加X锁,然后在主键索引记录上加X锁
非唯一键加锁:
- 对满足条件的记录和主键分别加X锁
- 在范围内加入Gap Lock
无索引加锁:表里所有行和间隙都会加X锁,导致全表锁定
Gap Lock(间隙锁)
- 锁定索引记录之间的间隙
- 仅支持RR隔离级别
- 防止幻读
Next-Key Lock(临键锁)
- Record Lock 和 Gap Lock 的组合
- 既锁住记录本身,也锁住记录之前的间隙
- InnoDB默认的行锁算法
实际应用示例
银行转账场景
BEGIN;
SELECT balance FROM accounts WHERE id=1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id=1;
COMMIT;
SELECT语句加锁
-- 共享锁
SELECT * FROM table WHERE id=1 LOCK IN SHARE MODE;
-- 排他锁
SELECT * FROM table WHERE id=1 FOR UPDATE;
不同索引的加锁行为
- 主键加锁:仅在主键索引记录上加X锁
- 唯一键加锁:在唯一索引和主键分别加X锁
- 非唯一键加锁:加X锁和Gap Lock
- 无索引加锁:全表锁定