|
|
51CTO旗下网站
|
|
移动端

为什么开发人员必须要了解数据库锁?

何为锁?锁在现实中的意义为:封闭的器物,以钥匙或暗码开启。在计算机中的锁一般用来管理对共享资源的并发访问,比如我们 Java 同学熟悉的 Lock,synchronized 等都是我们常见的锁。

作者:佚名来源:咖啡拿铁|2018-09-25 08:33

 

当然在我们的数据库中也有锁用来控制资源的并发访问,这也是数据库和文件白菜送彩金大全的区别之一。

为什么要懂数据库锁?

通常来说对于一般的开发人员,在使用数据库的时候一般懂点 DQL(select),DML(insert,update,delete)就够了。

小明是一个刚刚毕业在互联网公司工作的 Java 开发工程师,平常的工作就是完成 PM 的需求。

当然在完成需求的同时肯定逃脱不了 Spring,Spring MVC,Mybatis 的那一套框架,所以一般来说 SQL 还是自己手写,遇到比较复杂的 SQL 会从网上去百度一下。

对于一些比较重要操作,比如交易啊这些,小明会用 Spring 的事务来对数据库的事务进行管理,由于数据量比较小目前还涉及不到分布式事务。

前几个月小明过得都还风调雨顺,直到有一天,小明接了一个需求,商家有个配置项,叫优惠配置项,可以配置买一送一,买一送二等等规则。

当然这些配置是批量传输给后端的,这样就有个问题每个规则都得去匹配,他到底是删除还是添加还是修改,这样后端逻辑就比较麻烦。

聪明的小明想到了一个办法,直接删除这个商家的配置,然后全部添加进去。小明马上开发完毕,成功上线。

开始上线没什么毛病,但是日志经常会出现一些 mysql-insert-deadlock 异常。

由于小明经验比较浅,对于这类型的问题第一次遇见,于是去问了他们组的老司机大红。

大红一看见这个问题,然后看了他的代码之后,输出了几个命令看了几个日志,马上定位了问题,告诉了小明:这是因为 delete 的时候会加间隙锁。

但是间隙锁之间却可以兼容,但是插入新的数据的时候就会因为插入意向锁会被间隙锁阻塞,导致双方资源被互占,导致死锁。

小明听了之后似懂非懂,由于大红的事情比较多,不方便一直麻烦大红,所以决定自己下来自己想。

下班过后,小明回想大红说的话,什么是间隙锁,什么是插入意向锁,看来作为开发者对数据库不应该只会写 SQL 啊,不然遇到一些疑难杂症完全没法解决啊。想完,于是小明就踏上了学习 MySQL 锁这条不归之路。

什么是 InnoDB?

MySQL 体系架构

小明没有着急去了解锁这方面的知识,他首先先了解了下 MySQL 体系架构:

可以发现 MySQL 由连接池组件、管理服务和工具组件、SQL 接口组件、查询分析器组件、优化器组件、 缓冲组件、插件式存储引擎、物理文件组成。

小明发现在 MySQL 中存储引擎是以插件的方式提供的,在 MySQL 中有多种存储引擎,每个存储引擎都有自己的特点。

随后小明在命令行中打出了:

  1. show engines \G; 

一看原来有这么多种引擎。又打出了下面的命令,查看当前数据库默认的引擎:

  1. show variables like '%storage_engine%'

小明恍然大悟:原来自己的数据库是使用的 InnoDB,依稀记得自己在上学的时候好像听说过有个引擎叫 MyIsAM,小明想这两个有啥不同呢?

马上查找了一下资料:

小明大概了解了一下 InnoDB 和 MyIsAM 的区别,由于使用的是 InnoDB,小明就没有过多的纠结这一块。

事务的隔离性

小明在研究锁之前,又回想到之前上学的时候教过的数据库事务隔离性,其实锁在数据库中其功能之一也是用来实现事务隔离性。而事务的隔离性其实是用来解决脏读,不可重复读,幻读几类问题。

脏读

一个事务读取到另一个事务未提交的更新数据。什么意思呢?

在事务 A,B 中,事务 A 在时间点 2,4 分别对 user 表中 id = 1 的数据进行了查询。

但是事务 B 在时间点 3 进行了修改,导致了事务 A 在 4 中的查询出的结果其实是事务 B 修改后的。这样就破坏了数据库中的隔离性。

不可重复读

在同一个事务中,多次读取同一数据返回的结果不同,不可重复读和脏读不同的是这里读取的是已经提交过后的数据。

在事务 B 中提交的操作在事务 A 第二次查询之前,但是依然读到了事务 B 的更新结果,也破坏了事务的隔离性。

幻读

一个事务读到另一个事务已提交的 insert 数据。

在事务 A 中查询了两次 id 大于 1 的,在第一次 id 大于 1 查询结果中没有数据,但是由于事务 B 插入了一条 id = 2 的数据,导致事务 A 第二次查询时能查到事务 B 中插入的数据。

事务中的隔离性:

小明注意到在收集资料的过程中,有资料写到 InnoDB 和其他数据库有点不同,InnoDB 的可重复读其实就能解决幻读了,小明心想:这 InnoDB 还挺牛逼的,我得好好看看到底是怎么个原理。

InnoDB 锁类型

小明首先了解了 MySQL 中常见的锁类型有哪些:

S or X

在 InnoDB 中实现了两个标准的行级锁,可以简单的看为两个读写锁:

