1.使用set和其两个扩展参数(ex,nx)实现分布式锁
可以使用setnx (set if not exists) 加锁,如果执行结束,再将锁释放掉。
1 2 3
| > setnx lock:task_id true ··· doing task ··· > del lock:task_id
|
但是这样会有问题,如果在执行task之间出现了异常,删除锁的操作无法执行,就会出现死锁。
于是可以在加锁的时候加上过期时间,这样即使出现异常也可以保证锁可以正常释放。
1 2 3 4
| > setnx lock:task_id true > expire lock:task_id 5 ··· doing task ··· > del lock:task_id
|
以上逻辑还会有问题,如果在setnx和expire之间服务器突然挂掉了,导致没有给锁加上过期时间,仍然会造成死锁。
这种问题的根源在于setnx和expire不是原子指令。可以使用set的扩展参数,实现setnx和expire的原子操作,从而实现分布式锁。
1 2 3
| > set lock:task_id true ex 5 nx ··· doing task ··· > del lock:task_id
|
2.分布式锁的超时问题
redis分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑处理时间很长,超过了锁的过期时间,那么就会出现在第一个线程加锁之后,在处理过程中锁过期了,其他线程就可以重新持有这把锁,导致代码不能得到严格串行执行。
为了避免这个问题,redis分布式锁不要用于太长时间的任务。
3.可重入锁
可重入锁是指线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程持续加锁,那么这个锁是可重入的。Redis分布式锁如果要支持可重入,需要对客户端的set方法进行包装,使用Treadlocal变量存储当前持有锁的计数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import redis import threading
locks = threading.local() locks.redis = {}
def key_for(task_id): return "account_{}".format(task_id)
def _lock(redis_client, key): return bool(redis_client.set(key, 1, nx=True, ex=5))
def _unlock(redis_client, key): redis_client.delete(key)
def lock(redis_client, task_id): key = key_for(task_id) if key in locks.redis: locks.redis[key] += 1 return True locked = _lock(redis_client, key) if not locked: return False locks.redis[key] = 1 return True
def unlock(redis_client, task_id): key = key_for(task_id) if key in locks.redis: locks.redis[key] -= 1 if locks.redis[key] <= 0: del locks.redis[key] _unlock(redis_client, key) return True return False
client = redis.StrictRedis(password='Donghuan@2019') print("lock: ", lock(client, 'task1')) print("now lock num: ", locks.redis) print("lock: ", lock(client, 'task1')) print("now lock num: ", locks.redis) print("unlock: ", unlock(client, 'task1')) print("now lock num: ", locks.redis) print("unlock: ", unlock(client, 'task1')) print("now lock num: ", locks.redis)
|