关于mysql的事务隔离模式、快照读和当前读、MVCC,并用RC+CAS实现库存处理

2021年6月29日 28点热度 0条评论 来源: AirGo.

码字不易,转载请附原链,搬砖繁忙回复不及时见谅,技术交流请加QQ群:909211071

一切都源自一场面试

由于初出茅庐不知天高地厚,在简历上写了熟练掌握MySQL事务,今天去面了某大厂,被面试官虐的体无完肤,下面是当时的对话:

面试官:我看你写熟练掌握MySQL事务,那我们简单聊一下事务的隔离模式吧,先说一下有哪几种隔离模式?

我:四种隔离级别由低到高分别是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复度(Repeatable Read),串行(Serializable)。

面试官:那你能不能聊一下每个模式下解决了什么问题,存在什么问题呢?

我:由上到下应该是分别解决了脏读,呃,幻读,可重复读,每个阶段存在的问题,呃,这个记不太清了。。。

面试官:ok,了解了,我们今天的面试先到这,后面有消息会通知你。

恶补

回来好好翻了翻《InnoDB技术内幕》,又看了几篇大佬的文章,同时模拟了每种隔离模式的各种场景,终于对四种隔离模式有了深入的了解。 (由于看很多文章上面放了很多SQL测试截图,对于当初不太懂隔离模式的我,看起来是比较懵逼的,并且截图会占太大篇幅,影响各位看官的整体阅读理解,所以就不贴拉,但是还是建议小伙伴们亲自开2个事务实际操作一下,相信很快就能理解)

各个隔离模式表现

  • 读未提交(Read Uncommitted):可以看到其他事务未提交的内容和已提交的内容,脏读、不可重复读、幻读
  • 读已提交(Read Committed):可以看到其他事务已提交读内容,不可重复读、幻读
  • 可重复读(Repeatable Read):事务开始时和事务结束时读到的数据完全相同、幻读(部分解决,详细原因下面有)
  • 串行(Serializable):事务必须逐步执行,后来的会排队、不存在脏读、幻读、不可重复读

实现的原理

数据库通过视图来实现隔离模式,访问的时候以视图的逻辑结果为准。

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

隔离级别下的问题

前提条件:同时开启A和B两个事务

脏读:

  1. 事务查询id为1的数据,num字段为1
  2. B事务将id为1的数据num更新为2,但未commit
  3. 事务查询id为1的数据,num字段变为2
  4. B回滚,则A读到的数据为脏数据

不可重复读:

  1. 事务查询id为1的数据,num字段为1
  2. B事务将id为1的数据num更新为2,commit
  3. 事务查询id为1的数据,num字段变为2
  4. A事务前后两次读到的同一条记录num字段不同,不可重复读

幻读:

  1. A事务查询id >=1 and id <=3的数据,得到id=1和id=3两条数据(注意:没有id=2的数据)
  2. B事务插入id=2的数据
  3. A事务再查询id >=1 and id <=3的数据,发现多了一条id=2的数据,即两次查询数据的行数不同

各种隔离模式下会出现的问题

  • 读未提交:脏读、不可重复度、幻读
  • 读已提交:不可重复读、幻读
  • 可重复读:幻读

MVCC

划重点:快照读,无需加锁,同时解决了不可重复读、部分幻读的问题,当事务内部update之后,会引发当前版本号升级,还是会出现幻读。

解决幻读:

  • 事务A:查询num>=2 and num<=4的记录,但是不加for update!不加for update!不加for update!
  • 事务B:insert into table ('', 3),不会被阻塞,不会被阻塞,不会被阻塞!然后commit
  • 事务A:查询num>=2 and num<=4的记录,和之前查询相同
  • 事务A:提交
  • 事务A:查询num>=2 and num<=4的记录,可以查询到num=3的记录,此时返回3条结果

版本号升级现象:

  • 事务A:查询num>=2 and num<=4的记录,但是不加for update!不加for update!不加for update!
  • 事务B:insert into table ('', 3),不会被阻塞,不会被阻塞,不会被阻塞!然后commit
  • 事务A:查询num>=2 and num<=4的记录,和之前查询相同
  • 事务A:update table set name = 'mysql' where nume = 2; (版本号升级)
  • 事务A:查询num>=2 and num<=4的记录,可以查询到num=3的记录,此时返回3条结果
  • 事务A:提交

快照读和当前读

MySQL的读操作分为快照读和当前读:

快照读:提高读-写或写-读场景的并发性,只对写操作进行加锁,读操作读取当前事务的可见版本,通过MVCC实现,每个事务读取的是当前事务的可见版本(视图)。简单的说,MVCC机制对数据库的任何修改的提交都不会直接覆盖之前的数据,而是产生一个新的版本与老版本共存,可重复读模式就是通过MVCC的版本号,使事务开始时和结束时读同一个视图的数据,以此达到不用加锁,同时解决了不可重复读、幻读(版本号升级除外)的问题。

当前读:读和写都加锁,读取的是当前版本,通过next-key实现。当我们开启一个事务,并且在事务中查询时加select...lock in share mode或for update时,以及insert、update、delete操作,会使用next-key锁定查询范围里对应的行数,并且该锁是排他锁,其他事务排队等待。

小应用

电商场景下的库存处理,用读已提交模式(因为需要在当前事务内检测到其他事务对库存的更改),采用CAS+版本号的方式,失败自旋10次重试,全失败返回错误,否则返回成功。

     $returnData = [
            'is_success'         => true,
            'lack_goods_id_list' => [],
        ];

        $goodsIdList = array_column($agentOrderList, 'goods_id');

        $sql = 'select id, stock, version from t_goods where id in (%s)';
        $sql = sprintf($sql, implode(',', $goodsIdList));
        $rows = $this->modelGoods->sqlquery($sql);

        if(!$rows){
            $returnData['is_success'] = false;
            return $returnData;
        }

        $hasLack = false;
        $goodsList = array_column($rows, null, 'id');
        foreach($agentOrderList as $agentOrder){
            $goodsId    = $agentOrder['goods_id'];
            $agentCount = $agentOrder['agent_count'];
            $stock      = $goodsList[$goodsId]['stock'];
            $version    = $goodsList[$goodsId]['version'];

            //先判断库存是否充足
            if($stock < $agentCount){
                $returnData['lack_goods_id_list'][] = $goodsId;
                $hasLack                            = true;
                $returnData['is_success']           = false;
                continue;
            }

            if($hasLack){
                continue;
            }

            $maxAttempt       = 10;
            $currentIsSuccess = false;
            $changeSql = 'update t_goods set stock = stock - %d, version = version + 1 where id = %d and version = %d';
            $changeSql = sprintf($changeSql, $agentCount, $goodsId, $version);
            while(--$maxAttempt){
                $changeResult = $this->modelGoods->sqlquery($changeSql);
                if($changeResult){
                    $currentIsSuccess = true;
                    break;
                }
                $where = [
                    'id' => $goodsId,
                ];
                $row     = current($this->modelGoods->select($where, [], 0, 1, $fields));
                $version = $row['version'];
            }

            if(!$currentIsSuccess){
                $returnData['is_success'] = false;
                return $returnData;
            }
        }

        return $returnData;

    原文作者:AirGo.
    原文地址: https://blog.csdn.net/why444216978/article/details/105027900
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。