redis分布式鎖(初版)

最近研究了一下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ù)會不完全,這會讓鎖異掣礁耄或者失效屯断。
然而這種屬于小概率事件了文虏,對于我這種懶人來說,可以暫時不用考慮了~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末殖演,一起剝皮案震驚了整個濱河市氧秘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趴久,老刑警劉巖丸相,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異彼棍,居然都是意外死亡灭忠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門座硕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弛作,“玉大人,你說我怎么就攤上這事华匾∮沉眨” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵蜘拉,是天一觀的道長萨西。 經(jīng)常有香客問我,道長旭旭,這世上最難降的妖魔是什么谎脯? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮持寄,結(jié)果婚禮上源梭,老公的妹妹穿的比我還像新娘。我一直安慰自己稍味,他們只是感情好咸产,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著仲闽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪僵朗。 梳的紋絲不亂的頭發(fā)上赖欣,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天屑彻,我揣著相機(jī)與錄音,去河邊找鬼顶吮。 笑死社牲,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悴了。 我是一名探鬼主播搏恤,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼湃交!你這毒婦竟也來了熟空?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤搞莺,失蹤者是張志新(化名)和其女友劉穎息罗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體才沧,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡迈喉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了温圆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挨摸。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖岁歉,靈堂內(nèi)的尸體忽然破棺而出得运,到底是詐尸還是另有隱情,我是刑警寧澤刨裆,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布澈圈,位于F島的核電站,受9級特大地震影響帆啃,放射性物質(zhì)發(fā)生泄漏瞬女。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一努潘、第九天 我趴在偏房一處隱蔽的房頂上張望诽偷。 院中可真熱鬧,春花似錦疯坤、人聲如沸报慕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眠冈。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蜗顽,已是汗流浹背布卡。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雇盖,地道東北人忿等。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像崔挖,于是被迫代替她去往敵國和親贸街。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容