MySql的事务和锁详解

目录列表

  • 1、什么是事务
  • 2、 事务的特性
    • 2.1 A (Atomicity) 原子性
    • 2.2 C (Consistency) 一致性
    • 2.3 I(Isolation)隔离性
    • 2.4 D (Durability) 持久性
  • 3、事务的实现
    • 3.1 事务日志
    • 3.2 redo log 实现持久性和原子性
    • 3.3 undo log 实现一致性
    • 3.4 Mysql的日志类型
    • 3.5 事务操作步骤
  • 4、事务并发
    • 4.1 事务并发引发的问题
    • 4.2 事务隔离级别
  • 5、Mysql的锁
    • 5.1 按锁的粒度分类:全局锁、表级锁、页级锁、行级锁
      • 5.1.1 全局锁
      • 5.1.2 表级锁
      • 5.1.3 页级锁
      • 5.1.4 行级锁
    • 5.2 按属性分类:共享锁、排他锁
      • 5.2.1 共享锁
      • 5.2.2 排他锁
    • 5.3 按加锁方式分类:自动锁、显示锁
      • 5.3.1 隐式加锁(自动锁)
      • 5.3.2 显示加锁
    • 5.4 按照算法分类: 间隙锁、临键锁、记录锁
      • 5.4.1 间隙锁 Gap lock
      • 5.4.2 临键锁 Next-key lock
      • 5.4.3 记录锁 Record lock
    • 5.5 按照模式分类: 悲观锁、乐观锁
      • 5.5.1 悲观锁
      • 5.5.2 乐观锁
    • 5.6 死锁
      • 5.6.1 如何避免死锁
  • 6、MySql的MVCC
    • 6.1 什么是MVCC
    • 6.2 当前读和快照读
      • 6.2.1 当前读
      • 6.2.2 快照读
    • 6.3 MVCC解决了什么问题
    • 6.4 MVCC实现原理
      • 6.4.1 隐式字段
      • 6.4.2 undo log
      • 6.4.3 purge线程
  • 7、MySql 分布式事务
    • 7.1 2PC
    • 7.2 3PC


1、什么是事务

数据库中的事务是指对数据库执行一批操作,在同一个事务当中,这些操作最终要么全部执行成功,要么全部失败,不会存在部分成功的情况。在MySQL中,只有使用InnoDB引擎的数据库或表才支持事务。

  • 事务是一个原子操作。是一个最小执行单元。可以由一个或多个SQL语句组成
  • 在同一个事务当中,所有的SQL语句都成功执行时,整 个事务成功,有一个SQL语句执行失败,整个事务都执行失败。

举个例子:
比如A用户给B用户转账100操作,过程如下:

  1. 从A账户扣100
  2. 给B账户加100

如果在事务的支持下,上面最终只有2种结果:

  1. 操作成功:A账户减少100;B账户增加100
  2. 操作失败:A、B两个账户都没有发生变化

如果没有事务的支持,可能出现错:A账户减少了100,此时系统挂了,导致B账户没有加上100,而A账户凭空少了100。

2、 事务的特性

事务特性: 原子性、一致性、隔离性、持久性, 通常简称为事务的ACID属性。

2.1 A (Atomicity) 原子性

整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样

2.2 C (Consistency) 一致性

在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏

2.3 I(Isolation)隔离性

一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰

2.4 D (Durability) 持久性

在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,不会被回滚

3、事务的实现

事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。MySQL 中支持事务的存储引擎有 InnoDB。
事务的实现就是如何实现ACID特性。

  • 事务的隔离性是通过锁实现
  • 事务的原子性、一致性和持久性则是通过事务日志实现

3.1 事务日志

事务日志包括:重做日志redo log和回滚日志undo log.
两种日志均可以视为一种恢复操作

  • 作用不同,redo_log是恢复提交事务修改的操作,而undo_log是回滚行记录到特定版本。
  • 记录的内容也不同,redo_log是物理日志,记录的是物理修改操作,而undo_log是逻辑日志,根据每行记录进行记录

3.2 redo log 实现持久性和原子性

在innoDB引擎中,事务日志通过重做(redo)日志和innoDB存储引擎的日志缓冲(InnoDB Log Buffer)实现原子性和持久性。

  • 事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的“日志先行”(Write-Ahead Logging)。
  • 当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务可以继续提交,也可以选择回滚,这基于恢复的策略而定。
  • 在系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。所有的事务共享redo log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起。

3.3 undo log 实现一致性

undo log 主要为事务的回滚服务

  • 在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。
  • undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
  • Undo记录的是已部分完成并且写入硬盘的未完成的事务,默认情况下回滚日志是记录在表空间中的(共享表空间或者独享表空间)

