分布式锁的概念

不同于 Java、Golang 这些语言,PHP 本身并不支持并发编程,因为对于 PHP 的主战场 Web 应用而言,每次用户请求都是通过独立的 PHP-FPM 进程处理的,PHP 为了保持语言的简单性,并不支持在这个进程内开启多进程/线程,也就不存在什么基于锁的并发安全问题。

这也是很多 PHP 程序员刚开始迈入 Java/Golang 门槛时最容易犯错的地方,作为静态编译型语言,它们都是支持并发编程的,并且支持通过锁/通道处理并发安全问题。

我们今天要介绍的分布式锁也是为了解决并发安全问题所引入的一种锁机制。

要了解什么是分布式锁,先要了解并发和锁的概念。这一点,你可以参考学院君之前编写的 Go 并发编程或者 MySQL 并发事务了解详细细节,这里我们简单介绍下大致原理。

当两个并发运行的进程/线程要同时处理某个资源的时候,同时只能让一个进程/线程获取到这个资源,待其处理完成后,才能让另一进程/线程开始处理这个资源,否则就会导致这个资源的状态管理出现混乱,而要保证并发运行的程序同时只有一个进程/线程处理这个资源,就需要引入锁机制 —— 某个进程/线程获取到资源锁后,才能对其进行操作,当其他进程/线程试图获取这个资源进行处理时,发现对应的资源锁已经被占用了,就会进入阻塞状态,直到持有这个资源锁的进程/线程处理资源完毕,将锁释放。

注:你可以类比数据库事务的并发操作来理解为什么并发处理资源的进程/线程会导致资源状态出现混乱,比如对于更新用户账户余额的程序,一个线程将用户余额更新还未保存,另一个线程就进来将其更新,最终会导致处理结果与我们预期不一致。我们通过锁机制让并发运行的程序同时只有一个线程才能处理账户更新,则不会出现这样的问题。

所谓分布式锁,指的是这个锁可以被多个分布式部署的服务/应用/进程共享,而不仅仅局限于某个服务/应用/进程内部。

另外,对于所有锁而言,不同进程/线程在竞争获取锁时,要确保获取锁的操作是原子性的,否则依然存在并发安全问题,即同时有多个进程/线程获取并处理同一个资源。

最后,这个锁还支持在上锁的同时设置过期时间,否则万一某个进程/线程获取到锁之后,处理资源时异常退出,导致锁没有释放,那么其他进程/线程就永远处于阻塞状态,不能再处理这个资源了。

通过 Redis 实现分布式锁

Redis 作为分布式存储中间件,天然适合实现分布式锁,因为它同时满足上面这三个条件:

  • 以单进程模式运行的 Redis 服务可以同时被分布式部署和运行的多个服务/应用/进程共享;

  • Redis 的 SET 指令支持在设置键值的同时设置过期时间,并且整个操作是原子性的,所以完全可以基于这个操作来实现分布式锁,待资源处理完成后,再通过 DEL 指令删除键值来释放锁。

为了直观地给大家展示这个分布式锁的效果,我们在 Laravel 中编写一个 Artisan 命令来模拟并发运行的应用:

php artisan make:command ScheduleJob

先看看不使用分布式锁的运行情况:

