关于库存超卖问题,悲观锁和乐观锁的不同实现

准备数据

id name num version
1 张三 100 0

库存超卖问题是因为并发过程中,多个进程在并发时获取的库存数据是一致的,然后减库存的操作又是同时进行的,从而导致库存数据出现混乱。

当前mysql环境5.7 innodb 存储引擎,事务隔离级别 可重复读。

乐观锁

概念理解:假设并发过程中不存在,冲突的情况,而在出现冲突之后再进行处理

在事务中查询数据的同时,并查询一个版本号数据,然后在更新库存的时候,根据 id + 已查出的版本号 作为查询条件,来更新数量,并对版本号 +1。

此时如果是同时(并发)多个请求进来,那么只有一个进程会更新数据,其它进程因为在更新数据时因为版本号不一致而无法对数据做修改,从而更新失败。因为支持只有一个进程能修改数据,修改数据之后,其余的进程只能走减库存失败的逻辑。从而避免库存超卖。

代码如下:(laravel框架)

public function index()
    {
        DB::transaction(function () {
            $data=DB::table('stu')
                ->where('id',1)
                ->first(['num','version']);

info(microtime(true).'=='.$data->num.'='.$data->version.'===');

           $res = DB::table('stu')
               ->where('id',1)
               ->where('version',$data->version)
               ->update(['num'=>$data->num-2,'version'=>$data->version+1]);

           if($res) info(microtime(true)."==".($data->num-2).'='.($data->version+1)."\n");

          //  DB::table('tea')->where('id',1)->decrement('num');

        }, 5);

        echo "good!\n";
    }

使用ab压测工具做10个并发测试
ab -n 10 -c 10 http://local.laravel-test.com/test

打印的日志如下

[2021-04-29 16:09:08] local.INFO: 1619712548.8403==100=0===  
[2021-04-29 16:09:08] local.INFO: 1619712548.8652==98=1

[2021-04-29 16:09:13] local.INFO: 1619712553.4087==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4302==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4144==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4239==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4401==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4148==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4308==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4475==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.5869==96=2

[2021-04-29 16:09:13] local.INFO: 1619712553.826==96=2===  
[2021-04-29 16:09:13] local.INFO: 1619712553.8441==94=3

悲观锁

概念理解:假设并发就会出现冲突,在业务逻辑前就做阻塞处理

代码实现,通过在读取库存数量的时候进行加锁,在事务结束后再解锁,这样的话,再加锁过程中其它进程无法读取库存数据只能等待。从而在减库存的操作上依序执行来保证库存不被超卖。

使用悲观锁的话就不需要 version 字段了

ab -n 10 -c 10 http://local.laravel-test.com/test

  DB::transaction(function () {
            $num=DB::table('stu')->where('id',1)
                ->lockForUpdate()
                ->value('num');

            info(microtime(true).'=='.$num.'===');
            $res = DB::table('stu')->where('id',1)
                ->update(['num'=>$num-2]);

            if($res) info(microtime(true)."==".($num-2).'='."\n");

            //  DB::table('tea')->where('id',1)->decrement('num');

        }, 5);

        echo "good!\n";

打印日志如下

[2021-04-30 01:36:03] local.INFO: 1619746563.4187==100===  
[2021-04-30 01:36:03] local.INFO: 1619746563.4493==98=

[2021-04-30 01:36:08] local.INFO: 1619746568.1882==98===  
[2021-04-30 01:36:08] local.INFO: 1619746568.2638==96=

[2021-04-30 01:36:08] local.INFO: 1619746568.2679==96===  
[2021-04-30 01:36:08] local.INFO: 1619746568.3162==94=

[2021-04-30 01:36:08] local.INFO: 1619746568.3225==94===  
[2021-04-30 01:36:08] local.INFO: 1619746568.36==92=

[2021-04-30 01:36:08] local.INFO: 1619746568.3668==92===  
[2021-04-30 01:36:08] local.INFO: 1619746568.4083==90=

[2021-04-30 01:36:08] local.INFO: 1619746568.4168==90===  
[2021-04-30 01:36:08] local.INFO: 1619746568.4562==88=

[2021-04-30 01:36:08] local.INFO: 1619746568.4608==88===  
[2021-04-30 01:36:08] local.INFO: 1619746568.5036==86=

[2021-04-30 01:36:08] local.INFO: 1619746568.5095==86===  
[2021-04-30 01:36:08] local.INFO: 1619746568.5532==84=

[2021-04-30 01:36:08] local.INFO: 1619746568.5573==84===  
[2021-04-30 01:36:08] local.INFO: 1619746568.5913==82=

[2021-04-30 01:36:08] local.INFO: 1619746568.5959==82===  
[2021-04-30 01:36:08] local.INFO: 1619746568.6296==80=

关于乐观锁与悲观锁

乐观锁在并发时,因为是在出现冲突之后进行处理,好处就是可以提高并发量,坏处也显而易见,因为并发时最终只有一个进程会修改数据成功,那么其它并发进来的进程,就只能走失败的业务逻辑,那就意味着实际业务中不能保证每个人都下单成功。

悲观锁,与乐观锁相反,因为是阻塞执行,那么并发能力就不足,但是每个进程在服务器负载内都能正常下单成功。

其它,序列化隔离级别测试

修改mysql隔离级别为序列化

# 修改全局隔离级别为序列化
SET Global TRANSACTION ISOLATION LEVEL SERIALIZABLE;

# 修改全局隔离级别为可重复读
SET global TRANSACTION ISOLATION LEVEL REPEATABLE READ;

# 查看隔离级别
select @@global.tx_isolation;

ab工具并发执行如下代码
ab -n 10 -c 10 http://local.laravel-test.com/test1

DB::transaction(function () {
            $num=DB::table('stu')->where('id',1)
                //->lockForUpdate()
                ->value('num');

            info(microtime(true).'=='.$num.'===');
            $res = DB::table('stu')->where('id',1)
                ->update(['num'=>$num-2]);

            if($res) info(microtime(true)."==".($num-2).'='."\n");

            //  DB::table('tea')->where('id',1)->decrement('num');

        }, 5);

        echo "good!\n";

以为可以解决库存超卖为题,没想到laravel 日志报错,出现死锁。

据说序列化隔离级别会将事务串行化执行,既然串行化执行了,怎么还会出现死锁,不解中。

希望来个大佬留言指点一二,小弟感激

ocal.ERROR: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction (SQL: update `stu` set `num` = 88 where `id` = 1) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 40001): SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction (SQL: update `stu` set `num` = 88 where `id` = 1) at /mnt/hgfs/Centos7/laravel8/test/vendor/laravel/framework/src/Illuminate/Database/Connection.php:678) [stacktrace] 
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

第一个应该是乐观锁

2年前 评论
木大大 (楼主) 2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!