S 共享锁:又叫读锁,其他事务可以继续加共享锁,但是不能继续加排他锁。

X 排他锁:又叫写锁,一旦加了写锁之后,其他事务就不能加锁了。

兼容性:是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。

纵轴是代表已有的锁,横轴是代表尝试获取的锁。

意向锁

意向锁在 InnoDB 中是表级锁,和它的名字一样它是用来表达一个事务想要获取什么。

意向锁分为:

  • 意向共享锁:表达一个事务想要获取一张表中某几行的共享锁。
  • 意向排他锁:表达一个事务想要获取一张表中某几行的排他锁。

这个锁有什么用呢?为什么需要这个锁呢?首先说一下如果没有这个锁,要给这个表加上表锁,一般的做法是去遍历每一行看看它是否有行锁,这样的话效率太低。

而我们有意向锁,只需要判断是否有意向锁即可,不需要再去一行行的去扫描。

在 InnoDB 中由于支持的是行级的锁,因此 InnboDB 锁的兼容性可以扩展如下:

自增长锁

自增长锁是一种特殊的表锁机制,提升并发插入性能。

对于这个锁有几个特点:

  • 在 SQL 执行完就释放锁,并不是事务执行完。
  • 对于 insert...select 大数据量插入会影响插入性能,因为会阻塞另外一个事务执行。
  • 自增算法可以配置。

在 MySQL 5.1.2 版本之后,有了很多优化,可以根据不同的模式来调整自增加锁的方式。

小明看到了这里打开了自己的 MySQL 发现是 5.7 之后,便输入了下面的语句,获取到当前锁的模式:

  1. mysql> show variables like 'innodb_autoinc_lock_mode'
  2. +--------------------------+-------+ 
  3. | Variable_name            | Value | 
  4. +--------------------------+-------+ 
  5. | innodb_autoinc_lock_mode | 2     | 
  6. +--------------------------+-------+ 
  7. 1 row in set (0.01 sec) 

在 MySQL 中 innodbautoinclock_mode 有 3 种配置模式 0、1、2,分别对应:

传统模式:也就是我们最上面的使用表锁。

连续模式:对于插入的时候可以确定行数的使用互斥量,对于不能确定行数的使用表锁的模式。

交错模式:所有的都使用互斥量,为什么叫交错模式呢,有可能在批量插入时自增值不是连续的,当然一般来说如果不看重自增值连续一般选择这个模式,性能是最好的。

InnoDB 锁算法

小明已经了解到了在 InnoDB 中有哪些锁类型,但是如何去使用这些锁,还是得靠锁算法。

记录锁(Record-Lock)

记录锁是锁住记录的,这里要说明的是这里锁住的是索引记录,而不是我们真正的数据记录:

如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住。

如果没有表上没有索引(包括没有主键),则会使用隐藏的主键索引进行加锁。

如果要锁的列没有索引,则会进行全表记录加锁。

间隙锁

间隙锁顾名思义锁间隙,不锁记录。锁间隙的意思就是锁定某一个范围,间隙锁又叫 gap 锁,其不会阻塞其他的 gap 锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键。

next-key 锁

这个锁本质是记录锁加上 gap 锁。在 RR 隔离级别下(InnoDB 默认),InnoDB 对于行的扫描锁定都是使用此算法,但是如果查询扫描中有唯一索引会退化成只使用记录锁。

为什么呢? 因为唯一索引能确定行数,而其他索引不能确定行数,有可能在其他事务中会再次添加这个索引的数据造成幻读。

这里也说明了为什么 MySQL 可以在 RR 级别下解决幻读。

插入意向锁

插入意向锁 MySQL 官方对其的解释:

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

可以看出插入意向锁是在插入的时候产生的,在多个事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。

假设有一个记录索引包含键值 4 和 7,不同的事务分别插入 5 和 6,每个事务都会产生一个加在 4-7 之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。

这里要说明的是如果有间隙锁了,插入意向锁会被阻塞。

MVCC

MVCC,多版本并发控制技术。在 InnoDB 中,在每一行记录的后面增加两个隐藏列,记录创建版本号和删除版本号。通过版本号和行锁,从而提高数据库白菜送彩金大全并发性能。

在 MVCC 中,对于读操作可以分为两种读:

  • 快照读:读取的历史数据,简单的 select 语句,不加锁,MVCC 实现可重复读,使用的是 MVCC 机制读取 undo 中的已经提交的数据。所以它的读取是非阻塞的。
  • 当前读:需要加锁的语句,update,insert,delete,select...for update 等等都是当前读。

在 RR 隔离级别下的快照读,不是以 begin 事务开始的时间点作为 snapshot 建立时间点,而是以第一条 select 语句的时间点作为 snapshot 建立的时间点。以后的 select 都会读取当前时间点的快照值。

在 RC 隔离级别下每次快照读均会创建新的快照。

具体的原理是通过每行会有两个隐藏的字段一个是用来记录当前事务,一个是用来记录回滚的指向 Undolog。利用 Undolog 就可以读取到之前的快照,不需要单独开辟空间记录。

加锁分析

小明到这里,已经学习很多 MySQL 锁有关的基础知识,所以决定自己创建一个表搞下实验。

首先创建了一个简单的用户表:

  1. CREATE TABLE `user` ( 
  2.   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 
  3.   `namevarchar(11) CHARACTER