MySQL锁总结

     我们日常开发中用到最多的存储引擎是Innodb 与 MyISAM两种,而 Innodb 现在更多是首选,因此本文主要是对 Innodb 的说明,文中的内容全部来自于互联网,我自是做一个整理记录,以便用时翻阅。  -------------我是旧时代的残党,新世界没有能载我骄傲的巨船

目录

1.锁分类

2.InnoDB中的锁与索引  ​编辑

3.InnoDB中如何加锁?

4.显示锁的业务场景

5.InnoDB中行锁的实现:

6.加锁

7.实际SQL语句锁的分析

8.死锁模拟


1.锁分类

 锁分很多的类别,我们先来看看锁在不同维度上的分类

       MySQL锁总结_第1张图片

 以操作粒度区分:

  • 表级锁:每次操作锁住整张表。锁定的粒度大、开销小、加锁快;不会发生死锁,但发生锁冲突的概率极高,并发度最低,应用在InnoDB、MyISAM、BDB中;

  • 行级锁:每次操作锁住一行数据。锁定的粒度小、开销大、加锁慢;会出现死锁,发生锁冲突的概率极低,并发度最高,应用在InnoDB中;

  • 页级锁:每次锁定相邻的一组记录。锁定粒度、开销、加锁时间介于行级锁和表级锁之间;会出现死锁,并发度一般,应用在BDB中;

以操作类型区分 

  • 读锁(S):共享锁,针对同一份数据,多个读操作可以同时进行不会互相影响;

  • 写锁(X):排它锁,当前写操作没有完成时,会阻塞其他读和写操作;

    不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁

 为了允许行锁和表锁的共存,实现多粒度的锁机制,InnoDB还有两种内部使用的意向锁,这两种意向锁都是表锁:

  • 意向读锁(IS)、意向写锁(IX):属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之前,会先对表添加IS和IX锁,表明某个事务正在持有某些行的锁、或该事务准备去持有锁;意向锁存在是为了协调锁之间的关系,支持多粒度锁共存;

     那为什么两种意向锁是表锁呢?是为了减少确认次数,提升性能:如果意向锁是行锁,需要遍历每一行去确认数据是否已经加锁;如果是表锁的话,只需要判断一次就知道有没有数据行被锁定;

      比如:事务A有行锁的时候,MySQL会自动给该表加上意向锁,事务B如果想申请整个表的写锁,就不用遍历去每一行判断是否存在行锁,只需要判断是否存在意向锁,即可决定是否可以加表的写锁

以操作性能区分

  • 乐观锁:一般采用的方式是对数据记录版本进行对比,在数据更新提交时才会进行冲突检测,如果发现冲突了,则提示错误信息;

  • 悲观锁:在对一条记录进行修改时,为了避免被其他人修改,在修改数据之前先锁定再修改的方式。共享锁和排它锁是悲观锁的不同实现。

      这里我们注意,在mysql中,乐观锁的实现我们可以通过给表增加一个字段OBJECT_VERSION_NUMBER来实现,那悲观锁我们怎么实现呢?实现悲观锁利用select ... for update加锁, 操作完成后使用commit来释放锁;比如select status from table where id=1 for update; 并且在Mysql中共享锁和排他锁都是悲观锁的实现。

2.InnoDB中的锁与索引  MySQL锁总结_第2张图片

       在InnoDB中,支持行锁和表锁,行锁又分为共享锁和排它锁。InnoDB行锁是通过对索引数据页上的记录加锁实现的。由于InnoDB行锁的实现特点,导致只有通过索引条件检索并且执行计划中真正使用到索引时InnoDB才会使用行锁 ;并且不论使用主键索引、唯一索引、普通索引,InnoDB都会使用行锁来进行加锁,否则InnoDB将使用表锁。由于InnoDB是针对索引加锁,而不是针对记录加锁,所以即使多个事务访问不同行的记录,但如果使用的是相同的索引,还是会出现锁冲突的情况,甚至出现死锁。

     这里我们就要注意了,删除数据修改数据的时候,我们的where条件一定要命中索引不然就要锁表,并发情况下可能就造成了事务的锁的超时。

     虽然有时候where后面的额条件加了索引,但是并不一定会都走到索引,主要看MySQL的优化器如何去优化。Mysql优化还是有一些规则的 ,具体可以看我的这篇文章高性能MySql--01(数据库索引)_时空恋旅人的博客-CSDN博客

3.InnoDB中如何加锁?

  • 意向锁是 InnoDB 自动加的, 不需用户干预。
  • 对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB会自动给涉及数据集加排他锁(X);
  • 由于MVCC的存在,对于普通 SELECT 语句,InnoDB 不会加任何锁;
    事务可以通过以下语句显式给记录集加共享锁或排他锁:
    • 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。 其他 session 仍然可以查询记录,并也可以对该记录加 share mode 的共享锁。但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。
    • 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。其他 session 可以查询该记录,但是不能对该记录加共享锁或排他锁,而是等待获得锁

       从他的加锁的方式我们就可以看到,InnoDB锁也分为显式和隐式两种,隐式锁就是我们刚刚说的 UPDATE、 DELETE 和 INSERT 语句是会自动加上隐式的锁,InnoDB会根据隔离级别在需要的时候自动加锁;锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。

 显示锁比如:

