数据库事务之锁类型

数据库事务之锁类型

Posted by Ted on November 12, 2022

img

1、乐观锁和悲观锁

乐观锁和悲观锁是按照对并发冲突的处理方式进行分类的。

  • 乐观锁:乐观锁实际上就是没锁,认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新
  • 悲观锁:悲观锁假设并发冲突的概率较高,因此在读取数据时会立即加锁,阻止其他事务对数据进行读取或修改。悲观锁通过共享锁或排他锁来实现对数据的锁定。
    • 共享锁允许多个事务同时读取数据,但不允许修改;
    • 排他锁则只允许一个事务对数据进行读取或修改。悲观锁适用于并发修改较多、冲突较多的场景。

所以二者区别是:

  • 乐观锁在读取时不加锁,而是在提交时检查冲突。
  • 悲观锁在读取时立即加锁,以防止其他事务对数据进行修改。

举例

有商品表item,需要对库存quantity进行更新。

//乐观锁,整个过程中没有加锁
//查询出商品库存信息,quantity = 3
select quantity from item where id=1
//修改商品库存为2,在提交修改时,进行校验和冲突处理,整个过程中没有加锁
update item set quantity=2 where id=1 and quantity = 3;

或者,借助一个Version字段
//查询出商品信息,version = 2
select version from item where id=1
//修改商品库存为2,并且将version+1
update item set quantity=2,version = 3 where id=1 and version = 2;
// 悲观锁
// 则我们在读取时立即加锁锁定,然后再进行修改
//0.开始事务
begin; 
//1.读取加锁,这里加的是排它锁
select quantity from item where id=1 for update;
//2.修改库存
update item set quantity=2 where id = 1;
//3.提交事务
commit;

悲观锁补充: 悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  4. 其间如果有其他事务对该记录做加锁的操作,都要等待当前事务解锁或直接抛出异常。

处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

问:如何处理死锁

  • 策略一:直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置;默认为 50s,即如果不开启死锁检测,则在发生死锁之后,会等待 50s 后回滚事务释放锁。
  • 策略二:发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。innodb 默认开启死锁检测,但是死锁检测会消耗大量的CPU资源。

2、共享锁和排它锁

  • 共享锁(Shared Lock),又称之为读锁,简称S锁
    • 当事务对数据加上读锁后,其他事务只能对该数据加读锁,不能做任何修改操作,也就是不能添加写锁。共享锁主要是为了支持并发的读取数据而出现的,读取数据时,不允许其他事务对当前数据进行修改操作,从而避免”不可重读”的问题的出现。
  • 排它锁(Exclusive Lock),又称之为写锁,简称X锁
    • 当事务对数据加上写锁后,其他事务既不能对该数据添加读写,也不能对该数据添加写锁,写锁与其他锁都是互斥的。只有当前数据写锁被释放后,其他事务才能对其添加写锁或者是读锁。写锁主要是为了解决在修改数据时,不允许其他事务对当前数据进行修改和读取操作,达到了串行处理。

区别:

共享锁,多个事务共享读取,保证了本事务可以重复读。

排它锁,就是串行处理,单个事务占有,其他事务不能读写。

属性锁 共享锁(S) 排它锁(X)
共享锁(S) 允许 不允许
排它锁(X) 不允许 不允许

注:

  • 普通的 SELECT 语句在默认情况下不会获取共享锁。
    • 普通的 SELECT 语句使用的是快照读,共享读锁(Shared Read Lock),而不是共享锁(Shared Lock)。共享读锁是一种特殊的共享锁,它允许多个事务同时读取相同的数据,只为本事务所用,但不会阻塞其他事务的共享读锁或排他写锁。
  • 加共享锁 lock in share mode ,加排他锁 for update
select * from my_table where id = 1 lock in share mode; // 共享锁
select * from my_table where id = 1 for update; // 排它锁

3、表级锁和行级锁

  • 表级锁(Table-level Lock):对整个表进行锁定,阻止其他事务对表的读取或修改。
  • 行级锁(Row-level Lock):对表中的单个行记录进行锁定,允许其他事务对其他行进行读取或修改。

举例

// 表级锁
LOCK TABLES table_name READ; // 读取锁(共享锁)
LOCK TABLES table_name WRITE; // 写入表锁(排它锁)
UNLOCK TABLES;

// 行级锁
SELECT * FROM table_name WHERE condition FOR UPDATE;

需要注意的是,表级锁在 MySQL 中一般用于特定的场景,如备份、导入导出数据等操作,而不是常规的并发控制手段。在大多数情况下,MySQL 更常用的是行级锁来实现并发控制。表级锁的使用需要谨慎,因为它会对整个表进行锁定,可能会导致其他事务的阻塞和性能问题

4、行级锁

按照锁的粒度和范围分类:

  • 记录锁(Record Lock):对数据库中的单个记录(不是单行,可以多行)进行锁定,以防止其他事务对该记录进行修改
  • 间隙锁(Gap Lock):对索引范围内的间隙进行锁定,以防止其他事务在该范围内插入新记录。临键锁还锁定了记录之间的间隙,以防止其他事务在这些间隙中插入新记录。这样可以保证在索引范围内的间隙中不会出现新的记录,从而维护了数据的完整性
  • 临键锁(Next-Key Lock):结合了记录锁和间隙锁的特性,既锁定了记录,也锁定了记录之间的间隙。

记录锁和间隙锁的区别:

记录锁只影响其他事务对同一记录的修改操作,而间隙锁影响其他事务对索引范围内的插入操作

怎么确定是什么锁

加锁的基本单位为next-key lock

先定位到next-key lock,然后再看怎么退化,缩小范围

举例-记录锁

事务1:

BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 对记录进行修改操作
COMMIT;

事务2:

BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 尝试对同一记录进行修改操作,但会被阻塞等待事务1释放锁
COMMIT;

在事务1中,通过 FOR UPDATE 语句获取了对 id 为 1 的记录的排它锁。这意味着其他事务无法同时对该记录进行修改,事务2在尝试获取同一记录的锁时会被阻塞,直到事务1释放锁。

// 多行
BEGIN;
SELECT * FROM users WHERE age > 30 FOR UPDATE;
-- 对满足条件的记录进行修改操作
COMMIT;

间隙锁举例

事务1:

BEGIN;
SELECT * FROM users WHERE age > 30 AND age < 40 FOR UPDATE;
-- 对满足条件的记录进行修改操作
COMMIT;

事务2:

BEGIN;
INSERT INTO users (name, age) VALUES ('John', 35);
-- 尝试在事务1锁定的范围内插入新记录,但会被阻塞等待事务1释放锁
COMMIT;

在事务1中,通过 FOR UPDATE 语句获取了满足条件 age > 30 AND age < 40 的记录范围的间隙锁。这意味着其他事务无法在该范围内插入新记录,事务2在尝试插入新记录时会被阻塞,直到事务1释放锁。