mysql重学(三)事务--隔离性

2023-06-20  本文已影响0人  温岭夹糕

写在开头

本文是记录对林晓斌老师的《mysql45讲》的阅读,并整理成自己能更通俗理解的阅读笔记

前文

  1. mysql语句执行流程
  2. innodb的二阶段日志

1.事务的特性

用4个单词概括就是ACID:

本文主要研究隔离性,其他三个都是靠日志实现的


2隔离等级

mysql执行以下命令可以查看当前环境隔离等级

SELECT @@tx_isolation;
show variables like 'transaction_isolation';

2.1读未提交(read-uncommitted)

set global transaction_isolation=read-commit;

这里我使用的是林晓斌老师MYSQL实战45讲里的实验流程,表结构体为

CREATE TABLE IF NOT EXISTS `t` (
  id int primary key auto_increment,
  c  int default 0
)

INSERT INTO `t` (`c`) values (100);
image.png

go实验代码

    dns := "user:psd@tcp(ip:3306)/goitem"
    db, err := gorm.Open(mysql.Open(dns), &gorm.Config{})
    if err != nil {
        t.Fatal(err)
    }

    var wg sync.WaitGroup
    db.Save(&T{ID: 1, C: 100})
    wg.Add(2)
    go func() {
        defer wg.Done()
        tx := db.Begin()
        var res T
        //启动事务,查询得到值1
        tx.First(&res)
        t.Log("事务A,值1:", res.C)
        time.Sleep(time.Second)
        tx.First(&res)
        t.Log("事务A,V1:", res.C)
        time.Sleep(time.Second * 2)
        tx.First(&res)
        t.Log("事务A,V2:", res.C)
        tx.Commit()
        db.First(&res)
        t.Log("事务A,V3:", res.C)
    }()
    go func() {
        defer wg.Done()
        tx := db.Begin()
        var res T
        time.Sleep(time.Second)
        tx.First(&res)
        t.Log("事务B,值1:", res.C)
        res.C = 200
        tx.Save(&res)
        time.Sleep(time.Second * 2)
        tx.Commit()
    }()
    wg.Wait()

实验发现事务B中所做的改动(不论是否已经提交)竟然能完全被事务A所看到。


image.png

这就引发了一系列的问题:

  1. 脏读。读到了其他事务未提交的数据,如上V1=200
  2. 不可重复读。读到了其他事务修改后并提交的数据,如上V2=200
  3. 幻读。事务A前后两次读取同一范围的数据,因为事务B的原因导致A读取到了前一次未读取的数据。幻读和不可重复读类似,只是指的是范围读取,上面例子是读取单条未体现,但是读未提交也存在幻读问题,本次暂时不讨论幻读的问题。

综上,读未提交是隔离性最差的等级

2.2读已提交

set global transaction_isolation='read-committed';

还是执行上面代码(注意go_test会缓存上次结果,记得随便修改下测试用例的数据就行)


image.png

read committed,顾名思义,即读到了其他事务已提交的数据V2=200 (这里我们就想到了GO关于锁学习的二次检查double-check机制的重要性),脏读是解决了,但是不可重复读还在

2.3可重复读

set global transaction_isolation='repeatable-read'

可重复读要求事务执行期间看到数据前后一致


image.png

只有v3=200
脏读和不可重复读问题都不存在

2.3串行化

set global transaction_isolation='serializable'

它其实是更像go的RWMUTX读写锁,当读写锁冲突时,后访问的事务必须等前一个事务执行完成才能继续执行,也就是1->2的时候被锁住,直到A提交后B才能继续执行,这里需要修改事务A协程的部分代码,在查询V3前需要加上B的休眠时间

time.Sleep(time.Second*5)
db.First(&res)

结果就不用说了 v1=v2=100,v3=200
综上,串行化是隔离性最强的,也是性能最差的。

3.隔离的原理

事务的隔离性是依靠视图来实现的。关于视图,MySQL有两个概念:

  1. view,即查询语句过程中定义的虚拟表
  2. 就是用于上面的,相当于redis和虚拟机的快照,给数据备份一份,因此也叫数据版本

3.1视图

