zoukankan      html  css  js  c++  java
  • MySQL-事务中的一致性读和锁定读的具体原理

    前言

    上一篇文章MySQL-InnoDB行锁中,提到过一致性锁定读和一致性非锁定读,这篇文章会详细分析一下在事务中时,具体是如何实现一致性的。

    一致性读原理

    start transaction和begin语句,并不是立即开启一个事务,事务是在第一条读语句执行时才建立的。如果需要立即开启事务,可以使用这个语句:start transaction with comsistent snapshot。

    每一个事务都有一个唯一的事务id,在mysql系统中,这个事务id是唯一且递增的。每一条数据库记录也有一个版本号,这个版本号记录了修改记录的事务id,如图:
    微信图片_20200710095716.jpg

    最新的版本是V4,修改它的事务id为25,依次往前为V3,事务id17,一直到V1,事务id为10。
    数据库中并不是真的有这些V1~V4的物理实体,是根据当前最新版本号和undolog往前计算出来每一个版本的。另外,数据库记录中除了保存修改它的事务id以外,还会记录这条修改是否已经提交。

    在事务建立的一瞬间,当前事务会生成一个数组,保存了当前时刻系统中所有的活跃事务id(未提交事务),按照从小到大顺序排列,其中最小的id为低水位,最大的id为高水位。

    那么在读操作和更新操作的时候,具体是如何使用这个版本号的呢?

    我们知道,读分为一致性锁定读和一致性非锁定读;更新操作,其实可以拆解为两步,一步是一致性锁定读,一步是更新。我们只需要分析 一致性锁定读和一致性非锁定读就可以了。

    • 如果是一致性非锁定读,能读到的是低水位下的最近一个事务更新后的记录。
    • 如果是一致性锁定读,如果当前记录被锁定,需要等待锁释放;如果没被锁定,能读到最新一个已提交记录或者当前事务版本号对记录的修改。
    实验验证

    准备一张表

    create table t(id int,k int,primary key(id));
    insert into t(id,k) values(1,1),(2,2),(3,3),(4,4);
    

    事务的时间线图如下
    微信图片_20200710101306.png

    事务A:

    mysql> start transaction with consistent snapshot;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from t where id = 1;
    +----+------+
    | id | k    |
    +----+------+
    |  1 |    1 |
    +----+------+
    1 row in set (0.00 sec)
    

    事务B:

    mysql> start transaction with consistent snapshot;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update t set k=k+1 where id = 1;
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> select * from t where id = 1;
    +----+------+
    | id | k    |
    +----+------+
    |  1 |    3 |
    +----+------+
    1 row in set (0.00 sec)
    
    实验结果分析

    假设实验开始前记录的最新已提交版本事务id为90,事务A的id为99,事务B的id为100,事务C的id为101。

    先分析B
    在B查询的时候,id=1记录的最新版本为事务C更新的并且已经提交,事务B做的update操作,会被拆分成两步:

    1. select * from t for update;
    2. update t set k=k+1;

    第一步会在当前行上加X锁,并且读最新已提交的版本,虽然C记录的事务id大于B,但是B会去读取它,所以在第一步,B拿到了已经被事务C更新为2的数据。

    第二步,事务B会在2的基础上加一,把当前记录更新为3,并且未提交,且事务版本号为事务B的100。

    再分析A
    在A查询的时候,id=1记录的最新版本为事务B更新的,并且未提交,所以事务A继续往前找,直到找到事务id为90的已提交记录读取出来,所以事务A读取到的为事务id=90更新的1。

    场景实战

    并发减库存的场景,目前库存num=200,初始代码逻辑如下:
    select num from t where t > 0;
    update t set num = num -200;

    有两个并发的事务,事务A和事务B,在事务A执行到select语句后,事务B也执行到select,两个事务都拿到了num=200,按照上面的语句继续做更新操作,事务B结束后就会发现库存num变成了负值,如何修改呢?

    可以改成只写一个update语句

    update t set num = num - 200 where num >= 200
    

    然后根据返回的影响行数做判断,如果影响行数为0,说明库存已经为0,需要做相关的后续业务处理。

  • 相关阅读:
    LeetCode
    LeetCode
    控制反转(Ioc)
    KMP算法
    *&m与m的区别
    函数指针与函数指针数组的使用方法
    C++四种类型转换
    内存分配:堆内存,栈内存
    汇编 基础
    i++,++i 作为参数
  • 原文地址:https://www.cnblogs.com/ging/p/13467830.html
Copyright © 2011-2022 走看看