准备数据
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]