最近研究了一下redis的分布式鎖奢赂,總體來說,雖然有一些缺點(diǎn)窍株,但是對于小規(guī)模的并發(fā)還是比較實(shí)用的。
先說下為什么用redis作為鎖(個人主觀感受)
1攻柠、redis使用內(nèi)存存儲球订,加鎖和釋放鎖都比較快
2、redis是單線程的程序瑰钮,所有的操作都是串行化執(zhí)行的冒滩,不會有幾個client同時觸發(fā)的情況
3、可以用lua腳本模擬其他數(shù)據(jù)庫的事物浪谴,可將多個連續(xù)操作封裝成一個原子操作开睡,提交給redis
接下來還是上代碼(一直覺得代碼注釋的形式勝過一行行口述):
# 單例的元類,保證被構(gòu)造的類傳入相同的參數(shù)時只實(shí)例化1次
class SingleCache(type):
# 初始化一個緩存字典苟耻,用于緩存instance
def __init__(cls, name, bases, dct: dict):
super().__init__(name, bases, dct)
cls.__cache_dict = {}
# 連接池綁定為類屬性篇恒,供實(shí)例訪問
cls.pool = redis.ConnectionPool(
host='127.0.0.1', port='6379', password='',
max_connections=20, decode_responses=True
)
# 類的實(shí)例化時調(diào)用
def __call__(cls, *args, **kwargs):
cache_tuple = args + tuple(sorted(kwargs.items()))
if cache_tuple not in cls.__cache_dict:
cls.__cache_dict[cache_tuple] = type.__call__(cls, *args, **kwargs)
return cls.__cache_dict[cache_tuple]
class RedisLockUtil(metaclass=SingleCache):
def __init__(self):
# 復(fù)用類屬性連接池(聽說這個連接池源碼有些問題,但是使用中還好凶杖,沒發(fā)現(xiàn)太大問題胁艰,可能場景比較有限)
self.pool = self.pool
# 從連接池中獲取一個連接
def get_conn(self):
r = redis.Redis(connection_pool=self.pool)
return r
# 給資源加鎖,resource_id為資源id智蝠,request_id為當(dāng)前操作者的唯一id腾么,會把resource_id作為key,request_id及一些簡單的用戶信息作為value存入redis
def add_lock(self, resource_id: str, request_id: str, ex=30):
# 因?yàn)轭A(yù)設(shè)了獲取鎖成功后寻咒,返回值是1哮翘,失敗后返回是,所以
assert(request_id != '1'), f'1為腳本默認(rèn)返回值毛秘,所以請求id不能為1'
user_info = {'user_id': '1', 'user_name': '張三'}
value = f'{request_id}|{json.dumps(user_info, ensure_ascii=False)}'
lua_script_list = [
# 查看request_id是否已經(jīng)存在
'local _value = redis.call("get", KEYS[1]);',
# 如果不存在,就設(shè)置阻课,并配置過期時間叫挟,默認(rèn)30秒,然后返回字符串1
'if(_value==false)',
'then',
'redis.call("set",KEYS[1],ARGV[1]);',
'redis.call("expire",KEYS[1],ARGV[3]);',
'return "1";',
'end;',
# 如果存在限煞,則key的結(jié)構(gòu)是這樣的:request_id|user_info抹恳,豎線分隔,找到豎線索引署驻,取出request_id和user_info
'local split_index = string.find(_value, "|");',
'local request_id = string.sub(_value,0,split_index-1);',
# 如果請求id和當(dāng)前value中的一致奋献,則可以獲取鎖健霹,返回字符串1,否則不能獲取鎖
'if(request_id == ARGV[2])',
'then',
'return "1";',
'end;',
# 如果request_id不同瓶蚂,說明獲取鎖失敗糖埋,此時把占用當(dāng)前鎖的用戶信息返回
'local user_info = string.sub(_value,split_index+1);',
'return user_info;'
]
# lua腳本從列表轉(zhuǎn)到字符串
lua_script = '\n'.join(lua_script_list)
conn = self.get_conn()
# 注冊lua腳本
_script = conn.register_script(lua_script)
# 傳入所需參數(shù),keys對應(yīng)的是lua腳本中的KEYS窃这,args對應(yīng)的是lua腳本中的ARGV瞳别,注意下標(biāo)索引是從1開始,而非0
result = _script(keys=[resource_id], args=[value, request_id, ex])
# 如果沒有返回1杭攻,說明獲取鎖失敗祟敛,拋出異常
if result != "1":
user_info = json.loads(result)
msg = f'當(dāng)前資源id:{resource_id}已被占用,占用人信息:user_id:%(user_id)s,user_name:%(user_name)s' % user_info
raise Exception(msg)
print(f'加鎖成功兆解,resource_id:{resource_id},request_id:{request_id}')
# 釋放鎖
def release_lock(self, resource_id: str, request_id: str):
lua_script_list = [
# 檢查resource_id是否存在
'local _value = redis.call("get", KEYS[1]);',
# resource_id不存在的返回字符串0
'if(_value==false)',
'then',
'return "0";',
'end;',
# 如果resource_id存在馆铁,則比較request_id是否相同,如果相同锅睛,則可以釋放
'local split_index = string.find(_value, "|");',
'local request_id = string.sub(_value,0,split_index-1);',
'if(request_id == ARGV[1])',
'then',
'redis.call("del", KEYS[1]);',
'return "1";',
'end;',
# 如果request_id不同埠巨,釋放鎖失敗,返回當(dāng)前request_id
'return ARGV[1];'
]
# lua腳本從列表轉(zhuǎn)到字符串
lua_script = '\n'.join(lua_script_list)
conn = self.get_conn()
# 注冊lua腳本
_script = conn.register_script(lua_script)
# 傳入所需參數(shù)衣撬,keys對應(yīng)的是lua腳本中的KEYS乖订,args對應(yīng)的是lua腳本中的ARGV,注意下標(biāo)索引是從1開始具练,而非0
result = _script(keys=[resource_id], args=[request_id])
# 如果返回0乍构,說明資源已經(jīng)不存在,無需釋放鎖
if result == '0':
print(f"資源id為:{resource_id}的鎖已不存在")
# 如果返回1扛点,說明已經(jīng)成功釋放鎖
elif result == '1':
print(f"資源id為:{resource_id}的鎖已釋放")
# 如果返回其他哥遮,說明request_id不符,無法釋放鎖
else:
print(f"解鎖請求id為:{request_id},與加鎖id:{result}不同陵究,無法釋放鎖")
msg = '資源釋放失敗'
raise Exception(msg)
以上就是完整代碼眠饮,可根據(jù)實(shí)際場景做一些增加或是刪除,但基本思想已經(jīng)表達(dá)的足夠清晰了铜邮,接下來我們做幾個測試仪召,看看實(shí)際執(zhí)行效果:
rlu = RedisLockUtil()
rlu.add_lock(resource_id='lirui123', request_id='123')
# 加鎖成功,resource_id:lirui123,request_id:123
rlu.release_lock(resource_id='lirui123', request_id='123')
# 資源id為:lirui123的鎖已釋放
rlu.add_lock(resource_id='lirui123', request_id='123')
# 加鎖成功松蒜,resource_id:lirui123,request_id:123
rlu.release_lock(resource_id='lirui123', request_id='1234')
# Traceback (most recent call last):
# File "redis_lock2.py", line 125, in <module>
# rlu.release_lock(resource_id='lirui123', request_id='1234')
# File "redis_lock2.py", line 116, in release_lock
# raise Exception(msg)
# Exception: 資源釋放失敗
rlu.release_lock(resource_id='lirui123', request_id='123')
# 資源id為:lirui123的鎖已釋放
rlu.add_lock(resource_id='lirui123', request_id='123', ex=5)
# 加鎖成功扔茅,resource_id:lirui123,request_id:123
import time
time.sleep(5)
rlu.release_lock(resource_id='lirui123', request_id='123')
# 資源id為:lirui123的鎖已不存在
rlu.add_lock(resource_id='lirui123', request_id='123')
# 加鎖成功,resource_id:lirui123,request_id:123
rlu.add_lock(resource_id='lirui123', request_id='1234')
# File "redis_lock2.py", line 122, in <module>
# rlu.add_lock(resource_id='lirui123', request_id='1234')
# File "redis_lock2.py", line 75, in add_lock
# raise Exception(msg)
# Exception: 當(dāng)前資源id:lirui123已被占用秸苗,占用人信息:user_id:1,user_name:張三
以上召娜,基本覆蓋了平時用鎖的基本場景,基本都可以cover住惊楼。不過對于這種用法玖瘸,還存在一個問題:當(dāng)redis是集群時秸讹,如果某個node掛掉藏畅,因?yàn)閞edis數(shù)據(jù)是異步同步策略登夫,有一段時間數(shù)據(jù)會不完全,這會讓鎖異掣礁耄或者失效屯断。
然而這種屬于小概率事件了文虏,對于我這種懶人來說,可以暫時不用考慮了~~