简介

我们在做业务开发时候,经常遇到一些多进程/线程对同一数据修改而产生竞态条件,为了避免修改冲突覆盖,常常用锁机制来保证数据的正常性。同一进程内多线程并发可以用语言特性实现比如java的synchronized或GO的sync同步代码包或者直接利用本地文件锁实现。但是在分布式环境中,我们需要用到分布式锁。分布式锁的实现方式有很多,比如mysql,zookereper和redis。本文就讲讲最简单的redis分布式锁实现方式(代码示例基于php)。

获取锁

$key = "redis_lock";
$value = "550e8400-e29b-41d4-a716-446655440000";
$ttl = 5000;
$redisIns->set($key, $value, 'NX', 'PX', $ttl);
  1. $key自定义锁名,应该保证唯一性
  2. $ttl 过期时间,保证不会因某进程得到锁却意外退出导致死锁
  3. set参数nx实现效果等于setnx+expire操作,但是set是原子性操作而setnx+expire是非原子的,所以应使用set完成加锁操作
  4. 根据返回值判断是否加锁成功,如果加锁失败可以直接丢错误码给前台或用自旋锁来竞争加锁
  5. $ttl尽量要大于该进程的操作时间,避免因操作太久但是到期导致锁解除。在操作中可以通过expire进行锁续命
  6. $value uuid或其它方式生成一个分布式环境下全局唯一的字符串。删除锁时候需要重新获取下该key的value,判断是否与生成的一致,如若一致,则可以删除,避免因锁提前过期导致删除掉别的进程持有的锁。

解除锁

由于删除锁时候需要先重新获取下该key的value,判断是否与生成的一致,如若一致,才可以删除,而get和del并非原子性,所以需要用lua脚本或事务实现,保证其原子性。


//基于lua脚本
$script = <<<LUA
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
LUA;
$redisIns->eval($script, 1, $key, $value);
//基于事务
$options = array(
    'cas' => true,
    'watch' => $key,
    'retry' => 3,
);
$redisIns->transaction($options, function ($tx) use ($key, $value) {
    $tx->multi();
    if ($tx->get($key) == $value) {
        $tx->del($key);
    }
});

待续···