准备数据
id | name | num | version |
---|---|---|---|
1 | 张三 | 100 | 0 |
库存超卖问题是因为并发国过程中,多个进程在并发时获取的库存数据是一致的,然后减库存的操作又是同时进行的,从而导致库存数据出现混乱。
悲观锁
概念理解:假设并发过程中不存在,冲突的情况,而在出现冲突之后再进行处理
在事务中查询数据的同时,并查询一个版本号数据,然后在更新库存的时候,根据 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 环境 5.7 innodb 存储引擎,事务隔离级别 可重复读。
其它
修改 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]