单节点 Redis 分布式锁介绍

分布式锁

分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。

分布式锁需要具备的特性

互斥性:在任意一个时刻,只有一个客户端可以获取锁。

无死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续客户端能加锁,加一个有效时间。

持锁人解锁:加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给解了。

容错:只要大部分Redis节点都活着,客户端就可以获取和释放锁

分布式锁的实现方式

数据库
Memcached(add命令)
Redis(setnx命令)
Zookeeper(临时节点)

加解锁实例代码

/**
 * 加锁
 * @param $key        string 上锁key
 * @param $value      string 锁对应的唯一值 uniqid() 或 UUID+threadId
 * @param $lockExpire int 过期时间,单位毫秒
 * @return int
 */
function lock($key, $value, $lockExpire)
{
    // key不能为空
    if (empty($key)) {
        throw new Exception('Key can not be empty string.', '1004');
    }

    # 加锁
    # SET lock_key random_value NX PX 5000

    $result = Redis::connection()->set($key, $value, 'NX', 'PX', $lockExpire);
    return is_null($result) ? 0 : 1;
}

/**
 * 解锁
 * @param $key   string 上锁key
 * @param $value string 锁对应的唯一值
 * @return bool
 */
function unlock($key, $value)
{
    $lua = "if redis.call('get', KEYS[1]) == ARGV[1]
        then
            return redis.call('del', KEYS[1]) 
        else 
            return 0 
        end";
    return (bool)Redis::connection()->eval($lua, 1, $key, $value);
}

加解锁过程注意点

1、加锁即在redis中设置(set)一个key,注意只有在该key不存在时设置,存在时不设置(存在即说明已经被加锁了),使用 NX;不能用setnx()和expire()因为这样不具有原子性;

2、加锁设置key时,给key设置一个唯一不重复的value,避免解锁时解除了其他进程/线程设置的锁;

3、加锁时给key设置一个过期时间,用PX毫秒级,防止进程中断导致永远无法解锁;这个过期时间要设置的合理,大于代码运行时间,也不要太长;

4、解锁时用lua脚本,因为解锁需要判断key是否存在,且key中value是否相等再删除,要保证原子性操作才行,防止删除了其他进程的锁;

github 项目地址

地址一

地址二

单节点方式的缺点

加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

在Redis的master节点上拿到了锁;但是这个加锁的key还没有同步到slave节点;master故障,发生故障转移,slave节点升级为master节点;导致锁丢失。

分布式锁使用注意事项

1、加锁失败,即认为已有其他进程获得了锁,此时当前进程根据场景可以进行如下操作:接口返回错误码,请等待等;死循环获取锁,直到获取成功,每次循环之间加等待时间间隔;

引用链接

[1] 地址一: https://github.com/xiaorenaishu/laravel-redislock
[2] 地址二: https://github.com/Mitirrli/redis-lock