对于简单的业务,我们可以通过单实例的 Redis 实现一个全局独占锁。笔者以 PHP 为例,实现一个加锁类。
实现原理
对于 Redis 的 SET 命令,支持 NX 特性,官方解释为:Only set the key if it does not already exist。也就是,当 key 不存在时设置成功后,才会返回 OK,否则返回 nil。这样,当多个并发请求设置 key 时,只有一个会成功。
然后,结合 EX 过期时长,指定加锁时长,即过期后自动释放锁。再通过指定 key 的值为随机 Token,来保证当前操作不会错误的释放其它业务请求的锁。详见下文实例。
SET 用法如下:
set {LOCK_NAME} {LOCK_TOKEN} EX {LEASE_TIME} NX
具体实现
实例的完整源码,放到了:https://github.com/xingchaovv/php-example/tree/master/src/RedisLocker,请参考使用。源码基于 PHP 8。
方法 lock 实现
/**
* 加锁
* @return bool 成功时返回 true,失败时返回 false
*/
public function lock(): bool {
$lockName = self::LOCK_NAME_PREFIX . $this->lockName;
$lockToken = uniqid('', true);
// set {LOCK_NAME} {LOCK_TOKEN} EX {LEASE_TIME} NX
$isLocked = self::redis()->rawCommand('set', $lockName, $lockToken, 'EX', $this->leaseTime, 'NX');
if (!$isLocked) {
return false;
}
$this->lockToken = $lockToken;
return true;
}
方法 unlock 实现
/**
* 解锁
* @return bool 成功时返回 true,失败时返回 false
*/
public function unlock(): bool {
if (!$this->lockToken) {
return false;
}
$lockName = self::LOCK_NAME_PREFIX . $this->lockName;
if (self::redis()->get($lockName) !== $this->lockToken) {
return false;
}
self::redis()->del($lockName);
$this->lockToken = '';
return true;
}
用法
// 针对 order101 构建锁实例
$locker = RedisLocker::valueOf("order101");
// 加锁
$lockResult = $locker->lock();
printf("[%s] order101 lock result: %s\n", date('r'), print_r($lockResult, true));
printf("[%s] order101: %s\n", date('r'), print_r($locker, true));
// 解锁
$unlockResult = $locker->unlock();
printf("[%s] order101 unlock result: %s\n", date('r'), print_r($unlockResult, true));
printf("[%s] order101: %s\n", date('r'), print_r($locker, true));
(完)