mysql的锁机制和mvcc

一、mysql 常见锁

在多节点部署或者多线程执行时,同一个时间可能有多个线程更新相同数据,产生冲突,这就是并发问题。这样的情况下会出现以下问题:

更新丢失:一个事务更新数据后,被另一个更新数据的事务覆盖。

脏读:一个事务读取另一个事物为提交的数据,即为脏读。

其次还有幻读,针对并发引入并发控制机制,即加锁。加锁的目的是在同一个时间只有一个事务在更新数据,通过锁独占数据的修改权。

1、乐观锁:

使用版本标识来确定读到的数据与提交时的数据是否一致。在每一行记录的后面增加两个隐藏列,记录创建版本号和删除版本号,而每一个事务在启动的时候,都有一个唯一的递增的版本号。乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,认为事务间竞争没有那么多,先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,如果没有就提交,如果有就放弃并重试。适用于低数据争用,写冲突比较少的环境。

2、悲观锁

就是对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要将数据锁定。悲观锁的实现,通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select … for update来实现悲观锁。

共享锁(S锁、读锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

排它锁(X锁、写锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。共享锁和排他锁都属于悲观锁。排他锁又可以可以分为行锁和表锁。

MySQL常用的两种引擎MyISAM和InnoDB,MyISAM默认使用表锁,InnoDB默认使用行锁。注意:使用InnoDB引擎,如果筛选条件里面没有索引字段,就会锁住整张表,否则的话,锁住相应的行。

3、事务的隔离级别

已提交读:一个事务因为读取到另一个事务已提交的修改数据,导致在当前事务的不同时间读取同一条数据获取的结果不一致。

可重复读:一个事物读可以读取到其他事务提交的数据,但是在RR隔离级别下,当前读取此条数据只可读取一次,在当前事务中,不论读取多少次,数据仍然是第一次读取的值,不会因为在第一次读取之后,其他事务再修改提交此数据而产生改变。因此也成为幻读,因为读出来的数据并不一定就是最新的数据。可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻读。

事务隔离性问题:如果不考虑事务的隔离性,会出现以下问题:

  • 脏读:指一个线程中的事务读取到了另外一个线程中未提交的数据。
  • 不可重复读:指一个线程中的事务读取到了另外一个线程中提交的update的数据。
  • 幻读(虚读):指一个线程中的事务读取到了另外一个线程中提交的insert的数据。在一次的事务操作中,先读取了几行数据后,另一个事务又增加或删除了数据,在此之后,此事务又去读取数据,发现数据凭空生成或消失,跟幻觉一样,即幻读。

在这里插入图片描述

在可重复读中,该sql第一次读取到数据后,就将这些数据加锁(悲观锁),其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来实现已提交读和可重复读的隔离级别。

4、事务的锁处理机制

当前读,读取的是记录的最新版本,并且会对当前记录加锁,防止其他事务发修改这条记录。行共享锁(SELECT … LOCK IN SHARE MODE )、行排他锁(SELECT … FOR UPDATE / INSERT / UPDATE / DELETE)的操作都会用到当前读。当前读的实现方式:next-key锁(行记录锁+Gap间隙锁)。

  • record lock ,记录锁, 仅仅锁住索引记录的一行
  • Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
  • Next-Key Lock:是record lock和gap lock的组合,用于锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

间隙锁(Gap Lock)是Innodb在可重复读提交下为了解决幻读问题时引入的锁机制,(下面的所有案例没有特意强调都使用可重复读隔离级别)幻读的问题存在是因为新增或者更新操作,这时如果进行范围查询的时候(加锁查询),会出现不一致的问题,这时使用不同的行锁已经没有办法满足要求,需要对一定范围内的数据进行加锁,间隙锁就是解决这类问题的。根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。

默认情况下,InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。在可重复读隔离级别下,数据库是通过行锁和间隙锁共同组成的(next-key lock),来实现的。Next-Key Lock是行锁与间隙锁的组合,这样,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。如果一个间隙被事务T1加了锁,其它事务是不能在这个间隙插入记录的。

  • SELECT … FROM … FOR UPDATE: 在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。如果没有利用到索引将锁住全表(表级的排他锁),其它事务无法进行insert/update/delete操作
  • SELECT … FROM,是一个快照读,通过读取数据库的一个快照,不会加任何锁,除非将隔离级别设置成了 串行读 。
  • UPDATE/DELETE … WHERE … :在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。如果没有利用到索引将锁住全表(表级的排他锁),其它事务无法进行其他的insert/update/delete操作。;
  • INSERT : 在插入的记录上加一把排他锁,这个锁是一个index-record lock,并不是next-key 锁,因此就没有gap 锁,他将不会阻止其他会话在该条记录之前的gap插入记录。

二、MVCC 多版本并发控制机制

快照读是基于 MVCC 和 undo log 来实现的,适用于简单 select 语句,避免了幻读。当前读是基于 临界锁(行锁 + 间歇锁)来实现的,适用于 insert,update,delete, select … for update, select … lock in share mode 语句,以及加锁了的 select 语句。

1.MVCC 背景

为了提供更好的并发,InnoDB提供了非锁定读:不需要等待访问行上的锁释放,读取行的一个快照。该方法是通过InnoDB的一个特性:MVCC来实现的。为什么需要MVCC呢?数据库通常使用锁来实现隔离性。最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据。当然快照是一种概念模型,不同的数据库可能用不同的方式来实现这种功能。

2、隔离级别

MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行(包含事务未提交的数据), 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。

快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。主要应用于无需加锁的普通查询(select)操作。快照读的意思是,数据有多个版本, 当事务并发执行时, 某一事务读取的数据来自其中一个版本(快照)。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

3、MVCC原理实现

数据库并发场景有三种,分别为:

读-读:不存在任何问题,也不需要并发控制
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。

MVCC原理:多版本并发控制。MVCC是一种并发控制的方法。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。MVCC 使大多数读操作都可以不用加锁,这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

MVCC可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

MVCC的实现: 通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。在数据库表的记录中,每一个记录都会默认添加三个字段:

  • DB_TRX_ID:6个字节,表示最近一次修改本记录的事务ID

  • DB_ROLL_PTR :7 个字节,回滚指针,指向回滚段中的 undo log record,用于找出这个记录的上个修改版本的数据。

  • DB_ROW_ID:6 个字节,一个单调递增的 ID,确定表中记录的唯一性。

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表。对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。

在这里插入图片描述

功能: MVCC可以在不加锁的情况下解决读-写冲突,并不能解决写-写冲突,写操作还是需要上锁。MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读。而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。

readView: 已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView。可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。 在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。在 READ COMMITTED 中每次查询都会生成一个实时的 ReadView,做到保证每次提交后的数据是处于当前的可见状态。而 REPEATABLE READ 中,在当前事务第一次查询时生成当前的 ReadView,并且当前的 ReadView 会一直沿用到当前事务提交,以此来保证可重复读(REPEATABLE READ)。对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。最重要的是需要判断一下版本链中的哪个版本是当前事务可见的。

在开启事务时,会将当前活跃的事务(已经开启了事务,但是还没有提交)的事务 ID 放在一个数组里面,同时记录数组里面最小的事务 ID 为「低水位」,记录当前系统已经创建的事务ID 的最大值加一为「高水位」。这三者组成了一个事务的一致性视图(read-view)。当事务要查询某个记录的数据时,实际上就是拿该记录行的TRX_ID事务ID(包括历史版本的事务ID)和这个一致性视图进行比较,直到某个版本的数据是可见的为止。

其查询过程如下:

  • 读取的记录的事务ID小于低水位,说明这个版本的数据在开启本事务前已经提交,是可见的,直接返回这个数据
  • 读取的记录的事务ID大于高水位,说明这个版本的数据在开启本事务后提交的,不可见,从记录中取出 DB_ROLL_PTR 指向的记录并读取其事务 ID,开始下一轮的判断
  • 读取的记录的事务ID介于低水位和高水位中间,此时判断事务ID是否在一致性视图的事务数组中:
    • 如果不在,说明这个版本的数据在开启本事务前已经提交,是可见的,直接返回这个数据
    • 如果在,说明这个版本的数据是由开启事务后的其他活跃事务提交的,对本事务是不可见的,因此需要从记录中取出 DB_ROLL_PTR 指向的记录并读取其事务 ID,开始下一轮的判断。
      在这里插入图片描述

三、MVCC和幻读

MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log。

幻读是指多事务并发中一个事务读到了另一个事务insert的记录。在REPEATABLE READ隔离级别下,假设事务T1执行后,事务T2开始执行,并新增一条记录,然后事务T2提交,这时在事务T1中执行select是看不到事务T2新增的这条记录的。因为在事务T1生成readview的时刻,事务T2属于未来事务,所以是看不到事务T2新增的这条记录的。

# 事务T1,REPEATABLE READ隔离级别下
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM hero WHERE number = 30;
Empty set (0.01 sec)

# 此时事务T2执行了:INSERT INTO hero VALUES(30, '关羽', '魏'); 并提交

mysql> UPDATE hero SET country = '蜀' WHERE number = 30;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM hero WHERE number = 30;
+--------+---------+---------+
| number | name    | country |
+--------+---------+---------+
|     30 | 关羽   | 蜀      |
+--------+---------+---------+
1 row in set (0.01 sec)

REPEATABLE READ隔离级别下,T1第一次执行普通的SELECT语句时生成了一个ReadView,之后T2向表中新插入了一条记录便提交了,ReadView并不能阻止T1执行UPDATE或者DELETE语句来对改动这个新插入的记录(因为T2已经提交,改动该记录并不会造成阻塞),但是这样一来这条新记录的trx_id隐藏列就变成了T1的事务id,之后T1中再使用普通的SELECT语句去查询这条记录时就可以看到这条记录了,也就把这条记录返回给客户端了。因为这个特殊现象的存在,你也可以认为InnoDB中的MVCC并不能完完全全的禁止幻读。


四、锁和索引的关系

如果在事务中执行了一条不走索引的语句,执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。

即使在建表的时候没有指定主键,InnoDB会默认创建一个DB_ROW_ID的自增字段为表的主键,并且其主键索引(聚簇索引)为GEN_CLUST_INDEX。主键索引也被称为聚簇索引。

InnoDB的行锁是通过给索引上的索引项加锁来实现的,并不是直接对记录行加锁,而是对行对应的索引加锁:

  • 如果sql 语句操作了主键索引,Mysql 就会锁定这条主键索引。
  • 如果sql语句操作了非主键索引的唯一索引:MySQL会先锁定该非主键索引,再锁定相关的主键索引。 即如果当前id为唯一索引,name为主键索引,在进行索引查找时需要两个步骤,第一步,查询到索引id对应的主键,第二步,根据主键查询到数据库信息,那么这个加锁过程就需要在第一步锁定的数据和第二步锁定的数据分别加上排它锁。
  • 如果sql语句操作了非主键索引的普通索引(非唯一索引):根据InnerDB存储引擎的实现规则,同样在查询时,需要先找到id对应的主键,加锁,然后找到主键对应的行数据,再次加锁,不同的是,由于id非唯一索引,那么这两步加锁的数据可能不止一行,有可能是多行。
  • 在InnoDB中,如果SQL语句不涉及索引,则会通过隐藏的聚簇索引来对记录加锁。对聚簇索引加锁,实际效果跟表锁一样,因为找到某一条记录就得扫描全表,要扫描全表,就得锁定表。

五、事务死锁处理

概念:死锁是指两个或两个以上事务在执行过程中因争抢锁资源而造成的互相等待的现象。事务增删语句的时候加锁,COMMIT后释放。

成因: 在这里插入图片描述
1、不同表之间相同记录行锁冲突:事务A和事务B操作两张表,但出现循环等待锁情况。

2、相同表记录行锁冲突:在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而 jobB处理的id列表为[8,9,10,4,2],这样就造成了死锁。
在这里插入图片描述

3、有两个事务A、B开始都在查询表T,所以都获得了资源上的S锁。这时其中一个事务想要修改数据,所以想将S锁上升为X锁,而这时又有事务B在共享S锁。所以事务A一直等待B释放S锁之后,再上升。但不巧的是,事务B并没有释放锁,而是同样的也想修改数据,将S锁上升为X锁,但是事务A也在共享S锁,事务B就进入等待状态。这时A、B事务都在等待对方释放共享锁,发生了死锁。可通过mvcc乐观锁解决。

检测死锁现象:

  • 因为死锁被检测到后会回滚,这些信息都会以异常反应在应用的业务日志中,通过这些业务日志我们可以定位到相应的代码。
  • 查看死锁数据库日志,查看死锁日志的命令show engine innodb status;

如何尽可能避免死锁

  • 以固定的顺序访问表和行。比如对两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;又比如将两个事务的sql顺序调整为一致,也能避免死锁。

  • 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。

  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。

  • 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。

  • 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。


六、innodb如何解决幻读

InnoDB有三种行锁的算法:

1,Record Lock:单个行记录上的锁。

2,Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。

3,Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

在读已提交的情况下,即使采用了 MVCC 方式也会出现幻读。InnoDB 是采用 Next-key 锁机制,解决幻读。Next-Key锁住一个范围,同时锁住记录本身。InnoDB对于行的查询都是采用了Next-Key Lock的算法,锁定的不是单个值,而是一个范围(GAP)。锁定行前后的gap, 左开右闭,锁定的其实是索引位置,而不是索引值

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mysql锁机制是用来处理并发访问数据库时的问题,特别是在使用InnoDB引擎支持事务的情况下。锁机制可以按照的粒度分为表级和行级。表级是对整张表进行加,实现简单,消耗的资源较少,加快速,不容易出现死。而行级则是对当前操作的行进行加定粒度更小,可以提高并发性,但加的代价较高。 MySQL的InnoDB存储引擎默认的事务隔离级别是RR(可重复读),这是通过行级和多版本并发控制(MVCC)一起实现的。在正常读取数据时,不会加,而在写入数据时才会进行加操作。 MVCC是通过一些技术实现的,包括隐藏字段、Read View和Undo log。隐藏字段用于存储数据版本信息,Read View用于控制事务的隔离级别,而Undo log则用于记录事务对数据的修改操作,以便在需要回滚时进行恢复。 总结起来,Mysql锁机制包括表级和行级,用于处理并发访问数据库时的问题。而MVCC则是InnoDB存储引擎实现事务隔离级别的一种机制,通过隐藏字段、Read View和Undo log来实现数据的一致性和并发控制。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Mysql锁机制+MVCC](https://blog.csdn.net/qq_45901741/article/details/120245265)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [MySQL和事务](https://download.csdn.net/download/weixin_38739919/13683140)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [mysql锁机制mvcc](https://blog.csdn.net/u014618114/article/details/115534734)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值