当一个记录k=1被多个事务连续更新时,视图(这里指数据版本)的改变如下:

  1. 首先,事务是innodb引擎独有的,每个事务有唯一的ID,trx_id,按顺序严格递增。
  2. 每次事务更新都会记录回滚日志,生成一个新的数据版本
  3. 旧的数据版本要保留,并在新的版本中能拿到()
    通过链表的方式

总结就是更新数据的事务才会生成数据版本,版本中包含本次事务数据的值、事务trx_id、还有一个指向上一个数据版本的引用。同时表中的一行记录可能有多个版本,每个版本都有自己的事务id


image.png

图中的三个虚拟箭头U3->U2->u1 就是undolog

我们在进行数据更新操作的时候,不仅会记录redo log,还会记录undo log,如果因为某些原因导致事务回滚,那么这个时候MySQL就要执行回滚(rollback)操作,利用undo log将数据恢复到事务开始之前的状态。

3.2如何实现隔离级别-可重复读

隔离级别的实现实际上就是规定如何看数据版本。
思路分析:

  1. 事务有唯一的trx_id,且严格递增。
  2. 数据的数据版本包含事务的trx_id,更新数据会在此数据版本上生成链表结构
  3. 可重读的特点是看不到其他事务的更新,也就是只认trx_id小于本次事务id的数据版本(数据版本可以往回找),当然自己本次事务生成的数据版本也是要认的

也就是说read-reapetable是在事务开启时生成一致性视图(下文会提到),之后的事务的操作都是共用这一个视图。那举一反三,读未提交就是啥版本的数据都认(不需要生成一致性视图),读已提交就是只认链表末端的数据版本(每一个语句执行都会创建一致性视图),是不是这个道理?

实际上代码层面的实现:

  1. 事务启动时构造一个数组,数组保存的是当前“活跃”的事务,即启动了但未提交
  2. 数组中事务TRX_ID的最小值标记为低水位--最先启动未提交的事务,那么小于低水位的就是已经提交的事务
  3. 最大事务TRX_ID+1为高水位,+1的高水位就意味着这以上都是还没开始的事务
  4. 这个数组就代表了当前事务的一致性视图read-view(下图的黄色区域就是该数组)


    image.png
  5. 下面情况都是基于可重复读讨论的,按照它的定义,如果一个数据版本的trx_id:

3.3一致性视图的高水位为啥不一定是事务本身的id

  1. 一致性视图(上面的数组)是在执行第一个快照“读语句”时创建的
  2. 事务开始时就会有trx_id,判断事务开启条件的一种是begin并且执行第一个操作。所以当begin后第一句只执行非select语句会生成id(举例本次id=88),但不会产生一致性视图,因此在后面执行select语句时,其他事务可能已经创建,即存在着比当前事务还大且未提交的事务ID[72,88,90,91]
begin
  update A -----产生事务id88
  update B  ---- .....中间过程有许多事务提交和产生
  select C ---产生一致性视图[72,88,90,91]
commit
  1. 像上面的更新数据update A都是先读后写,而这个读是当前都--只能读当前数据版本的数据值,否则本次更新会被丢失

3.4当前读

我们使用该命令开启事务(立马生成read-view),这个语句只在可重复读级别以上才有意义

start transaction with consistent snapshot
image.png

当前读是基于锁实现的(当前事务加锁读,若遇到其他事务持有该数据写锁,则等待,最终得到其他事务修改后提交的最新值),除了更新语句select加锁也是当前读

select * from T lock in share mode;
image.png

也就是说如果事务C不提交,事务B的k+1会被卡住,提交后,记录的版本变成了id=101,get k =3,因此出现了可重复读实现读已提交的效果。

复习下上面的知识,若在此之前有个事务A=100未提交呢? image.png

A的get K = 1,因为不是当前读,只能读小于100的90版本的数据版本(前提是使用start tansaction with xxxxxxx语句开启事务,否则 k =2,因为read-view也变了)

3.5小结

隔离级别是依赖数据版本和一致性视图实现的,每个数据版本依赖事务ID:

当前读依赖锁实现每次都能读取到最新数据版本的数据,更新语句会触发当前读

上一篇下一篇

猜你喜欢

热点阅读