1. 操作系统中的常见锁

1.1 互斥锁 & 自旋锁

  1. 基础
    • 互斥锁和自旋锁是最底层的两种锁。
    • 互斥锁加锁失败后,线程会释放 CPU ,给其他线程。
    • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁。
  2. 什么时候使用互斥锁,什么时候使用自旋锁?
    • 互斥锁加锁失败后,会从用户态陷入到内核态切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本,即两次线程上下文切换的成本。如果能够确定能够在短时间内获取到锁,就不应该使用互斥锁,而是使用自选锁。
    • 单核CPU不能使用自选锁,因为自选的线程永远不会放弃CPU。

1.2 读写锁

  • 当写锁没有被线程持有时,多个线程能够并发地持有读锁。
  • 一旦写锁持有后,获取读锁的操作会被阻塞。
    根据实现的不同,读写锁可以分为 读优先锁 和 写优先锁,但是有可能造成读写饥饿。

1.3 悲观锁 & 乐观锁

  • 悲观锁:访问共享资源前,先上锁。
  • 乐观锁:先修改共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
    乐观锁全程不加锁。只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

2. Mysql中的锁

2.1 全局锁

数据库处于只读状态

1
2
flush tables with read lock
unlock tables

2.2 表级锁

2.2.1 表锁

1
2
3
4
5
6
-- 表级共享锁,读锁
lock tables t_student read;
-- 表级独占锁,写锁
lock tables t_stuent write;
-- 解锁
unlock tables

表锁和行锁满足读读共享、读写互斥、写写互斥。

2.2.2 元数据锁

  • 不需要显示使用;
  • 对一张表进行CRUD操作时,加的是MDL读锁;
  • 对一张表做结构变更操作的时候,加的是MDL写锁;
  • 目的是保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

2.2.3 意向锁

(1) 在使用 InnoDB 引擎的表里对某些记录加上 共享锁 之前,需要先在表级别加上一个 意向共享锁。
(2) 在使用 InnoDB 引擎的表里对某些纪录加上 独占锁 之前,需要先在表级别加上一个 意向独占锁。

1
2
3
4
-- 先在表上加上意向共享锁,然后对读取的记录加共享锁
select ... lock in share mode;
-- 先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;

(3) 意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会冲突。
(4) 意向锁只会和共享表锁和独占表锁冲突。
(5) 如果没有意向锁,那么加独占表锁时,需要遍历表里的所有记录判断是否有独占锁;有意向锁之后,在对记录加独占锁前,先会加上表级别的意向独占锁,这样,加独占表锁,直接查该表是否有意向独占锁就可以了。
(6) 意向锁的目的是为了快速判断表里是否有记录被加锁

2.2.4 AUTO-INC锁

(1) AUTO-INC锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是在执行完插入语句后立即释放。
(2) 在插入语句时,会加一个表级别的AUTO-INC锁,一个事务在持有AUTO-INC锁的过程中,其他食物的插入语句都会被阻塞。
(3) MySQL5.1.22开始,提供了一种轻量级的锁来实现自增。Innodb使用innodb_autoinc_lock_mode 的系统变量控制。

2.3 行级锁

InnoDB支持行级锁。普通sql语句是不会对记录加锁的,因为它属于快照读。
如果需要加锁,使用

1
2
3
4
-- 对读取的记录加共享锁
select ... lock in share mode;
-- 对读取的记录加独占锁
select ... for update;

依旧是读读兼容,读写兼容,写写冲突。

2.3.1 Record Lock 记录锁

  • Record Lock 称为记录锁,锁住的是一条记录。
  • 记录锁是有 S 锁和 X 锁之分的。

2.3.2 Gap Lock 间隙锁

  • 只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
  • 间隙锁之间是兼容的。

Q: 可重复读隔离级别下,能够完全解决幻读情况吗?
A: 不可以,如果先快照读,再当前读,中间有其他事务插入满足条件的数据,就有可能产生幻读。

2.3.3 Next-Key Lock 临键锁

  • 锁定一个范围,并且锁定记录本身。

2.3.4 插入意向锁

  • 一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁;如果有的话,插入操作就会发生阻塞,在此期间会生成一个插入意向锁,并设置为等待状态。