select ... lock in share mode //共享锁 
select ... for update //排他锁 

   两种显示的锁呢在实际的开发中可能用的比较少,下面我就来介绍一下他的使用业务场景。

4.显示锁的业务场景

    select *** for update 的使用场景:为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到 for update 子句。

   select lock in share mode :in share mode 子句的作用就是将查找到的数据加上一个 share 锁,这个就是表示其他的事务只能对这些数据进行简单的select 操作,并不能够进行 DML 操作。select *** lock in share mode 使用场景:为了确保自己查到的数据没有被其他的事务正在修改,也就是说确保查到的数据是最新的数据,并且不允许其他人来修改数据。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据 使用了 in share mode 的方式上了 S 锁。

5.InnoDB中行锁的实现:

行锁的主要实现有三种: RecordLockGapLockNext-KeyLock

  • RecordLock:记录锁,锁定单个行记录的锁,RC和RR隔离级别支持。

  • GapLock:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。范围锁,RR隔离级别支持。(加锁之后间隙范围内不允许插入数据,防止发生幻读)使用间隙锁的目的是为了防止幻读。防止已存在的数据,更新成间隙内的数据.

  • Next-Key Lock:临键锁,它是记录锁和间隙锁的结合体,锁住数据的同时锁住数据前后范围。记录锁+范围锁,RR隔离级别支持,

6.加锁

  1. SELECT...FROM...
     :InnoDB采用MVCC机制实现非阻塞读,对于普通的 SELECT 语句,InnoDB不加锁。

  2. SELECT...FROM...LOCKInSHARE MODE
     :显式追加共享锁,InnoDB会使用临键锁(Next-key Lock)进行处理,如果发现了唯一索引,可以降级为记录锁(RecordLock)。

  3. SELECT...FROM...FOR UPDATE
     :显式追加排它锁,InnoDB会使用Next-Key Lock锁进行处理,如果发现唯一索引,可以降级为RecordLock锁。

  4. UPDATE...WHERE
     :InnoDB会使用临键锁(Next-key Lock)进行处理,如果扫描发现唯一索引,可以降级为记录锁(RecordLock)。

  5. DELETE...WHERE
    :InnoDB会使用临键锁(Next-key Lock)进行处理,如果扫描发现唯一索引,可以降级为记录锁(RecordLock)。

  6. insert
    :InnoDB会在将要插入的那一行设置一个排他的记录锁(RecordLock)。

7.实际SQL语句锁的分析

  锁等待超时:ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

  死锁:1213 Deadlock found when trying to get lock

 我们来分析一个实际的案例,准确的理解MySQL中的锁 

假设有这样的一个表: 建表语句,以及表中的数据如下

