分布式锁
由于分布式应用在逻辑处理时存在并发问题,比方修改数据,要先读取到内存,在内存中修改后再保存回去,这两个操作是单独的,如果同时进行,就会出现并发问题。
此时就要用到分布式锁来限制程序的并发执行。
本质
本质就是在Redis内占一个位置,若别的进程也想占用该位置,发现有进程在使用该位置,就放弃或等待。
- 在Redis中实现依靠
setnx
(set if not exists)指令,用完了再调用del
指令来释放位置。 - 在1中,如果逻辑执行到中间出现异常,可能导致
del
未调用,这就陷入死锁,锁永远得不到释放,因此可以给这个位置增加一个过期时间,这样即使出现异常也能保证位置会被释放。 - 在2中,如果在
setnx
和expire
中间出现问题导致进程挂掉,则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()
不推荐使用可重入锁,其加重了客户端的复杂性,调整逻辑结构完全可以不使用可重入锁。
原文地址:https://www.cnblogs.com/ikct2017/p/9498534.html
时间: 2024-10-11 20:19:36