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;
悲观锁补充: 悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
- 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 其间如果有其他事务对该记录做加锁的操作,都要等待当前事务解锁或直接抛出异常。
处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
问:如何处理死锁
- 策略一:直接进入等待,直到超时。这个超时时间可以通过参数 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释放锁。