• laravel中锁以及事务的简单使用


     一、首先来说一下什么是共享锁?什么是排他锁?

    共享:我可以  加锁 , 别人可以  加锁
    排他:只有我 才 可以   加锁 , 也就是说,必须要等我提交事务,其他的才可以操作。

    二、简单例子实现加锁

     锁和事务在使用时需要配合使用,也就是用锁时需要先开启事务,事务提交时,会自动解锁。

    1. DB::beginTransaction(); // 开启事务
    2. $good = \App\Models\Good::sharedLock()->first(); //共享锁 s锁 读锁
    3. // $good = \App\Models\Good::lockForUpdate()->first(); //排他锁 x锁 写锁...
    4. DB::commit();
    1. DB::beginTransaction();
    2. $goodsInfo = Goods::where('goods_id',$gid)->lockForUpdate()->first();
    3. $goodsInfo->seckill_stock-=1;
    4. $goodsInfo->save();
    5. DB::commit();

    三、怎样利用锁和事务解决并发问题?

    在我们的工作中,常常会出现一些对数量控制有精确要求的需求,比如商品库存量、奖品数量、报名人数限制等等,这些应用场景往往都存在高并发可能,比较容易出现数据量超量问题。以下做一下示例探索:

    (1)首先设计一个存量表
    1. CREATE TABLE `product` (
    2. `id` int(11) NOT NULL AUTO_INCREMENT,
    3. `product_name` varchar(255) NOT NULL DEFAULT '',
    4. `count` int(10) NOT NULL DEFAULT '0',
    5. PRIMARY KEY (`id`)
    6. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
    (2)添加一行数据如下,设定基础库存量为 10

    (3)问题代码如下:
    1. $process_num = 50; //开50个进程,模拟50个用户
    2. for ($i = 0; $i < $process_num; $i++) {
    3. MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
    4. if (Db::name('product')->where('id', 1)->value('count') > 0) {
    5. $res = Db::name('product')->where('id', 1)->setDec('count');
    6. if ($res) {
    7. dump('获取到更新资源权限:' . $i);
    8. }
    9. }
    10. });
    11. }

    执行结果,50 个用户都获取到了更新资源的权限,但是数据库相应数据存量变成了 - 40

    高并发带来的问题,同一时刻有多个进程读取同一条数据,同一时刻有多个进程更新同一条数据

    (4)解决方案
    1.方案1

    要进行 DML 层面的限制(最后关卡安全,报错总比出现数据问题产生的影响小),主要的修改是将 count 的类型改成了无符号整数,这样该值就不可能再出现负数值

    1. CREATE TABLE `product` (
    2. `id` int(11) NOT NULL AUTO_INCREMENT,
    3. `product_name` varchar(255) NOT NULL DEFAULT '',
    4. `count` int(10) unsigned NOT NULL DEFAULT '0',
    5. PRIMARY KEY (`id`)
    6. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

    执行一下代码,当 count 值从 10 减到 0 时,就不能再减少了,再减就会出现数据库报错

    2.方案2

    mysql 提供的行级锁 select ... lock in share mode(阻塞写),select ... for update(阻塞读写,悲观锁),所以 for update 机制能满足我们的原子要求。编辑代码如下:

    1. $process_num = 50; //开50个进程,模拟50个用户
    2. for ($i = 0; $i < $process_num; $i++) {
    3. MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
    4. Db::startTrans(); //行级锁必须在事务中才能生效
    5. //设置for update,进程会阻塞在这里,只能允许一个进程获取到行锁,其他等待获取
    6. if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) {
    7. $res = Db::name('product')->where('id', 1)->setDec('count');
    8. if ($res) {
    9. dump('获取到更新资源权限:' . $i);
    10. }
    11. }
    12. Db::commit();
    13. });
    14. }

    只有十个进程获取到了更新权限,消费正常

    3.方案3

    将条件语句放到 update 上,保持语句执行的原子性,杜绝并发幻读
    修改代码如下:

    1. $process_num = 50; //开50个进程,模拟50个用户
    2. for ($i = 0; $i < $process_num; $i++) {
    3. MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
    4. //合并两条语句为一条更新语句
    5. $res = Db::name('product')->where('id', 1)->where('count', '>', 0)->setDec('count');
    6. if ($res) {
    7. dump('获取到更新资源权限:' . $i);
    8. }
    9. });
    10. }

    只有十个进程获取到了更新权限,消费正常

    4.方案4

    文件锁机制解决

    1. $process_num = 50; //开50个进程,模拟50个用户
    2. for ($i = 0; $i < $process_num; $i++) {
    3. MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
    4. $filename = app()->getRootPath() . 'runtime/lock';
    5. $file = fopen($filename, 'w'); //打开文件
    6. $lock = flock($file, LOCK_EX);
    7. // $lock=flock($handle, LOCK_EX|LOCK_NB); (异步非阻塞,所有进程如果出现获取不到锁,不等待跳过,加锁失败)
    8. //获取文件排他锁:LOCK_EX(异步阻塞,只有一个进程获得锁,其他竞争进程等待)
    9. //还有一种共享锁:LOCK_SH(所有进程都可以获取共享锁,读取文件,当且只有一个锁时,才允许写操作,否则操作失败,容易出现死锁问题)
    10. if ($lock) {
    11. try {
    12. if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) {
    13. $res = Db::name('product')->where('id', 1)->setDec('count');
    14. if ($res) {
    15. dump('获取到更新资源权限:' . $i);
    16. }
    17. }
    18. } catch (\Exception $e) {
    19. dump($e->getMessage());
    20. } finally {
    21. flock($file, LOCK_UN); //无论如何都要释放锁
    22. }
    23. }
    24. fclose($file); //关闭文件句柄
    25. });
    26. }

    只有十个进程获取到了更新权限,消费正常

    5.方案5

    分布式锁机制解决

    以上文件锁,只适应于单体架构的需求,在集群架构、分布式等多机联网结构中就是掩耳盗铃了,所以适应性更好地锁机制还是要使用分布式锁,分布式锁最常用和最易用就是 redis 的 setnx 锁了。

    1. $process_num = 50; //开50个进程,模拟50个用户
    2. for ($i = 0; $i < $process_num; $i++) {
    3. MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
    4. //获取redis
    5. //关于CacheHelper::getRedisLock是怎样获取锁的,注意几个点就行:1.如何避免死锁;2.如何设置过期时间;3.如何设置抢占条件;4.如何循环等待判断。这些不在本文讨论范围,可自行研究,以后有空我也可以写一篇博文
    6. $lock = CacheHelper::getRedisLock('redis_lock');
    7. if ($lock) {
    8. try {
    9. if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) {
    10. $res = Db::name('product')->where('id', 1)->setDec('count');
    11. if ($res) {
    12. dump('获取到更新资源权限:' . $i);
    13. }
    14. }
    15. } catch (\Exception $e) {
    16. dump($e->getMessage());
    17. }
    18. } else {
    19. // dump('获取redis锁失败');
    20. }
    21. });
    22. }

    参考链接:浅谈并发加锁 | Laravel China 社区 (learnku.com)

  • 相关阅读:
    windows 下 vs code 格式化代码(clang-format)
    Spring Boot发布2.6.2、2.5.8:升级log4j2到2.17.0
    .netcore对传输类设置区分大小
    Java异常、断言和日志①——Java基础
    C++设计模式之单例模式
    oracle 11g从DBF恢复数据
    MySQL 存储引擎
    C++入门
    【Arduino32】PWM控制直流电机速度
    Remote & Local File Inclusion (RFI/LFI)-文件包含漏洞
  • 原文地址:https://blog.csdn.net/smallmww/article/details/133894311