DROP TABLE IF EXISTS `t`;
CREATE TABLE `t`  (
  `id` int(11) NOT NULL,
  `c` int(11) NULL DEFAULT NULL,
  `d` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `c`(`c`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;


INSERT INTO `t` VALUES (0, 0, 0);
INSERT INTO `t` VALUES (5, 5, 5);
INSERT INTO `t` VALUES (10, 10, 11);
INSERT INTO `t` VALUES (15, 15, 15);
INSERT INTO `t` VALUES (20, 20, 20);
INSERT INTO `t` VALUES (25, 25, 25);

MySQL锁总结_第3张图片

主键加锁

我们先来看看通过主键加锁是怎么加的?加锁是通过索引来完成的,上面的表组成的主键的索引就是这样的,

          

当我们执行 update t  set d=‘12’  where  id=5 的时候,MySQL一上来会使用临键锁,扫描发现了唯一的索引,这个唯一索引又恰好是主键,会直接通过主键索引记录上加排它锁。也就是X锁。

       MySQL锁总结_第4张图片

 唯一索引加锁:

    假设c字段为唯一索引唯一索引在数据库中的排列为

        

       假设我们的SQL语句为   update  t  set d=‘12’  where c =5  ,那么他的加锁行为是这样的,先上来他会使用临键锁,扫描发现了唯一的索引,所以他会退化成记录锁,并且先加在唯一键上,然后再id=5的主键记录上加X锁。

     MySQL锁总结_第5张图片

 

    普通索引加锁(非唯一索引):

         假设c字段为普通索引,索引在数据库中的排列为

      

       同样的现在我们的SQL语句为: update  t  set d=‘12’  where c =5 , 这个时候MySQL一上来还是会加临键锁,没有扫描到唯一的索引,那临键锁就加上了,我们知道临键锁就是记录锁和间隙锁的合集,所以他加锁后的状态如下:首先在满足where条件c=5的记录和主键分别加X锁,这是一个记录锁,然后在(0,5),(5,10)范围分别加Gap Lock。

MySQL锁总结_第6张图片

 无索引加锁

    加锁行为:表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎 锁机制是基于索引实现的记录锁定)。

   

通过以上的实例分析我们对MySQL的加锁行为有了一定的了解 ,然后我们再来分析几组复杂场景的加锁。

等值查询间隙锁

MySQL锁总结_第7张图片

分析:

  1. session1会开启一个事务,执行update语句,我们知道update语句Mysql会自己隐式加锁,并且首先会使用临键锁来处理,如果发现唯一的索引会降级为记录锁,只锁一条记录,由于表T没有 id=7 这条记录,加锁单位是 Next-keyLock ,事务1加锁范围是 (5,10],注意是左开右闭,因为 id=7 是一个等值查询,根据优化规则, id=10不满足条件, Next-keyLock
     退化成 GapLock ,因此最终加锁范围是 (5,10)

  2. Session2想要向这个间隙中插入 id=8的记录必须等待 Session1事务提交后才可以。

  3. Session3想要插入 id=11,不在加锁范围,所以可以插入成功。

这是如果有 Session4,想要更新 id=8的记录,id=8这个记录不存在,他也是间隙锁,因为间隙锁之间互不冲突;是可以执行成功的,

     我们在MySQL里面实际操作执行一下语句看看是不是和分析的一样,OK 我们先执行session1,再执行session2的语句。然后我们查看当前运行的所有事物:

SELECT * FROM  INFORMATION_SCHEMA.INNODB_TRX;

 

 我们发现有两个事务,一个是297248,一个是297249,  297248在运行中,297249在等待中

然后我们再查询一下当前出现的锁:

SELECT  * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

可以看到我们的两个事务加的都是间隙锁,所以我们之前分析的是正确的。 

然后我们再看一下锁等待的对应关系

SELECT  * FROM  INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

MySQL锁总结_第8张图片

 blocking_trx_id表示当前拥有锁的事务id,是297248,请求锁的事务id requesting_trx_id是297249

  非唯一键等值锁

MySQL锁总结_第9张图片

分析:

  1. Session1给索引 c 上的 c=5 这一列加上读锁,读锁呢其他的事务能读不能写,加锁单位为 Next-keyLock,因此会给 (0, 5] 区间加上 Next-keyLock,因为c是普通索引,并且加锁的方式是LOCK IN SHARE MODE,所以这里锁住索引只会锁覆盖索引,不会像之前一样去锁主键id,访问 c=5 之后还要向右遍历,直到 c=10 停止,访问到的都要加锁,所以加锁范围为 (5, 10] ,根据优化规则, id=10不满足条件,等值查询退化为 GapLock变为 (5, 10),所以最终的加锁范围是 (0, 10);锁的类型也是两个c=5时候的记录锁,(0,5)(5,10)的两个间隙锁。

  2. Session2想要插入 id=7 的记录,要等待 Session1提交之后才可以成功插入,因为 Session1
     的间隙范围是(5, 10);

  3. session3这个是需要拿到记录锁才能执行,session1使用的LOCK IN SHARE MODE共享锁,锁住的只是覆盖索引,没有锁主键索引。所以 Session3去拿主键的写锁是可以拿到的,所以语句可以正常执行;

LOCK IN SHARE MODE; 只锁覆盖索引,FOR UPDATE; 会顺便锁上主键索引;

主键索引范围锁

  1. select * from t where id=10 for update;

  2. select * from t where id>=10 and id<11 for update;

对于以上两条SQL,加锁的范围不一致,第一条是id=10 的行锁,第二条是 (10, 15] 的 Next-keyLock

MySQL锁总结_第10张图片

分析:

 session1加锁的范围怎么确定呢?

我们再看一眼索引,首先id>=10,id=10是主键唯一索引 所以这个记录被记录锁给锁住了,然后是id<11,没有11这一条数据,锁住的就是这个间隙(10,15],所以最终锁住的就是id=10这个索引和(10,15]这个间隙

那么我们再来分析session2和session3中的语句的执行就好办了,只要需要获得这个 Next-keyLock的SQL,都会被阻塞。

session3给该行添加 RecordLock ,由于 RecordLock 和 Next-keyLock不兼容,所以阻塞

非唯一索引范围锁

MySQL锁总结_第11张图片

 

  1. Session1给索引c加上了 (5,10], (10,15] 两个 Next-keyLock ;由于是范围查询,不触发优化,不会退化成间隙锁

8.死锁模拟

 1. AB BA操作问题

MySQL锁总结_第12张图片

互相等待对方释放锁就会出现死锁问题

2.S-lock 升级 X-lock

 MySQL锁总结_第13张图片

分析:

  1. Session1 获取到 S-Lock

  2. Session2 尝试获取到 X-Lock ,但是被 Session1 的S-Lock 阻塞

  3. Session1 想要获取到 X-Lock,本身拥有一个 S-Lock ,但是Session2 申请 X-Lock 在前,需要等待 Session2 释放之后才能提升到 X-Lock,两个事务造成资源争抢导致死锁

 

 

来源:

MySQL锁总结 - 知乎

MySQL锁总结 - 墨天轮

你真的了解MySQL中的锁吗-开源基础软件社区-51CTO.COM

你可能感兴趣的:(MySql全卷,mysql,数据库,database)