手寫Redis分布式鎖

一 前言

我為什么要寫分布式鎖呢,最近工作中寫一個(gè)查詢接口,因?yàn)檫壿嫃?fù)雜,不希望用戶不停的點(diǎn)擊,需要過濾掉重復(fù)的請求。
簡單的需求:用戶每3秒只能請求一次,否則拒絕。

這個(gè)實(shí)現(xiàn)很簡單。用戶第一次請求在redis記下標(biāo)記,設(shè)置3秒過期時(shí)間,下次用戶再請求判斷標(biāo)記是否過期,沒過期就拒絕請求,過期了,就重新設(shè)置標(biāo)記。

后來我在做加鎖這一塊的時(shí)候,腦袋里出來一個(gè)想法:不如寫一套分布式鎖吧!

二 加鎖

1 SETNX

SETNX key value當(dāng)不存在key時(shí)設(shè)置,相當(dāng)于SET命令,否則不做任何操作。

--- setIfAbsent函數(shù),當(dāng)不存在時(shí)設(shè)置成功(相當(dāng)于redis命令setnx)
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, "");
if (absent) {
     stringRedisTemplate.expire(key, 3L, TimeUnit.SECONDS);
} else {
    return "請求頻繁,請稍后再試。";
}

上邊代碼能夠?qū)崿F(xiàn)加鎖,但是有兩個(gè)問題需要改進(jìn):

  • set 和 expire 需要發(fā)送兩次請求,無形中增加了連接損耗
  • set 和 expire 不是原子操作,如果set成功后系統(tǒng)報(bào)錯(cuò),就會(huì)造成死鎖,給系統(tǒng)增加了隱患。

針對這兩個(gè)問題,引出下一節(jié)set命令的講解。

2 SET NX EX

SET key value [EX seconds] [PX milliseconds] [NX|XX]

從 Redis 2.6.12 版本開始, SET 命令的行為可以通過一系列參數(shù)來修改:

  • EX seconds-設(shè)置指定的終止時(shí)間,以秒為單位。
  • PX 毫秒 -設(shè)置指定的到期時(shí)間(以毫秒為單位)。
  • NX -僅設(shè)置不存在的密鑰。
  • XX -僅設(shè)置密鑰(如果已存在)。

注意:由于SET命令選項(xiàng)可以替換SETNX,SETEX,PSETEX,因此在Redis的未來版本中,這三個(gè)命令可能會(huì)被棄用并最終刪除。

介紹我最終使用的命令:SET key value NX EX second
該命令是使用Redis實(shí)現(xiàn)鎖定系統(tǒng)的簡單方法。當(dāng)key不存在時(shí)設(shè)置key為value并設(shè)置過期時(shí)間為second。

注意,value應(yīng)該使用隨機(jī)值,不應(yīng)該使用固定數(shù)據(jù)。這樣可以避免客戶端在到期時(shí)間之后嘗試刪除該鎖,但是刪除了后面獲得該鎖的另一個(gè)客戶端創(chuàng)建的鎖。

三 解鎖

3.1 EVAL

EVAL script numkeys key [key ...] arg [arg ...]·

eval命令用來執(zhí)行l(wèi)ua腳本,因?yàn)閘ua語言非常精小,redis內(nèi)置了對lua語言的支持,redis原子執(zhí)行Lua腳本。

  • script :用戶編寫的一段lua腳本
  • numkeys :傳入的KEYS參數(shù)數(shù)量
  • key :鍵,可以有多個(gè)
  • args :ARGS參數(shù),可以有多個(gè)

快速入門:以下命令展示了eval如何使用以及通常后面腳本中使用的元素

> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 key value
OK

以上有三點(diǎn)需要了解:

  • redis.call() : Lua腳本中調(diào)用redis命令
  • KEYS[1] :Lua使用KEYS訪問全局變量,從1開始
  • ARGV[1] :其他參數(shù)不應(yīng)該代表鍵名稱,可以通過Lua來訪問ARGV全局變量,與鍵非常相似。

3.2 解鎖腳本

有了第一節(jié)的知識鋪墊,我這里就直接拋出解鎖腳本。

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

上述腳本,獲取key是否等于value,如果一致,說明這把鎖是我自己加的,那么就刪除它,否則解鎖失敗。
該腳本應(yīng)使用 EVAL script 1 key value來使用。

四 看門狗??

上述加鎖解鎖腳本都已經(jīng)完成,已經(jīng)具備了分布式鎖的外觀!可是,別急,這里還有一個(gè)問題。

如果我程序執(zhí)行時(shí)間很長,超過了加鎖時(shí)長,那么等鎖過期后,其他線程就會(huì)加鎖成功,業(yè)務(wù)就會(huì)出問題。

我翻閱了Redisson分布式鎖源碼,發(fā)現(xiàn)Redisson內(nèi)部有一個(gè)Watch Dog的概念,加鎖成功后,啟動(dòng)一個(gè)線程每隔10秒會(huì)檢查鎖是否還存在,還存在的話就重新設(shè)置為30秒,這叫定時(shí)續(xù)約。

依照這一思路,我實(shí)現(xiàn)了自己的簡易版Watch Dog程序。

首先需要解決兩個(gè)問題:

  • 需要一個(gè)線程,跟隨主線程,當(dāng)主線程銷毀后,它也會(huì)跟著銷毀,這就需要我們的Daemon Thread啦。
  • 需要定時(shí)執(zhí)行,幸運(yùn)的是,Java提供了Timer和ScheduledExecutorService提供給我們做定時(shí)任務(wù)。

具體實(shí)現(xiàn)是,加鎖成功后,創(chuàng)建Daemon Thread放到Timer,設(shè)置執(zhí)行頻率,就可以實(shí)現(xiàn)我們的看門狗啦!

本文源碼在:https://gitee.com/hello-piper/LockDistributed ,如果有用就給我一個(gè)Star吧!

如果大家覺得有用,可以點(diǎn)贊、評論、收藏支持我哦!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容