浅析 Mysql 中的锁

浅析 Mysql 中的锁

一、全局锁

mysql 中的全局锁,指的是对整个数据库实例加锁,一般的实现方式有两种。一是可以执行语句 flush tables with read lock,即所谓的 FTWRL,让整个数据库处于只读状态,之后在这个数据库上面的增删改操作都会被阻塞,例如下面的例子:

在这里插入图片描述

针对 FTWRL,释放锁的方式有两个:一是客户端断开连接,二是使用命令 unlock tables

第二种加全局锁的方式是修改 mysql 的系统配置,有一个配置为 read_only ,可以使用命令 set global read_only = true 来开启。需要注意的是,如果是对 mysql 拥有超级权限的用户,例如 root,那么 read_only 的配置是不会对其生效的。

对 mysql 数据库实例加全局锁的目的一般是进行逻辑备份,即把数据库的数据 select 出来存为文本,但是备份并不推荐使用 set global read_only = true ,因为这种方式对数据库的影响范围很大,即便是客户端断开连接,数据库仍然是被锁住的,这样长期让数据库处于只读状态,对业务可能有风险,还有一个原因便是在 mysql 主备配置中,read_only 的值常被用来判断是否是备库(备库的 read_only 一般会设为 true)。

官方推荐的 mysql 逻辑备份工具是 mysqldump,如果你的数据库表使用的是支持事务的 InnoDB 引擎的话,可以加上参数 --single-transaction,确保拿到一致性视图,这样的话由于 MVCC 的支持,备份期间其他事务是可以正常执行的。

二、表级锁

mysql 表级锁共有两种形式,一是和全局锁类似,显式的使用语句 lock tables 表名 read/write lock,如果不加表名的话,则默认是对当前数据库的所有表加锁。释放锁的方式和全局锁是一样的,一是客户端断开连接,二是使用命令 unlock tables

meta data lock

另一种表级锁是 meta data lock,即 MDL 锁,这种锁是 mysql 自带的,在必要的时候会自动加上,并不需要我们显式的使用,其主要的目的是为了避免 DDL(修改表结构) 和 DML(增删改数据) 之间的并发冲突。

这种情况不难理解,例如一个事务读取一张表的数据,而另一个事务同时在修改这个表的结构(例如增加一个字段),如果不加锁的话,那么读取的数据则和表结构不一致。

因此 mysql 在 5.5 版本引入了 MDL 锁,在对表进行增删改查数据的时候,会加 MDL 读锁,变更表结构的时候,会加上 MDL 写锁。读锁和写锁之间的冲突规则如下:

  • 读锁与读锁之间不冲突,即多个事务可以同时进行 DML
  • 读锁和写锁之间冲突
  • 写锁和写锁之间冲突

下面是一个简单的例子,当另一个事务修改表结构的时候,就会被阻塞,直到另一个事务 commit 之后才会继续执行。

start transaction(开启事务)start transaction(开启事务)
select * from t whre id = 1;
alter table t add column c varchar(10);(语句被阻塞)

三、行级锁

mysql 中的行锁是在存储引擎层实现的,并不是所有的存储引擎都支持行锁,mysql 中的 MyISAM 引擎就不支持行锁,InnoDB 则支持行锁。行锁的粒度更小,因此对并发的支持更好,这也是 InnoDB 取代 MyISAM 成为 mysql 默认引擎的重要原因之一。

行锁很好理解,它针对的是表中的一行记录的读写冲突,例如一个事务要修改一行数据,而另一个事务同时也要修改同一行数据,那么后面的事务必须等前面的事务提交之后才能继续执行。

在实现上,mysql 使用了共享锁(shared lock,S 锁)和排它锁(exclusive lock, X 锁),共享锁有读取一行数据的权限,而排它锁则拥有更新和删除行数据的权限,S 锁和 X 锁之间是互斥的。

start transaction(开启事务 A)start transaction(开启事务 B)
update t set c = 5 whre id = 1;(占有 id 为 1 这一行的行锁)
update t set c = 10 whre id = 1;(语句被阻塞)

3.1 两阶段锁协议

在上面表格中所示的例子中,事务 A 开启之后,执行 update 语句并占据了 id 为 1 这一行的行锁,需要注意的是,锁并不是 sql 语句执行完毕之后释放的,而是在当前事务 A 提交之后释放的,这便叫做两阶段锁协议。

两阶段锁协议在日常的业务开发中可能会有一些指导性的意义,例如用户 A 和用户 B 需要同时给用户 C 转账,涉及到的操作便是扣除 A 和 B 各自账户的余额,并加到 C 的账户余额中。