<?php namespace App\Console\Commands;use Illuminate\Console\Command;use Illuminate\Support\Facades\Storage;class ScheduleJob extends Command{protected $signature = 'schedule:job {process}';protected $description = 'Mock Schedule Jobs';public function __construct(){parent::__construct();    }public function handle(){        $processNo = $this->argument('process');for ($i = 1; $i <= 10; $i++) {            $log = "Running Job #{$i} In Process #{$processNo}";// 将运行日志记录到本地文件存储(storage/app/schedule_job_logs)            Storage::disk('local')->append('schedule_job_logs', $log);            sleep(1);  // 模拟长时间运行的任务        }    }}

我们通过 Artisan 命令参数传入模拟的进程 ID,然后将运行日志记录到本地存储的 storage/app/schedule_job_logs 日志文件。打开两个终端窗口同时运行这个 Artisan 命令,并传入不同的进程 ID:


打开日志文件,可以看到运行记录呈犬牙交错状:


两个进程可以并行处理这个程序,由于没有引入锁机制,所以如果把 for 循环看作一个资源处理,那么两个进程可以同时获取这个资源进行处理,进而导致并发安全问题,要解决这个问题,我们可以通过 Redis 实现一个锁,Laravel 底层已经实现了基于 Redis 的锁 Illuminate\Cache\RedisLock,所以不需要重复造轮子了,直接拿来用就好了:

<?php namespace App\Console\Commands;use Illuminate\Cache\Lock;use Illuminate\Cache\RedisLock;use Illuminate\Console\Command;use Illuminate\Contracts\Cache\LockTimeoutException;use \Illuminate\Redis\Connections\Connection as RedisConnection;use Illuminate\Support\Facades\Storage;class ScheduleJob extends Command{protected $signature = 'schedule:job {process}';protected $description = 'Mock Schedule Jobs';protected Lock $lock;public function __construct(RedisConnection $redis){parent::__construct();// 基于 Redis 实现锁,过期时间 60s$this->lock = new RedisLock($redis, 'schedule_job', 60);    }public function handle(){// 如果没有获取到锁,阻塞 5s,否则执行回调函数$this->lock->block(5, function () {            $processNo = $this->argument('process');for ($i = 1; $i <= 10; $i++) {                $log = "Running Job #{$i} In Process #{$processNo}";                Storage::disk('local')->append('schedule_job_logs', $log);            }        });    }}

删除上次生成的 schedule_job_logs,再次同时运行这两个 Artisan 命令 schedule:job,这一次的日志输出结果就变成先执行一个进程,再执行另一个进程了:


这是因为锁生效的缘故。

RedisLock 底层实现源码

这个 RedisLock 底层正是使用了 Redis SET 指令实现锁的设置,我们查看 block 函数底层源码:


它在底层会先调用 acquire 函数试图获取锁:

public function acquire(){    if ($this->seconds > 0) {        return $this->redis->set($this->name, $this->owner, 'EX', $this->seconds, 'NX') == true;    } else {        return $this->redis->setnx($this->name, $this->owner) === 1;    }}

这里我们设置了锁的过期时间,所以会调用第一个 if 里面的代码,即通过 Redis 的 SET key value EX expire NX 指令设置锁,该指令只会在锁不存在的情况下设置,如果已经存在,则返回 false,这是一个原子操作;如果初始化 RedisLock 时未指定过期时间,则调用 SETNX 指令设置锁,这也是一个只有锁不存在的情况下操作才会成功的原子操作。

回到 block 函数,如果获取锁失败,则当前进程会阻塞一段时间(通过 usleep 函数模拟)后尝试重新获取锁,如果阻塞时间过长,超出锁的过期时间设置,则抛出锁超时异常。

如果成功获取到锁,则执行回调函数中的代码(真正的业务代码),最后调用 release 函数释放锁:

public function release(){    return (bool) $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner);}

如果你进一步追溯底层源码,会发现其实调用的是 Redis 的 DEL 指令删除对应的键实现锁释放。

由于这把锁是基于 Redis 实现的,所以它既可以作为 Laravel 应用中普通进程之间的锁,也可以作为分布式锁,不过对于 PHP 应用而言,主要的多进程场景在于控制台应用,比如消息队列这种多进程处理,或者任务调度中的多进程处理。限于篇幅,学院君将在下篇教程给大家详细介绍分布式锁在任务调度底层的应用。

RedisLock 外,Laravel 底层还基于其他驱动实现了类似的分布式锁,比如 CacheLockDatabaseLockDynamoDbLock,感兴趣的同学可以去一探究竟,这里就不一一介绍了。

本系列教程首发在学院君网站(xueyuanjun.com),你可以点击页面左下角阅读原文链接查看最新更新的教程。

锁php_基于 Redis 实现分布式锁及对应的 PHP 实现源码相关推荐

  1. php使用redis分布式锁,php基于redis的分布式锁实例详解

    在使用分布式锁进行互斥资源访问时候,我们很多方案是采用redis的实现. 固然,redis的单节点锁在极端情况也是有问题的,假设你的业务允许偶尔的失效,使用单节点的redis锁方案就足够了,简单而且效 ...

  2. 基于 Redis 实现分布式锁思考

    以下文章来源方志朋的博客,回复"666"获面试宝典 来源:blog.csdn.net/xuan_lu/article/details/111600302 分布式锁 基于redis实 ...

  3. nx set 怎么实现的原子性_基于Redis的分布式锁实现

    前言 本篇文章主要介绍基于Redis的分布式锁实现到底是怎么一回事,其中参考了许多大佬写的文章,算是对分布式锁做一个总结 分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问 ...

  4. 基于Redis的分布式锁和Redlock算法

    来自:后端技术指南针 1 前言 今天开始来和大家一起学习一下Redis实际应用篇,会写几个Redis的常见应用. 在我看来Redis最为典型的应用就是作为分布式缓存系统,其他的一些应用本质上并不是杀手 ...

  5. redis系列:基于redis的分布式锁

    一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

  6. 基于 Redis 的分布式锁到底安全吗?

    [完整版] 网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词"Redis 分布式锁"随便到哪个搜索引擎上去搜索一下就知道了.这些文章的思路大体相近,给出的实现 ...

  7. js 拉勾网效果_Node.js 中实践基于 Redis 的分布式锁实现

    在一些分布式环境下.多线程并发编程中,如果对同一资源进行读写操作,避免不了的一个就是资源竞争问题,通过引入分布式锁这一概念,可以解决数据一致性问题. 作者简介:五月君,Nodejs Developer ...

  8. 基于Redis的分布式锁到底安全吗(上)?

    网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词"Redis 分布式锁"随便到哪个搜索引擎上去搜索一下就知道了.这些文章的思路大体相近,给出的实现算法也看似合 ...

  9. 基于Redis的分布式锁实现

    本文转自 一.分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的 ...

  10. redis使用sysc超时_基于redis的分布式锁实现

    随着业务越来越复杂,应用服务都会朝着分布式.集群方向部署,而分布式CAP原则告诉我们,Consistency(一致性). Availability(可用性).Partition tolerance(分 ...

最新文章

  1. Exchange 服务器查看版本号
  2. 2021-01-07 python opencv实现车牌识别 颜色定位
  3. evc4工程移植vs.net2005所碰到的问题积累
  4. class12_pack_grid_place 放置位置
  5. node npm包安装 save和save-dev的区别
  6. hive求差集和交集
  7. win32获取当前运行程序的内存地址_一个lock锁,就可以分出程序员对问题处置方式水平的高中低...
  8. jinja Comments
  9. Activiti获取当前活动(任务)的出口(动态生成提交按钮)
  10. QQ音乐文件缓存位置以及修改方法步骤
  11. 微信小程序05 事件绑定与事件传参
  12. 挑战性价比,刷新你对千元级投影仪的认知,这份详细评测送给你
  13. JavaSE数组基础练习题
  14. JavaScript replace 强行保留后三位小数点
  15. Linux中文乱码-word-pdf字体转换问题处理
  16. linux编写多时区时间显示程序,Linux系统时区时间修改
  17. 金蝶记账王、迷你版、标准版引入凭证方法
  18. 价值180的仿3641美nv图库帝国CMS内核整站源码
  19. 机房环境监控系统机房守护者!
  20. orb_slam3实现保存/加载地图功能and发布位姿功能

热门文章

  1. 【语音隐写】基于matlab GUI LSB语音信号数字水印嵌入提取(带面板)【含Matlab源码 1676期】
  2. 【优化预测】基于matlab天牛须算法优化ELMAN神经网络预测【含Matlab源码 1375期】
  3. 【TWVRP】基于matalb粒子群算法求解带时间窗的车辆路径规划问题【含Matlab源码 1272期】
  4. 【水果蔬菜识别】基于matlab GUI形态学水果蔬菜识别【含Matlab源码 919期】
  5. 【图像分割】基于matlab模糊聚类算法FCM图像分割【含Matlab源码 084期】
  6. mc2180 刷机方法_MC控制和时差方法
  7. 网络计算机应急处理,网络安全应急响应
  8. vuex模块化 怎么引用state_vue 组件如何调用 vuex 模块中的getters
  9. 游戏挂机计算机自己保护怎么办,游戏中遇到玩家挂机该如何应对?做好这5件事可以降低输的机率...
  10. flash 围棋_17岁攻读剑桥计算机,围棋只有业余一段,研发阿尔法狗战胜柯洁