1.事务概述

1.1 事务基本概念

请参考我之前写的博客:【MySQL基础】事务

  • 幻读:一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。专指新插入的行

1.2 SQL标准的事务隔离级别

  • 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交(read committed):一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的
  • 串行化(serializable ):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

1.3 MySQL中的两个视图

  • view:一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view … ,而它的查询方法与表一样。
  • consistent read view:InnoDB在实现MVCC时用到的一致性读视图,用于支持RC和RR隔离级别的实现。没有物理结构,作用是事务执行期间用来定义“能看到什么数据”。

1.4 MySQL事务隔离的实现

MySQL开启事务时会创建一个视图,访问的时候以视图的逻辑结果为准。

  • 可重复读隔离级别:视图是在事务启动时创建的,整个事务存在期间都用这个视图
  • 读提交隔离级别:视图是在每个SQL语句开始执行的时候创建的
  • 读未提交隔离级别:直接返回记录上的最新值,没有视图概念;
  • 串行化隔离级别:直接用加锁的方式来避免并行访问。

1.5 刷脏

  • InnoDB引擎中的数据是存放在磁盘中的,但为了提交IO效率,引入了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲
  • 下图是buffer pool的流程图
  • 当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;
  • 当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)
  • 可能存在的问题
  • 预读失效:由于预读(Read-Ahead),提前把页放入了缓冲池,但最终MySQL并没有从页中读取数据,称为预读失效
  • 缓冲池污染:当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染。解决办法:加入老生代停留时间窗口策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。或使用LFU算法。

2.事务的基础-MVCC

2.1 MVCC概述

  1. MVCC(多版本并发控制)定义:在查询某条记录的时候,不同时刻启动的事务会有不同的read-view,同一条记录在系统中可以存在多个版本
  2. 在MySQL中,每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除,即当系统里没有比这个回滚日志更早的read-view
  3. MySQL的隐藏列
  • row_id:行记录的唯一标志,如果有主键就没有该列
  • transaction_id:唯一的事务ID
  • roll_pointer:回滚指针,指向该记录对应的undo log
  • 标志位:每条记录的头信息中会存储一个标志位,标志该记录是否删除。

2.2 MVCC相关名词

  • transaction id:InnoDB每个事务的唯一事务ID,在事务开始时向InnoDB的事务系统申请,按申请顺序严格递增。
  • up_limit_id:事务在启动时,找到已提交的最大事务ID记为up_limit_id。
  • 数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。
  • 一个记录被多个事务连续更新后的状态,图中的三个虚线箭头就是undo log;V1、V2、V3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。
  • 事务在更新一条语句时,比如id=1改为了id=2.会把id=1和该行之前的row trx_id写到undo log里,并且在数据页上把id的值改为2,并且把修改这条语句的transaction id记在该行行头

2.3 事务中的记录是否可见

  • 活跃的所有事务ID: InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,活跃指的是启动了但还没提交。数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。
  • 一个事务要查看一条数据时,必须先用该事务的up_limit_id与该行的transaction id做比对,如果up_limit_id>=transaction id,那么可以看.如果up_limit_id<transaction id,则只能去undo log里去取。去undo log查找数据的时候,也需要做比对,必须up_limit_id>transaction id,才返回数据
  • 一个数据版本的row trx_id有几种可能(当前事务的启动瞬间)
  • 绿色可见:已提交的事务或者是当前事务自己生成的
  • 红色不可见:将来启动的事务生成的
  • 黄色:若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见; 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。

2.4 小结

  • InnoDB利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。MVCC读不加锁,因此读写不冲突,并发性能好。
  • 更新逻辑:更新数据都是先读后写的,而这个读,只能读当前的值,称为当前读(update语句或select语句加锁)
  • 事务的可重复读:核心是一致性读,事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。.当前读会更新事务内的up_limit_id为该事务的transaction id
  • Innodb支持RC和RR隔离级别实现是用的一致性视图(consistent read view),
  • RR与RC的区别:
  • RR(可重复读):只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图。查询只承认在事务启动前就已经提交完成的数据;
  • RC(读提交):每一个语句执行前都会重新算出一个新的视图。查询只承认在语句启动前就已经提交完成的数据;
  • RR能实现可重复读而RC不能的原因:
  • 快照读的情况下,rr不能更新事务内的up_limit_id,而rc每次会把up_limit_id更新为快照读之前最新已提交事务的transaction id,则rc不能可重复读
  • 当前读的情况下,rr是利用record lock+gap lock来实现的,而rc没有gap,所以rc不能可重复读

3. 事务原理剖析

3.1 事务的提交方式与启动时机

  1. 提交事务的方式
  • 显式启动事务语句, begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。缺点:多一次语句交互
  • set autocommit=0,这个事务持续存在直到你主动执行commit 或 rollback 语句,或者断开连接。优点:省去了再次执行begin语句的开销。从程序开发的角度明确地知道每个语句是否处于事务中。
  1. 事务的启动时机
  • begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句(第一个快照读语句),事务才真正启动。
  • start transaction with consistent snapshot 命令立即启动一个事务。

3.2 事务实现原理

  • MySQL中的ACID实现原理
  • A(Atomicity原子性):undo log(回滚日志)
  • C(Consistency一致性):AID都是为了保证数据库状态的一致性,既需要数据库层面的保障,也需要应用层面的保障。
  • I(Isolation隔离性):写与写的事务通过锁实现,写与读的事务是MVCC
  • D(Durability持久性):redo log(重做日志)
  • MySQL在当前读时通过next key lock(间隙锁和行锁的合称)解决幻读,而快照读时通过MVCC解决幻读

3.3 事务示例

# 建表SQL
drop table if exists `t`;
create table `t` (
    `id` int(11) unsigned NOT NULL DEFAULT 0,
    `k` int(11) unsigned NOT NULL DEFAULT 0,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

# 插入数据
insert into t(id, k) values(1, 1), (2, 2);

下图是同一条select语句在不同事务中查出不同结果的示例:

mysql未读取idb库 mysql读未提交实现原理_MySQL


上图示例中事务B查到的k的值是3,而事务A查到的k的值是1。虽然期间这一行数据被修改过,但是事务A不论在什么时候查询,看到这行数据的结果都是一致的,即一致性读。具体分析如下:

  • (1,3)还没提交(版本未提交)不可见
  • (1,2)虽然提交了,但是是在视图数组创建之后提交的(版本已提交,但是是在视图创建后提交的)不可见
  • (1,1)是在视图数组创建之前提交的(版本已提交,而且是在视图创建前提交的)可见

4.使用事务时的注意事项

4.1 尽量不使用长事务

  • 长事务意味着系统里面会存在很老的事务视图。
  • 由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

4.2 乐观锁

  • 基于version字段对row进行cas式的更新,每次cas更新不管成功失败,结束当前事务。而判断是否成功的标准是 affected_rows 是不是等于预期值。
  • 业务中一般是匹配唯一主键,所以预期值一般是1。