3.4 Mysql的日志类型

  • 错误日志:记录出错信息,也记录一些警告信息或者正确的信息。error log
  • 查询日志:记录所有对数据库请求的信息,不论这些请求是否得到了正确的执行。
  • 慢查询日志:设置一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询的日志文件中。
  • 二进制日志binlog:记录对数据库执行更改的所有操作。 主从依赖binlog和relay-log
  • 中继日志relay log:中继日志也是二进制日志,用来给slave 库恢复
  • 事务日志:重做日志redo log和回滚日志undo log

3.5 事务操作步骤

  1. START TRANSACTION(或BEGIN): 开始一个事务。所有在该语句之后执行的语句都将视为该事务的一部分。
  2. COMMIT: 提交事务。如果成功,则所有修改将成为永久性的。如果提交失败,则事务将回滚到其开始状态。
  3. ROLLBACK: 撤消事务中进行的所有修改,并将数据库恢复到事务开始时的状态。

4、事务并发

4.1 事务并发引发的问题

MySql的事务并发时是通过锁来实现事务的隔离性。

  1. 更新丢失(Lost Update)
    事务A和事务B选择同一行,基于最初选定的值更新该行时,由于两个事务都不知道彼此的存在,就会发生丢失更新问题.
  2. 脏读
    事务A读取了事务B更新但还没有提交的数据,然后B回滚操作,那么A读到的就是脏数据
  3. 不可重复读
    事务A多次读取同一条数据,事务B在事务A读取的过程中对数据做了更新并提交,导致事务A多次读取到的同一条数据结果是不一样,重点是事务过程中其他事务对之前读取到的数据进行了update操作
  4. 幻读
    事务A读取某个范围的记录时,事物B在该范围插入了新事物,事物A在随后的查询中,就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。重点是事务A过程中其他事务对读取到的范围数据进行了INSERT或者DELETE操作

4.2 事务隔离级别

MySql事务隔离级别

  1. 读未提交:
    事务B修改了数据,但尚未提交,事务A中的SELECT会读取到这些未提交的数据, 可能会导致脏读、幻读或不可重复读。
  2. 读已提交:
    事务A多次读取同一条数据,事务B在A的多次读取过程中,对数据做了更新并提交,导致事务A多次读取同一条数据时的结果不一样,出现不可重复读问题.可以阻止脏读,但是幻读或不可重复读仍有可能发生
  3. 可重复读:
    同一事务里,多次查询同一条数据的结果都是事务开始时的状态,都是一样的,此时可以阻止脏读和不可重复读,但幻读仍有可能发生。Mysql 的 InnoDB存储引擎通过MVCC(多版本控制)解决了幻读的问题,准确来说是通过间隙锁解决了幻读的问题。MySql默认的隔离级别是可重复读
  4. 可串行化
    最高的级别级别,在这个级别下,不会产生任何异常,并发的事务都是一个个按照顺序执行,这样事务之间就完全不可能产生干扰.该级别可以防止脏读、不可重复读以及幻读

5、Mysql的锁

在前面,我们了解了数据库事务和各种事务隔离级别,在并发的情况下,数据库是通过锁的机制实现隔离级别。
Mysql锁分类

5.1 按锁的粒度分类:全局锁、表级锁、页级锁、行级锁

5.1.1 全局锁

全局锁是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的MDL、DDL语句、更新操作的事务提交语句都将被阻塞。其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

通过以下SQL 可以添加全局锁: FLUSH TABLES WITH READ LOCK;
可以通过以下SQL 释放全局锁: UNLOCK TABLES;

5.1.2 表级锁

表级锁会对当前操作的整张表加锁,最常使用的 MyISAM 与 InnoDB 都支持表级锁定。MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
添加表锁,格式如下: LOCK TABLES xxx READ/WRITE;
清除表锁,格式如下: UNLOCK tables;

  • 以下方式表示,对表添加只读锁,表加上了表读锁之后,本线程和其他线程都可以读数据,本事务写数据会报错,其他事务写数据会阻塞。 LOCK TABLES account_tbl READ;
  • 以下方式表示,对表添加只写锁,表加上了表写锁之后,当前线程的读和写操作正常;其他线程的读写操作都会被阻塞。LOCK TABLES account_tbl WRITE;
    在这里插入图片描述
  • 元数据锁(Metadata Locking,简称:MDL锁),MySQL5.5版本引入了MDL锁,用于解决或者保证DDL操作与DML操作之间的一致性。当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL写锁。开启事务之后,进行查询操作时,对字段进行更新的操作会一直处理阻塞状态。

