1.MySQL锁机制

1.1 乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。乐观锁适合读多写少的情况。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。

1.2 悲观锁

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此,在整个数据处理过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。适合写多的情况。

1.3 行锁

行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。 行级锁按照使用方式分为共享锁和排他锁。
行锁:容易出现死锁,发生冲突概率低,并发高,InnoDB 支持行锁(必须有索引才能实现,否则会自动锁全表,那么就不是行锁了)。

共享锁用法(S锁 读锁)
若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。

排它锁用法(X 锁 写锁)
若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。

1.4 表锁

表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。

总结:
MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。

1.5 间隙锁

间隙锁,是在索引的间隙之间加上锁,这是为什么Repeatable Read隔离级别下能在一定情况下防止幻读的主要原因。 (RR级别不能完全解决幻读)

图片.png
MySQL在REPEATABLE READ隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC方案解决,也可以采用加锁方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。InnoDB提出了一种称之为Gap Locks的锁,官方的类型名称为:LOCK_GAP,我们可以简称为gap锁。
image.png
图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。

间隙锁可能会导致死锁的出现。

临键锁(Next-key Locks):Next-Key Locks是行锁与间隙锁的组合。

无索引行锁会升级为表锁:
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁。InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。

1.6 死锁

Session_1执行:select from account where id=1 for update;
Session_2执行:select
from account where id=2 for update;
Session_1执行:select from account where id=2 for update;
Session_2执行:select
from account where id=1 for update;
查看近期死锁日志信息:show engine innodb status\G;
大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁

2. MVCC

2.1 版本链

我们创建一个hero表:

  1. CREATE TABLE hero (
  2. number INT,
  3. name VARCHAR(100),
  4. country varchar(100),
  5. PRIMARY KEY (number)
  6. ) Engine=InnoDB CHARSET=utf8;

然后向这个表里插入一条数据:

  1. INSERT INTO hero VALUES(1, ‘刘备’, ‘蜀’);

现在表里的数据就是这样的:
image.png
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中除了包含用户定义的列外,还包含两个必要的隐藏列:

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

    假设插入该记录的事务id为80,那么此刻该条记录的示意图如下所示:
    image.png 假设之后两个事务id分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:
    image.png
    每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性,可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:
    image.png 对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。

2.2 ReadView

版本链中存放了该记录各个时间段的历史数据,而对于不同隔离级别的事务来说,版本链中的数据对它们的可见性是不同的,即它们需要读取的数据版本不同。

  • 对于使用READ UNCOMMITTED隔离级别的事务来说, 由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;
  • 对于使用SERIALIZABLE隔离级别的事务来说,它们是使用加锁的方式来访问记录,当事务获取相应的锁后,直接读取记录的最新版本就好了;
  • 对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。[

](https://blog.csdn.net/STILLxjy/article/details/112190576)

READ COMMITTED和REPEATABLE READ隔离级别的区别在于:
假设初始时数据表的数据为:
image.png
如下图,事务trx1和事务trx2为活动事务,正在执行。
image.png当trx1事务的隔离级别为 READ COMMITTED时,事务trx1第一个执行 SELECT from hero; 语句得到的结果中name为刘备,第二次执行 SELECT from hero; 语句时,由于 trx2事务的更新已经提交了,所以得到的结果中name为黄忠。对于READ COMMITTED隔离级别来说,在一个事务过程中,每次读取的数据是可能不同的。每次读到的是最新提交的数据。
当trx1事务的隔离级别为 REPEATABLE READ时,事务trx1第一个执行 SELECT from hero; 语句得到的结果中name为刘备,第二次执行 SELECT from hero; 语句时,还是以第一次的数据为准。
[

](https://blog.csdn.net/STILLxjy/article/details/112190576)
对于不同隔离级别的事务,它们是如何来判断自己应该读取数据的版本呢?为此,设计InnoDB的大叔提出了一个ReadView的概念,这个ReadView中主要包含4个比较重要的内容:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。(还未提交的事务)
  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
  • creator_trx_id:表示生成该ReadView的事务的事务id。

    有了这个ReadView,事务在访问某条记录时,只需要按照下边的步骤,就可以判断记录的某个版本是否可见:

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。 (自己创建的得认)

  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

[

](https://blog.csdn.net/STILLxjy/article/details/112190576)
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

由上面介绍的READ COMMITTED和REPEATABLE READ隔离级别的区别 可知,在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的本质区别就是它们生成ReadView的时机不同。

  • 对于使用READ COMMITTED隔离级别的事务来说 —— 每次读取数据前都生成一个ReadView
  • 对于使用REPEATABLE READ隔离级别的事务来说 —— 只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。

MVCC整体操作流程:
1. 首先获取事务自己的版本号,也就是事务 ID;
2. 获取 ReadView;
3. 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
4. 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
5. 最后返回符合规则的数据。

快照读:普通的Select查询语句
当前读:执行insert、Update、Delete、Select…for update、Select…lock in share mode时数据的读取方式

连续多次快照读时,ReadView会产生复用,没有幻读问题,但是当两次快照读之间存在当前读,并且当前读的操作覆盖了另外一个事务插入的“幻行”, 这就会造成该行数据的 DB_TRX_ID 被更新为当前事务 ID,此后即便进行快照读,依然会查出该行数据,产生幻读。

image.png
事务A除了select还执行update,事务B负责插入数据,于是。

image.png
由这个例子说明,一旦事务A的修改操作覆盖到了其他事务插入的“幻行”,那么在下次select的时候,也会把这行数据一起查出来

2.3 小结

MVCC的作用:MVCC是一种用来解决读 - 写冲突的无锁并发控制。在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。同时还可以解决脏读、幻读、不可重复读等事务隔离问题。
MVCC实现的关键:记录中的 3 个隐式字段、undo 日志、Read View 来实现的。