前言
本文围绕 redis 的 SETNX
命令展开对“锁”的研究与实现。多个进程同时对 redis 执行 SETNX string_key timestamp_expired
命令,只有一个进程会成功,其余都会失败。上述命令中的 string_key 代表被当作锁的 redis 键名,其类型为 String,timestamp_expired 代表该键的过期时间戳,下同。
软件版本
- windows 10
- php 7.4.14 nts
- thinkphp 6.1.0
- redis 6.2.7
- predis/predis 2.0.3(php第三方扩展包)
锁算法
算法中使用 redis 的 EVAL
命令保证执行语句的原子性。
申请加锁前需约定锁的有效期、加锁失败后的重试次数、休眠时间(申请加锁时,其他线程加的锁还未过期,休眠一段时间后再重试加锁)。
申请加锁步骤:
- 申请加锁(
SETNX string_key timestamp_expired
),如果加锁成功,设置锁的过期时间并返回成功。否则进入步骤 2; - 如果加锁失败原因为 string_key 刚刚被其他进程删除(释放锁),返回步骤 1 重新申请加锁,否则进入步骤 3;
- 如果加锁失败原因为 string_key 还未过期(此时也会得到旧的过期时间戳),休眠一段时间后返回步骤 1 重新申请加锁,否则进入步骤 4;
- 执行
GETSET string_key timestamp_expired_new
命令,如果命令返回时间戳与旧的过期时间戳相等,加锁成功,设置锁的过期时间并返回成功。否则进入步骤 5; - 锁被其他进程删除了或者被其他进程抢先执行了
GETSET string_key timestamp_expired_new
命令,返回步骤 1 重新申请加锁。
释放锁:
如果 string_key 键未被删除且还未过期,执行删除键操作( DEL string_key
)。
实现
主要类文件有 2 个,为文件 Redis.php(获取 redis 操作客户端) 和 Lock.php(加、释放锁),内容分别如下:
<?php
namespace app\service;
use Predis\Client;
use think\facade\Env;
class Redis
{
/**
* @var Client $client
*/
protected $client;
public function __construct()
{
$this->client = new Client([
'host' => Env::get('redis.host