5.1.3 页级锁

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 引擎支持页级锁。

5.1.4 行级锁

行级锁是粒度最低的锁,发生锁冲突的概率也最低、并发度最高。但是加锁慢、开销大,容易发生死锁现象。MySQL中只有InnoDB支持行级锁,行级锁可分为共享锁和排他锁。
在MySQL中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。 在UPDATE、DELETE操作时,MySQL不仅锁定WHERE条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key locking。

注意事项:

  • 在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。
  • 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以即使是访问不同行的记录,如果使用了相同的索引键,也是会出现锁冲突的。
  • 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
  • 即便在条件中使用了索引字段,但具体是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

5.2 按属性分类:共享锁、排他锁

5.2.1 共享锁

共享锁,又称之为读锁,简称S锁
共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。只有当数据上的读锁被释放后,其他事务才能对其添加写锁。共享锁主要是为了支持并发的读取数据而出现的,读取数据时,不允许其他事务对当前数据进行修改操作,从而避免”不可重读”的问题的出现。
可以通过以下SQL 添加共享锁: SELECT * FROM TABLE WHERE ... LOCK IN SHARE MODE
共享锁事例
其他事务修改时,则会进入阻塞,但是是可以正常读取数据的,而且其他事务可以继续添加共享锁;但是在共享锁上再添加排它锁时,会进入阻塞,直到共享锁释放,或等待锁超时。

5.2.2 排他锁

排它锁,又称之为写锁、独占锁,简称X锁
排他锁锁住一行数据后,其他事务不能再在其上加其他的锁。当事务对数据加上写锁后,其他事务既不能对该数据添加读锁,也不能对该数据添加写锁,写锁与其他锁都是互斥的。只有当前数据写锁被释放后,其他事务才能对其添加写锁或者是读锁。写锁主要是为了解决在修改数据时,不允许其他事务对当前数据进行修改和读取操作,从而可以有效避免”脏读”问题的产生。

  • mysql InnoDB引擎默认的修改数据语句(update,delete,insert),都会自动给涉及到的数据加上排他锁
  • select语句默认不会加任何锁类型
  • 可以显式地加排他锁,使用select …for update语句。

加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

5.3 按加锁方式分类:自动锁、显示锁

5.3.1 隐式加锁(自动锁)

InnoDB自动加意向锁。
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
对于普通SELECT语句,InnoDB不会加任何锁;

5.3.2 显示加锁

共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
排他锁(X) :SELECT * FROM table_name WHERE … FOR UPDATE
按照算法分类

5.4 按照算法分类: 间隙锁、临键锁、记录锁

5.4.1 间隙锁 Gap lock

间隙锁基于非唯一索引,它锁定一段范围内的索引记录。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。

select * from goods where id between 1 and 10 for update;

即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。

5.4.2 临键锁 Next-key lock

  • 临键锁 Next-key lock:record+gap 锁定一个范围,包含记录本身,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间,是一个左开右闭区间
  • Gap lock是InnoDB在Repeatable Read级别下为了解决幻读问题时引入的锁机制。
  • 临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
  • 非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
  • InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。

如何防止间隙锁

尽量采用乐观锁,乐观锁是在php等代码层面的锁,就不会锁住数据库资源
事务中update,where后面的字段尽量带上索引,不然间隙锁的范围很大(最好是主键或者唯一索引)
尽量不要出现长事务,否则事务中更新订单时间隙锁会被锁很久,另一事务插入订单就会执行很久;
update订单表,begin和commit之间的时间不要太长,之间不要写一些慢代码,比如请求第三方接口等

5.4.3 记录锁 Record lock

记录锁是封锁记录,记录锁也叫行锁,例如: select *from goods where id=1 for update; ,它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。

5.5 按照模式分类: 悲观锁、乐观锁

5.5.1 悲观锁

概念:
悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的。

特点
可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁,然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高,还有增加产生死锁的风险。

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下

  • 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。
  • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  • 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

拿比较常用的 MySql Innodb 引擎举例,来说明一下在 SQL 中如何使用悲观锁。
要使用悲观锁,必须关闭 MySQL 数据库的自动提交属性。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。(sql语句:set autocommit=0)

以电商下单扣减库存的过程说明一下悲观锁的使用:
悲观所使用示意
以上,在对id = 1的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。
如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。

上面提到,使用 select…for update 会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

5.5.2 乐观锁

概念
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

