小艾的自留地

Stay foolish, Stay hungry

这篇文章来浅谈一下什么是Mysql 行锁,以及产生行锁的原因。

锁的分类

MySQL有三种锁的级别:页级、表级、行级。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

因为这篇笔记只介绍Mysql 行锁,所以这里不对其他类型的锁做介绍了。

行锁

InnoDB实现了两种类型的行锁:

  • 共享锁【S锁】又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
  • 排他锁【X锁】又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

所谓X锁,是事务T对数据A加上X锁时,只允许事务T读取和修改数据A; 所谓S锁,是事务T对数据A加上S锁时,其他事务只能再对数据A加S锁,而不能加X锁,直到T释放A上的S锁

场景重现

  1. 首先创建一个 InnoDB类型的数据表,SQL 如下:
1
2
3
4
CREATE TABLE `gap` (
`id` int(11) DEFAULT NULL,
KEY `ind_gap_id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 创建会话1,开启事务A并执行update 语句
1
2
start transaction;
update gap set id = 30 where id = 33;
  1. 创建会话2,开启事务B并执行另一个update 语句
1
2
start transaction;
update gap set id = 22 where id = 20;

在会话2中 插入20 > id < 39范围外的值时 可以执行成功,而当要插入 [20,39)范围内的值时 会遇到gap lock 。

  1. 用会话1 查看当前正在进行中的事务
    1
    SELECT * FROM information_schema.INNODB_TRX;

不会意外,能看到下面两条记录:

可以看到 进程id为3175 的事务在锁住了,而另一个id为3173的事务正在执行,但是没有提交事务。

这是因为执行update 语句之后,mysql 会执行索引扫描并在该表上施加一个 next-key lock ,向左扫描到20,向右扫描到39 ,锁定区间左闭右开,所以lock的范围是 [20,39)

解决办法

根据实际情况的不同,有不同的方式可以避免死锁,这里介绍常用的几种:

  1. 改变数据库操作逻辑,尽量避免在不同的事务中,对同一条记录进行更改。
  2. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。

参考链接

评论