阅读 130

InnoDB锁与事务简析

重新梳理了一下锁、锁与事务的关系,希望能够帮大家厘清一些知识点。本文如果不做特殊说明,默认是可重复读隔离级别。

悲观锁与乐观锁

在讲述InnoDB锁之前,先和大家聊一下悲观锁与乐观锁

悲观锁和乐观锁阐述的是一种设计理念。

悲观锁是无论做什么都需要先获取到锁,乐观锁其实并没有锁的概念,做任何操作都不加锁,但是更新数据的时候会检查要更新的数据是否被修改过,一般用CAS实现(Compare-and-Set)

悲观锁先取锁再访问。数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁)均为悲观锁

乐观锁不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功

悲观锁相对影响性能,乐观锁因为不加锁,性能会更好,大家可以根据具体情况选择不同的设计。

InnoDB锁

现在让我们聊一下InnoDB的锁,InnoDB支持两种级别的锁

行级别锁:共享锁(S)和排它锁(X)

表级别锁:意向共享锁(IS)和意向排它锁(IX)。

1)意向共享锁(IS锁):事务在请求S锁前,要先获得IS锁 2)意向排他锁(IX锁):事务在请求X锁前,要先获得IX锁

因为InnoDB存储引擎支持的是行级别的锁,所以意向锁其实不会阻塞除全表扫描以外的任何请求。另外意向共享锁(IS)和意向排它锁(IX)是由InnoDB自行做加锁和解锁操作的,所以本文主要讲一下行级别锁。

共享锁与排它锁

共享锁和排他锁的特性

共享锁

  • 允许其它事务也增加共享锁读取
  • 不允许其它事物增加排他锁 (for update)
  • 当事务同时增加共享锁时候,事务的更新必须等待先执行的事务 commit 后才行,如果同时并发太大可能很容易造成死锁

排它锁

  • 事务之间不允许其它排他锁或共享锁读取,修改更不可能
  • 一次只能有一个排他锁执行 commit 之后,其它事务才可执行

两者的兼容性如下图所示:

在InnoDB中如何加共享锁或者排它锁?

添加共享锁:SELECT...LOCK IN SHARE MODE,如select * from test1 where id = 1 lock in share mode;

添加排它锁:SELECT...FOR UPDATE,如 select * from test1 where name = 5 for update;

如果不使用lock in share mode或者for update,仅使用select,是不会加锁的。此时极易产生丢失更新或者幻读的情况。

演示

表结构如下:

