MySQL 进阶系列一:锁

46 阅读7分钟

锁是计算机在执行多线程时用于并发访问同一共享资源时的同步机制,MySQL中的锁是在服务器层或者存储引擎层实现的,保证了数据访问的一致性与有效性。

我们都知道MySQL有很多的存储引擎,今天我们来了解最常用的Innodb中的锁机制。

读锁和写锁

Innodb的锁可以分为两类:

  • 共享锁 / 读锁:允许事务读(select)数据
  • 排他锁 / 写锁:允许事务删除(delete)或更新(update)数据

读锁是共享的,或者说是相互不阻塞的。多个事务在同一时刻可以同时读取同一个资源,而互不干扰

写锁是排他的,也就是说一个写锁会阻塞其他的读锁和写锁,这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

读锁写锁互斥

表级锁

表级锁特点:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
表级锁更适合于以查询为主,并发写入少的场景。比如Myisam存储引擎就只支持表级锁。

Innodb如何加表锁

1. 隐式锁定

这种又叫元数据锁,简写为MDL。
对表做结构变更操作的时候,加 MDL 写锁。比如:修改表结构、创建/删除索引等。
对表进行增删改查的时候,加 MDL 读锁。

2. 显示锁定

在执行 SQL 语句时,可以明确显示指定对某个表进行加锁。但在实际工作中一般是不会用到的。

# 加锁
lock tables t1 read,t2 write;
# 解锁
unlock tables;

行级锁

行级锁的特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

Innodb如何加行锁

1. 不加锁

对于普通的select语句,Innodb是不加锁的。(只在串行化隔离级别下,会加读锁)

2. 隐式锁定

InnoDB在事务执行过程中,会根据事务隔离级别在需要的时候自动加锁(关于事务隔离级别,会在下文中讲到)。
锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。

3. 显式锁定

# 读锁
select ... lock in share mode 
# 写锁
select ... for update 

select ... for update:

在执行这个语句的时候,会为对应的索引访问条目加上写锁,锁定对应资源,只能由自己进行修改。

使用场景:一般在事务中为了保证使用到的数据不会被其他事务修改,会用到 for update 子句。比如在订单处理时,可以有效防止同一订单的重复处理。

select ... lock in share mode :

在执行这个语句的时候,会为对应的索引访问条目加上读锁。这样其他的事务只能对这些数据进行查询操作。

使用场景:为了确保自己查询的是最新数据,且不会被其他事务修改,会用到这个语句。但是自己不一定能够修改数据,因为可能有其他事务也对这些数据使用相同方式加上了读锁。

Innodb行锁的实现方式和注意事项

下面划重点了哈,圈起来,要考!

  • Innodb 行锁是通过给索引上的索引项加锁。Innodb 这种行锁实现意味着:只有通过索引条件检索数据,Innodb 才使用行级锁,否则,Innodb 将使用表锁!
  • 不论是使用主键索引、唯一索引或普通索引,Innodb 都会使用行锁来对数据加锁
  • 只有执行计划真正使用了索引,才能使用行锁:即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 来决定的,有可能会索引失效,也有可能 MySQL 会优先使用全表扫描(也就是锁全表)。
  • 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以有可能多个事务是访问不同行的记录,但是如果是使用相同的索引键值,是会出现锁冲突的,比如非唯一索引,可能出现相同的键值。

在实际工作中一定要注意这几点,否则可能导致频繁锁表。

间隙锁

当我们用范围条件对数据加锁时,Innodb会给符合条件的已有数据的索引项加锁。
对于键值在条件范围内但并不存在的记录,叫做“间隙”,Innodb也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁

当键值处于间隙锁范围内时,间隙锁会阻塞条件范围内键值的并发插入,如果是并发插入较多的应用,可能会造成严重的锁等待。

Innodb使用间隙锁的主要目的是为了防止幻读。

在实际工作中,尤其是并发插入比较多的应用,我们要尽量使用相等条件来更新数据,尽量避免间隙锁。

死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。

可能产生死锁的原因

  1. 当事务试图以不同的顺序锁定相同资源时,就可能产生死锁。
  2. 多个事务同时锁定同一个资源时也可能会产生死锁。
  3. 通过SELECT ... LOCK IN SHARE MODE获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。

死锁检测

  1. 数据库系统实现了各种死锁检测和死锁超时的机制。Innodb能检测到死锁的循环依赖并立即返回一个错误。
  2. 在涉及外部锁,或涉及表锁的情况下,Innodb 并不能完全自动检测到死锁,需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决。

死锁恢复

死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,Innodb目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。

InnoDB避免死锁的一些方法

  • 为了在单个表上执行多个并发写入时避免死锁,可以在事务开始时为预期要修改的每行使用SELECT ... FOR UPDATE语句来获取必要的锁,即使这些行的更改语句是在之后执行的。
  • 在事务中,如果要更新记录,应该直接申请足够级别的锁,而不应先申请写锁,在更新时再申请读锁,因为这时候当用户再申请读锁时,其他事务可能已经获得了相同记录的读锁,从而造成锁冲突,甚至死锁。
  • 如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。

如果出现死锁,可以用 SHOW INNODB STATUS 命令查看最后一个死锁产生的详情信息。分析死锁产生的原因和改进措施。

乐观锁和悲观锁

乐观锁

乐观锁, 顾名思义,就是很乐观。假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

在程序上,我们可以采用版本号机制来实现。

在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

悲观锁

悲观锁,顾名思义,就是很悲观。假设一定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。比如之前的行锁,表锁等,都是在做操作之前先上锁。