为了保证数据一致性,肯定需要将扣除余额和给 C 加上余额这两个操作放到一个事务中,那么我们可以将给 C 账户加余额这条语句放到事务的最后执行,这样的话占有这一行的行锁时间最短,能够有效提升并发下的性能。

3.2 死锁

在 InnoDB 的 行锁策略下,死锁是一种常见的现象,它指的是多个线程之间出现了资源的循环依赖,并且互相等待对方释放资源,由此造成一种恶性循环,下面是一个死锁的例子:

start transaction(开启事务 A)start transaction(开启事务 B)
update t set d = d + 1 where id = 0;(持有 id = 0 这一行的行锁)
update t set d = d + 1 where id = 5;(持有id = 5 这一行的行锁)
update t set d = d + 1 where id = 5;(等待 id = 5 这一行的行锁)
update t set d = d + 1 where id = 0;(等待 id = 0 这一行的行锁)

出现死锁之后,mysql 有两种策略:一是进入等待直到超时,二是死锁检测。

mysql 中的参数 innodb_lock_wait_timeout 表示的是锁等待超时时间,默认值为 50s,对于大多数线上服务来说,50s 的超时等待时间往往是不可接受的。

如果将超时时间设置得很短的话,例如 1s,这样死锁的确能够很快解开,但同时容易出现误伤,因为有些情况下是正常的锁等待,而不是死锁,这样对业务有可能造成影响。

因此在大多数情况下,还是更加倾向于使用第二种方式,即死锁检测。参数 innodb_deadlock_detect 为 ON 的时候,表示开启死锁检测,默认值也是开启的。

死锁检测的方案可能对性能产生副作用,因为每次只要有一个线程加入,mysql 都要去检测所有的锁,查看是否有死锁产生,遍历的时间复杂度是 O(n),如果最后检测的结果并不是死锁,而是正常的锁等待,这样便会极大的消耗 CPU 资源。

我们可以采用一些方式来避免这种情况,例如将逻辑上的一行拆分成多行,当需要更新这一行数据的时候,随机从从中选择一行进行更新,如果拆分成了 10 行,那么并发下锁冲突的概率便降低了 10 倍,但是这种处理方式需要业务代码有特殊的处理。

四、间隙锁

4.1 幻读

mysql 中的间隙锁是为了解决幻读问题而产生的,所以我们先来了解一下什么是幻读。在 mysql 的可重复读隔离级别下,如果一个事务中的前后相同的查询,后一次的查询查到了新插入的行,则称这种现象为幻读,下面我举一个简单的例子:

假如建表语句如下,并且往表中插入了几行记录:

CREATE TABLE `t` (
  `id` int NOT NULL,
  `c` int DEFAULT NULL,
  `d` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

然后开启一个事务,查询 id 小于等于 5 的行,并且在事务执行的过程当中新插入一行。(注意这是一个假设的场景,实际的执行中因为有间隙锁的存在 insert 语句会被阻塞)

start transaction(开启事务 A)
select * from t where id <= 5 for update; (查询到的结果 0, 0, 0; 5, 5, 5)
insert into t value (3, 3, 3);
select * from t where id <= 5 for update; (查询到的结果 0, 0, 0; 3, 3, 3; 5, 5, 5)
commit;

可以看到,后一次查询查到了新插入的行 (3, 3, 3),这种现象便是幻读,幻读的产生有两个条件:

  • 在可重复读隔离级别下,普通的查询是一致性读,由于 MVCC 的支持,事务是不会查询到新插入的行的,因此幻读只可能在当前读的情况下出现,你可以看到上面的例子中我的查询语句加上了 for update 表示读当前行的最新值。
  • 幻读仅指新插入的行,如果上面的例子中不是 insert 语句,而是 update 语句的话,就算事务 A 的后一次查询查到了 updae 后的值,那么也不能叫做幻读。

4.2 Gap Lock

为了解决幻读的问题,InnoDB 引入了间隙锁,对于扫描到的行,不仅会加上行锁,还会在行之间的间隙上加锁。例如上面的示例表 t,新插入了 6 行数据,那么就产生了 7 个间隙:

在这里插入图片描述

需要注意的是,间隙锁和普通的行锁是不同的,行锁之间是互斥的,即多个行锁之间可能会出现资源争用而导致阻塞,但是多个间隙锁之间是不会互斥的,与间隙锁产生互斥的是往间隙中插入数据的这个动作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值