Redis 分布式锁是一种使用 Redis 数据库来实现的同步机制,用于在分布式系统中保证多个进程或线程对共享资源的互斥访问。Redis 分布式锁的基本原理是使用 Redis 的 SETNX 命令来尝试设置一个键,如果这个键不存在,那么获取锁成功,否则获取锁失败。获取锁成功后,需要设置锁的过期时间,以防止锁被永久占用。释放锁时,需要删除这个键。
Redis 分布式锁有一些优点和缺点:
优点:
Redis 是一种高性能的内存数据库,支持多种数据结构和原子操作,可以快速地实现分布式锁的功能。
Redis 支持使用 Lua 脚本来执行原子操作,可以避免在设置键和设置过期时间之间的时间窗口内发生的竞态条件。
Redis 提供了一种更高级的锁,称为 RedLock,可以在多个 Redis 实例之间提供更强的一致性保证。
缺点:
Redis 不是一个强一致性的数据库,可能会出现数据丢失或不一致的情况,导致锁的可靠性降低。
Redis 分布式锁需要考虑一些边界情况,例如锁的有效性,锁的重入,锁的续约等,否则可能会导致死锁或资源浪费。
方法一
/** * redis分布式锁 * @param $lockKey string 锁的key * @param $lockExpire int 过期时间 秒 * @param $retryInterval int 重试间隔 秒 * @param $fun callable 回调函数 * @return void */ function redisSyncsLock($lockKey, $lockExpire, $retryInterval, $fun) { // 尝试获取锁 while (true) { // 如果这个键不存在,SETNX 命令会设置这个键并返回 1,否则返回 0。 $lockValue = time() + $lockExpire; $lock = Redis::setnx($lockKey, $lockValue); if ($lock || (Redis::exists($lockKey) && Redis::get($lockKey) < time())) { // 获取锁成功,设置锁的过期时间 Redis::expire($lockKey, $lockExpire); // 尝试获取锁成功,执行需要加锁的代码 // begin $fun(); // 尝试获取锁成功,执行需要加锁的代码 // end // 释放锁 Redis::del($lockKey); break; } else { // 获取锁失败,等待一段时间后重试 sleep($retryInterval); } } }
redisSyncsLock
函数是一个使用 Redis 实现的分布式锁。它接受四个参数:$lock_key
(锁的键名),$lock_expire
(锁的过期时间,单位是秒),$retry_interval
(获取锁失败时的重试间隔时间,单位是秒)和 $fun
(一个回调函数,当获取锁成功时会被执行)。
以下是这个函数的工作流程:
进入一个无限循环,尝试获取锁。
使用 Redis 的
SETNX
命令尝试设置一个键,键名是$lock_key
,键值是当前的时间。如果这个键不存在,SETNX
命令会设置这个键并返回1
,否则返回0
。如果获取锁成功(
SETNX
命令返回1
),则设置锁的过期时间为$lock_expire
,然后执行回调函数$fun
,最后删除这个键(释放锁),并退出循环。如果获取锁失败(
SETNX
命令返回0
),则等待$retry_interval
秒后再次尝试获取锁。
这个函数可以确保在分布式系统中,对共享资源的访问是互斥的。也就是说,同一时间只有一个进程或线程可以执行回调函数 $fun
。
方法二
/** * 使用 Lua 脚本:Redis 支持使用 Lua 脚本来执行原子操作。你可以使用 Lua 脚本来同时执行 SETNX 和 EXPIRE 命令,这样可以避免在设置键和设置过期时间之间的时间窗口内发生的竞态条件。 * redis分布式锁 * @param $lockKey string 锁的key * @param $lockExpire int 过期时间 秒 * @param $retryInterval int 重试间隔 秒 * @param $fun callable 回调函数 * @return void */ function redisSyncsLock2($lockKey, $lockExpire, $retryInterval, $fun) { // 定义 Lua 脚本 $lua = <<<EOF if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end EOF; // 尝试获取锁 while (true) { $lockValue = time() + $lockExpire; $lock = Redis::eval($lua, [$lockKey, $lockValue, $lockExpire], 1); if ($lock) { Redis::expire($lockKey, $lockExpire); // 尝试获取锁成功,执行需要加锁的代码 // begin $fun(); // 尝试获取锁成功,执行需要加锁的代码 // end // 释放锁 Redis::del($lockKey); break; } else { // 获取锁失败,等待一段时间后重试 sleep($retryInterval); } } }
检查锁的有效性:在当前的实现中,如果进程在执行回调函数时崩溃,那么锁可能会被永久占用,导致其他进程无法获取锁。你可以在获取锁失败时检查锁的有效性,如果锁已经过期,那么可以强制释放锁。
使用 Lua 脚本:Redis 支持使用 Lua 脚本来执行原子操作。你可以使用 Lua 脚本来同时执行 SETNX 和 EXPIRE 命令,这样可以避免在设置键和设置过期时间之间的时间窗口内发生的竞态条件。
方法三
/** * 使用 Lua 脚本:Redis 支持使用 Lua 脚本来执行原子操作。你可以使用 Lua 脚本来同时执行 SETNX 和 EXPIRE 命令,这样可以避免在设置键和设置过期时间之间的时间窗口内发生的竞态条件。 * redis分布式锁 * @param $lockKey string 锁的key * @param $lockExpire int 过期时间 秒 * @param $retryInterval int 重试间隔 秒 * @param $fun callable 回调函数 * @return void */ function redisSyncsLock3($lockKey, $lockExpire, $retryInterval, $fun) { // 创建一个 RedLock 对象 $redLock = new Predis\Client(); while (true) { // 尝试获取锁 $lock = $redLock->lock($lockKey, $lockExpire); if ($lock) { // 获取锁成功,执行共享资源的操作 $fun(); // 释放锁 $redLock->unlock($lock); break; } else { // 获取锁失败 sleep($retryInterval); } } }
RedLock 是一种分布式锁的算法,由 Redis 的作者 Salvatore Sanfilippo 提出。它可以在多个 Redis 实例之间提供更强的一致性保证。
以下是使用 RedLock 的基本步骤:
获取当前的时间,单位是毫秒。
尝试在所有的 Redis 实例上获取锁。在每个实例上,使用相同的键名和随机的键值,锁的过期时间应该足够长,以确保你的操作可以在锁过期之前完成。
如果你在大多数(N/2+1)的 Redis 实例上获取到了锁,那么锁就被认为是获取成功的。否则,需要在所有的 Redis 实例上释放锁,并从步骤 1 重新开始。
如果获取到了锁,可以开始执行操作。在操作完成后,需要在所有的 Redis 实例上释放锁。
使用:
redisSyncsLock('goods:id' . $id, 5, 1, function () { $this['sale_num'] = $this['sale_num'] + 1; $this->save(); // 更新缓存 $this->updateCache(); // ..... });