一般实现机制:
一般通过版本号机制实现乐观锁,在数据表中加上版本号字段 version,表示数据被修改的次数。当数据被修改时,这个字段值会加1。举个简单的例子:假设帐户信息表中有一个 version 字段,当前值为 1 ,而当前帐户的余额( balance )为 100 。

操作员 A 此时准备将其读出( version=1 ),并从其帐户余额中扣除 50( 100-50 );
操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 20 ( 100-20 );
操作员 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50 ),提交到数据库完成更新;
操作员 B 完成了操作,也将版本号加1( version=2 )试图向数据库提交数据( balance=80 ),但此时比对数据库记录版本发现,操作员 B 提交的数据版本号为 2 ,数据库记录的当前版本也为 2 ,不满足“提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。

因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改,最终造成覆盖操作员 A 操作结果的可能。

5.6 死锁

概念:
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如图:
死锁示意图
如图,事务A已经锁定了数据a,需要去操作数据b,但是此时事务B已经锁定了数据b,相反事务B想去操作数据a也被事务A锁定了,事务A和B都在等待对方释放自己想要的数据,于是就一直等下去了(mysql默认锁超时时间60s)

5.6.1 如何避免死锁

  • 操作完之后立即提交事务,特别是在交互式命令行中。比如中途不要去请求第三方接口或者执行一些慢逻辑,能不放到事务里面的php代码, 就别放到事务里面
  • 资源一次性分配: 一次性锁协议,事务开始时,即一次性申请所有的锁,之后不会再申请任何锁,如果其中某个锁不可用,则整个申请就不成功,事务就不会执行,在事务尾端,一次性释放所有的锁。不足是并发度不高。
  • 尽量采用乐观锁,因为悲观锁都是要求mysql锁资源,而乐观锁不是
  • 采用超时设置,时间越短,锁等待时间越短。到点就会自动超时,不会继续等待锁释放
	mysql 锁超时设置:
	  SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
	  SET innodb_lock_wait_timeout=10;
  • 保证没有长事务,长事务尽量拆分成短事务,因为2个短事务很难重合到一起。就是说,如果一个事务瞬间执行完毕了,就很好. 如果一直没执行完毕, 就很可能另外一个事务冲进来二者重合,就加大死锁几率
  • 修改多个表或者多个行的时候,将修改的顺序保持一致。死锁是因为锁定资源的顺序刚好相反。如果顺序是一样的,就不会产生死锁。
  • 创建索引,可以使创建的锁关联到的数据更少(主键或者唯一索引最佳)。如果where后面的字段没有索引,哪怕只操作一行数据, 也会锁整张表, 因为锁是基于索引的。

6、MySql的MVCC

6.1 什么是MVCC

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读.

总结: MVCC是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。

6.2 当前读和快照读

在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读

6.2.1 当前读

  • 像select 语句 lock in share mode(共享锁), select 语句 for update ; update, insert ,delete(排他锁)这些操作都是一种当前读
  • 为什么叫当前读?因为读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

6.2.2 快照读

  • 像不加锁的select * from 操作就是快照读,即不加锁的非阻塞读,不涉及其他锁之间的冲突;
  • 快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;
  • 快照读是基于提高并发性能的考虑;
  • 快照读的实现是基于多版本并发控制,即MVCC,它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

6.3 MVCC解决了什么问题

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

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

多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。

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

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

6.4 MVCC实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突。
它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的
所以我们先来看看这个三个point的概念:

6.4.1 隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的字段

  1. DB_TRX_ID 6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  2. DB_ROLL_PTR 7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
  3. DB_ROW_ID 6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引

在这里插入图片描述如上图:
DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键
DB_TRX_ID是当前操作该记录的事务ID,
而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

6.4.2 undo log

undo log日志主要分为两种:

  1. insert undo log
    代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  2. update undo log
    事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读(select,当读的过程中有写的事务开始和提交,会造成读数据的脏读、不可重复读、幻读等)时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。

6.4.3 purge线程

从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下

  • 比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
    在这里插入图片描述
  • 现在来了一个事务1对该记录的name做出了修改,改为Tom,在事务1修改该行(记录)数据时,数据库会先对该行加排他锁然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本,拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它. 事务提交后,释放锁
    在这里插入图片描述
  • 又来了个事务2修改person表的同一个记录,将age修改为30岁在事务2修改该行数据时,数据库也先为该行加锁,然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录.事务提交,释放锁
    在这里插入图片描述
    从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既事务链,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。

7、MySql 分布式事务

7.1 2PC

2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段

7.2 3PC

3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit

对于Mysql的分布式事务此处简单提及,后续会在专门的分布式事务中详细介绍说明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小完美世界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值