CREATE TABLE test1 ( id int unsigned NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

现有数据如下:

先看一下不加锁的情况下会出现的一些问题

丢失更新:

Session1 Session2
1 start transaction; start transaction;
2 select name into @name from test1 where id =1; select name into @name from test1 where id =1;
3 update test1 set name = @name - 100;
4 commit;
5 update test1 set name = @name + 100;
6 commit;

这种情况下,session1做的更改被session2覆盖了,最终的数值会变为188。当然这种情况发生的主要原因是sql写的不好。

  • update的时候,使用update test1 set name = name - 100(没有@),就可以避免这个问题。不要先查询,然后用查询到的值来更新
  • sql如果写的严谨,对于mysql的任何隔离级别来说,都不会发生丢失更新的问题,因为mysql会对DML操作加锁,两个事务更新同一条数据的时候,后来的更新会被阻塞住。

幻读:

Session1 Session2
1 start transaction; start transaction;
2 select * from test1 where name =1;(显示2 5 6 三条)
3 update test1 set name = 2 where id=2;
4 select * from test1 where name =1;(显示2 5 6 三条)
5 commit;
6 select * from test1 where name =1;(显示2 5 6 三条)
7 update test1 set name = 3 where name=1;(只影响两行)
8 commit;

可以看出,尽管session2更新了数据,但是session1查询的时候,数据仍然没有变化,但是更新的时候,只更改了两行,这就出现了幻象

  • session1读取的时候一直为三条,是因为mysql的select使用一致性的非锁定读操作,这个操作是通过多版本并发控制(Multi Version Concur-rency Control,MVCC)实现的。简单来说select读取的是个快照

    • 可重复读隔离级别下,读取的快照是事务启动前的快照,所以无论别的事务怎么更改数据,当前事务读取的数据是不变的

    • 读已提交隔离级别下,读取的的快照是最新的数据快照,所以别的事务提交后,当前事务读取会读取到最新的值

  • 更新的时候因为需要真正的修改数据,此时发现有一条不符合,所以只更新了两条

为了写代码的时候没有bug,我们可以加锁,将要变更的资源锁住,这样只有本事务可以对数据做操作,不怕其他事务对数据做update delete insert等操作,这里简单写一个排它锁

排它锁

Session1 Session2
1 start transaction; start transaction;
2 select * from test1 where name =1 for update;(显示2 5 6 三条)
3 update test1 set name = 2 where id=2; (阻塞)
4 select * from test1 where name =1;(显示2 5 6 三条)
5 update test1 set name = 3 where name=1;(影响三行)
6 commit;
7 update得以执行
8 commit;

可以看出,该示例和幻读的示例相比,只是session1在select时添加了for update(排它锁),通过这个操作便锁住了资源,session2的update无法执行。相信到这里大家对mysql的锁的作用有了比较清晰的理解。

锁的算法

InnoDB存储引擎有3种行锁的算法设计,分别是:

  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

在REPEATABLE READ模式下,Next-Key Lock算法是默认的行记录锁定算法。但是InnoDB存储引擎会根据情况自己选一个最小的算法模型,即Next-Key Lock会退化成Record Lock或者Gap Lock。

Record Lock比较好理解,就是对单行加锁,只锁定一行,如一般where = 的时候会使用行锁。Gap Lock和Next Key Lock锁定一个范围,一般where < 的时候会锁定范围,如果我使用select * from test1 where id <100 for update;,那么其他事务无论是insert或者update id<100的记录都会被阻塞,但是100之外的没有问题。所以Mysql在REPEATABLE READ模式下通过Record Lock解决了幻读问题。

错误用锁导致的问题

锁如果使用错误,会导致一些问题产生,如死锁或者不小心将整个表锁住。

死锁

Session1 Session2
1 start transaction; start transaction;
2 select * from test1 where id =1 for update;
3 select * from test1 where id =2 for update;
4 select * from test1 where id =1 for update;
5 select * from test1 where id =2 for update;(ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction)

死锁发生情况很多,上面只展示了其中一种,mysql有解除死锁的机制:发现死锁后,InnoDB存储引擎会马上回滚一个事务。但大家尽量不要写出有死锁的代码。

锁住整张表

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁!

大家可以用select * from performance_schema.data_locks;查看被锁住的数据。

锁与事务的关系

上面讲述了锁的很多信息,那么锁与事务有什么关系呢?

大家都知道到Mysql的事务有四个特性,即ACID,原子性(Atomicity)、一致性(Correspondence)、隔离 性(Isolation)、持久性(Durability)。

锁和事务的关系:事务的隔离性通过锁来实现。

为什么锁能实现隔离性,因为加了锁之后,数据就不能被别人随便更改了。

常用命令

  1. 查看是否自动提交 show session variables like 'autocommit';

  2. 查询正在执行的事务 SELECT * FROM information_schema.INNODB_TRX;

  3. 查看正在锁的事务

    • SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
    • select * from performance_schema.data_locks; - mysql8.0
  4. 查看等待锁的事务

    • SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

    • SELECT * FROM sys.innodb_lock_waits; - mysql8.0

  5. 查看mysql当前默认的存储引擎 show variables like '%storage_engine%';

  6. 查看mysql版本 select version();

  7. 查看隔离级别 select @@transaction_isolation;

参考资料

  1. 悲观锁与乐观锁的实现(详情图解)
  2. MySQL的SELECT ...for update
  3. MySQL 共享锁 (lock in share mode),排他锁 (for update)
  4. MySQL的自动提交模式
  5. mac 安装mysql@5.7 (brew 安装配置)
  6. MySQL 函数
  7. mysql8.0查看锁信息
  8. www.jianshu.com/p/32904ee07… 间歇锁
  9. Mysql加锁过程详解(9)-innodb下的记录锁,间隙锁,next-key锁
  10. mysql的共享锁(S)、排他锁(X)、意向共享锁(IS)、意向排他锁(IX)的关系
  11. Mysql-丢失更新
  12. MySQL技术内幕:InnoDB存储引擎

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

往期文章回顾:

  1. CDN请求过程详解
  2. 关于程序员职业发展的思考
  3. 记博客服务被压垮的历程
  4. 常用缓存技巧
  5. 如何高效对接第三方支付
  6. Gin框架简洁版
  7. 关于代码review的思考
  8. InnoDB锁与事务简析
  9. Markdown编辑器推荐-typora