深入Redis(一)分布式锁

分布式锁

由于分布式应用在逻辑处理时存在并发问题,比方修改数据,要先读取到内存,在内存中修改后再保存回去,这两个操作是单独的,如果同时进行,就会出现并发问题。

此时就要用到分布式锁来限制程序的并发执行。

本质

本质就是在Redis内占一个位置,若别的进程也想占用该位置,发现有进程在使用该位置,就放弃或等待。

  1. 在Redis中实现依靠setnx(set if not exists)指令,用完了再调用del指令来释放位置。

  2. 在1中,如果逻辑执行到中间出现异常,可能导致del未调用,这就陷入死锁,锁永远得不到释放,因此可以给这个位置增加一个过期时间,这样即使出现异常也能保证位置会被释放。

  3. 在2中,如果在setnxexpire中间出现问题导致进程挂掉,则expire不会执行,同样造成死锁。因此出现了分布式锁的library,但Redis作者从2.8版本开始加入了set指令的拓展参数,因此可以通过set key value ex seconds nx指令来合并nx和expire为原子操作,这就是奥义。

超时问题

如果加解锁之间的逻辑执行时间超出过期时间,则会导致这个锁被其它进程获取,而其它进程执行逻辑时,若第一个逻辑执行完,它将调用解锁操作,则会导致第二个程序还没运行完锁就被释放。

为了解决后一个问题,引入tag标签来标记锁,设置一个随机数作为锁的value,在释放锁时要匹配该随机数,但Redis没有提供匹配的方法,因此需要Lua脚本来处理。(Lua脚本可以保证连续多个指令的原子性执行)

以上并没有完美解决超时问题,只是相对安全一点。

可重入性

可重入性指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,则这个锁就是可重入的。这是为了解决超时问题,若超时判断逻辑是否执行完,未完则再加锁,直到由代码调用解锁。

Redis分布式锁要支持可重入,则需对客户端set方法进行包装,用线程的Treadlocal变量存储当前持有锁的计数。

import redis
import threading


locks = threading.local()
locks.redis = {}


def key_for(user_id):
    return "account_{}".format(user_id)


def _lock(client, key):
    return bool(client.set(key, True, nx=True, ex=5))


def _unlock(client, key):
    client.delete(key)


def lock(client, user_id):
    key = key_for(user_id)
    if key in locks.redis:
        locks.redis[key] += 1
        return True
    ok = _lock(client, key)
    if not ok:
        return False
    locks.redis[key] = 1
    return True


def unlock(client, user_id):
    key = key_for(user_id)
    if key in locks.redis:
        locks.redis[key] -= 1
        if locks.redis[key] <= 0:
            _unlock(client, key)
        return True
    return False


client = redis.StrictRedis()

不推荐使用可重入锁,其加重了客户端的复杂性,调整逻辑结构完全可以不使用可重入锁。

猜你喜欢

转载自www.cnblogs.com/ikct2017/p/9498534.html