InnoDB锁与事务简析

从新梳理了一下锁、锁与事务的关系,但愿可以帮你们厘清一些知识点。本文若是不作特殊说明,默认是可重复读隔离级别。html

悲观锁与乐观锁

在讲述InnoDB锁以前,先和你们聊一下悲观锁与乐观锁mysql

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

悲观锁是不管作什么都须要先获取到锁,乐观锁其实并无锁的概念,作任何操做都不加锁,可是更新数据的时候会检查要更新的数据是否被修改过,通常用CAS实现(Compare-and-Set)算法

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

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

悲观锁相对影响性能,乐观锁由于不加锁,性能会更好,你们能够根据具体状况选择不一样的设计。缓存

InnoDB锁

如今让咱们聊一下InnoDB的锁,InnoDB支持两种级别的锁session

行级别锁:共享锁(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
相关文章
相关标签/搜索