锁机制

悲观锁

悲观锁(Pessimistic Locking)是一种并发控制机制,它基于”先锁定,后修改”的保守策略。在数据处理过程中,悲观锁会假设并发操作很可能导致冲突,因此会先将数据置于锁定状态,确保在当前事务完成前其他事务无法修改该数据。

从实现方式来看,悲观锁主要分为以下几种类型:

  1. 行级锁(Row Lock):锁定单行数据,如MySQL的SELECT…FOR UPDATE语句
  2. 表级锁(Table Lock):锁定整个数据表
  3. 读锁(Shared Lock):允许多个事务同时读取数据但禁止修改
  4. 写锁(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 tableread|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锁,是一种基本的数据库锁机制。这种锁具有排他性,即当一个事务获取了某个数据行的排他锁后,其他事务既不能对该行记录进行任何修改操作,也不能获取该行的任何类型的锁。

排他锁的特性

  1. 排他性:排他锁与其他任何锁(包括共享锁和其他排他锁)互斥,不能共存
  2. 独占访问:持有排他锁的事务可以读取和修改数据
  3. 阻塞其他操作:其他事务不能修改数据,也不能通过for update子句获取新的记录锁

使用方式

SELECT * FROM employees WHERE id = 1 FOR UPDATE;

InnoDB引擎的默认行为

  • UPDATEDELETE语句中,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;

注意事项

  1. 排他锁会降低系统的并发性能,应尽量缩短持有锁的时间
  2. 避免在事务中执行耗时的操作时持有锁
  3. 确保查询使用适当的索引,防止意外锁表
  4. 在事务结束时(提交或回滚)会自动释放所有排他锁

乐观锁

乐观锁是一种并发控制机制,与悲观锁形成鲜明对比。它不是数据库系统内置的功能,而是需要开发者自行设计实现的。这种机制得名于其乐观的假设:系统认为当前事务不会与其他事务发生冲突,因此在数据操作阶段不会采取任何锁机制。

实现原理

  1. 版本字段机制:乐观锁通常通过在数据表中添加一个version版本号字段来实现
  2. 工作流程
  • 读取阶段:事务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锁

锁兼容矩阵

请求\持有无锁ISIXSX
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;

不同索引的加锁行为

  1. 主键加锁:仅在主键索引记录上加X锁
  2. 唯一键加锁:在唯一索引和主键分别加X锁
  3. 非唯一键加锁:加X锁和Gap Lock
  4. 无索引加锁:全表锁定