redis分布式鎖(基于springboot實(shí)現(xiàn))

在公司的項(xiàng)目中用到了分布式鎖褥伴,但只會(huì)用卻不明白其中的規(guī)則

所以寫一篇文章來記錄

使用場景:交易服務(wù)蟹地,使用redis分布式鎖挫剑,防止重復(fù)提交訂單贯莺,出現(xiàn)超賣問題

分布式鎖應(yīng)該具備哪些條件

  • 在分布式系統(tǒng)環(huán)境下风喇,一個(gè)方法在同一時(shí)間只能被一個(gè)機(jī)器的一個(gè)線程執(zhí)行
  • 高可用的獲取鎖與釋放鎖
  • 高性能的獲取鎖與釋放鎖
  • 具備可重入特性(可理解為重新進(jìn)入,由多于一個(gè)任務(wù)并發(fā)使用缕探,而不必?fù)?dān)心數(shù)據(jù)錯(cuò)誤)
  • 具備鎖失效機(jī)制魂莫,防止死鎖
  • 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗

分布式鎖的實(shí)現(xiàn)方式

  1. 基于數(shù)據(jù)庫樂觀鎖/悲觀鎖
  2. Redis分布式鎖(本文):利用setnx命令爹耗。此命令是原子性操作耙考,只有key不存在的情況下,才能set潭兽,就意味著線程獲取到了鎖
  3. Zookeeper分布式鎖:利用 Zookeeper 的順序臨時(shí)節(jié)點(diǎn)琳骡,來實(shí)現(xiàn)分布式鎖和等待隊(duì)列。Zookeeper 設(shè)計(jì)的初衷讼溺,就是為了實(shí)現(xiàn)分布式鎖服務(wù)的
  4. Memcached:利用add命令。此命令是原子性操作最易,只有key不存在的情況下怒坯,才能add,也就意味著線程獲取到了鎖

redis是如何實(shí)現(xiàn)加鎖的藻懒?

在redis中剔猿,有一條命令,實(shí)現(xiàn)鎖

SETNX key value

該命令的作用是將 key 的值設(shè)為 value 嬉荆,當(dāng)且僅當(dāng) key 不存在归敬。若給定的 key 已經(jīng)存在,則 SETNX 不做任何動(dòng)作。設(shè)置成功汪茧,返回 1 椅亚;設(shè)置失敗,返回 0

使用 redis 來實(shí)現(xiàn)鎖的邏輯就是這樣的

線程 1 獲取鎖  -- > setnx lockKey lockvalue
              -- >  1  獲取鎖成功
線程 2 獲取鎖  -- > setnx lockKey lockvalue 
              -- >  0  獲取鎖失敗  (繼續(xù)等待舱污,或者其他邏輯)
線程 1 釋放鎖  -- > 
線程 2 獲取鎖  -- > setnx lockKey lockvalue
              -- > 1 獲取成功

接下來我們將基于springboot實(shí)現(xiàn)redis分布式鎖

1. 引入redis呀舔、springmvc、lombok依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.miao.redis</groupId>
    <artifactId>springboot-caffeine-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-redis-lock-demo</name>
    <description>Demo project for Redis Distribute Lock</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.4.RELEASE</version>
        </dependency>

        <!--springMvc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2. 新建RedisDistributedLock.java并書寫加鎖解鎖邏輯

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;

import java.nio.charset.StandardCharsets;

/**
 * @author miao
 * redis 加鎖工具類
 */
@Slf4j
public class RedisDistributedLock {

    /**
     * 超時(shí)時(shí)間
     */
    private static final long TIMEOUT_MILLIS = 15000;

    /**
     * 重試次數(shù)
     */
    private static final int RETRY_TIMES = 10;

    /***
     * 睡眠時(shí)間
     */
    private static final long SLEEP_MILLIS = 500;

    /**
     * 用來加鎖的lua腳本
     * 因?yàn)樾掳娴膔edis加鎖操作已經(jīng)為原子性操作
     * 所以放棄使用lua腳本
     */
    private static final String LOCK_LUA =
            "if redis.call(\"setnx\",KEYS[1],ARGV[1]) == 1 " +
                    "then " +
                    "    return redis.call('expire',KEYS[1],ARGV[2]) " +
                    "else " +
                    "    return 0 " +
                    "end";

    /**
     * 用來釋放分布式鎖的lua腳本
     * 如果redis.get(KEYS[1]) == ARGV[1],則redis delete KEYS[1]
     * 否則返回0
     * KEYS[1] , ARGV[1] 是參數(shù)扩灯,我們只調(diào)用的時(shí)候 傳遞這兩個(gè)參數(shù)就可以了
     * KEYS[1] 主要用來傳遞在redis 中用作key值的參數(shù)
     * ARGV[1] 主要用來傳遞在redis中用做 value值的參數(shù)
     */
    private static final String UNLOCK_LUA =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1] "
                    + "then "
                    + "    return redis.call(\"del\",KEYS[1]) "
                    + "else "
                    + "    return 0 "
                    + "end ";

    /**
     * 檢查 redisKey 是否上鎖
     *
     * @param redisKey redisKey
     * @param template template
     * @return Boolean
     */
    public static Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template) {

        return lock(redisKey, value, template, RETRY_TIMES);
    }

    private static Boolean lock(String redisKey,
                                String value,
                                RedisTemplate<Object, Object> template,
                                int retryTimes) {

        boolean result = lockKey(redisKey, value, template);

        while (!(result) && retryTimes-- > 0) {
            try {

                log.debug("lock failed, retrying...{}", retryTimes);
                Thread.sleep(RedisDistributedLock.SLEEP_MILLIS);
            } catch (InterruptedException e) {

                return false;
            }
            result = lockKey(redisKey, value, template);
        }

        return result;
    }


    private static Boolean lockKey(final String key,
                                   final String value,
                                   RedisTemplate<Object, Object> template) {
        try {

            RedisCallback<Boolean> callback = (connection) -> connection.set(
                    key.getBytes(StandardCharsets.UTF_8),
                    value.getBytes(StandardCharsets.UTF_8),
                    Expiration.milliseconds(RedisDistributedLock.TIMEOUT_MILLIS),
                    RedisStringCommands.SetOption.SET_IF_ABSENT
            );

            return template.execute(callback);
        } catch (Exception e) {

            log.info("lock key fail because of ", e);
        }

        return false;
    }


    /**
     * 釋放分布式鎖資源
     *
     * @param redisKey key
     * @param value    value
     * @param template redis
     * @return Boolean
     */
    public static Boolean releaseLock(String redisKey,
                                      String value,
                                      RedisTemplate<Object, Object> template) {
        try {
            RedisCallback<Boolean> callback = (connection) -> connection.eval(
                    UNLOCK_LUA.getBytes(),
                    ReturnType.BOOLEAN,
                    1,
                    redisKey.getBytes(StandardCharsets.UTF_8),
                    value.getBytes(StandardCharsets.UTF_8)
            );

            return template.execute(callback);
        } catch (Exception e) {

            log.info("release lock fail because of ", e);
        }

        return false;
    }

}

補(bǔ)充:

1. spring-data-redisStringRedisTemplaRedisTemplate兩種媚赖,但是我選擇了RedisTemplate,因?yàn)樗容^萬能珠插。他們的區(qū)別是:當(dāng)你的redis數(shù)據(jù)庫里面本來存的是字符串?dāng)?shù)據(jù)或者你要存取的數(shù)據(jù)就是字符串類型數(shù)據(jù)的時(shí)候惧磺,那么你就使用StringRedisTemplate即可, 但是如果你的數(shù)據(jù)是復(fù)雜的對(duì)象類型捻撑,而取出的時(shí)候又不想做任何的數(shù)據(jù)轉(zhuǎn)換磨隘,直接從Redis里面取出一個(gè)對(duì)象,那么使用RedisTemplate是 更好的選擇布讹。
2. 選擇lua腳本是因?yàn)榱帐茫_本運(yùn)行是原子性的,在腳本運(yùn)行期間沒有客戶端可以操作描验,所以在釋放鎖的時(shí)候用了lua腳本白嘁,
而redis最新版加鎖時(shí)保證了Redis值和自動(dòng)過期時(shí)間的原子性,所用沒用lua腳本

3. 創(chuàng)建測試類 TestController

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author miao
 */
@RestController
@Slf4j
public class TestController {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    @PostMapping("/order")
    public String createOrder() throws InterruptedException {

        log.info("開始創(chuàng)建訂單");

        Boolean isLock = RedisDistributedLock.isLock("testLock", "456789", redisTemplate);

        if (!isLock) {

            log.info("鎖已經(jīng)被占用");
            return "fail";
        } else {
            //.....處理邏輯
        }

        Thread.sleep(10000);
        //一定要記得釋放鎖膘流,否則會(huì)出現(xiàn)問題
        RedisDistributedLock.releaseLock("testLock", "456789", redisTemplate);

        return "success";
    }
}

4. 使用postman進(jìn)行測試

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

5. redis分布式鎖的缺點(diǎn)

上面我們說的是redis絮缅,是單點(diǎn)的情況。如果是在redis sentinel集群中情況就有所不同了呼股。在redis sentinel集群中耕魄,我們具有多臺(tái)redis,他們之間有著主從的關(guān)系彭谁,例如一主二從吸奴。我們的set命令對(duì)應(yīng)的數(shù)據(jù)寫到主庫,然后同步到從庫缠局。當(dāng)我們申請(qǐng)一個(gè)鎖的時(shí)候则奥,對(duì)應(yīng)就是一條命令 setnx mykey myvalue ,在redis sentinel集群中狭园,這條命令先是落到了主庫读处。假設(shè)這時(shí)主庫down了,而這條數(shù)據(jù)還沒來得及同步到從庫唱矛,sentinel將從庫中的一臺(tái)選舉為主庫了罚舱。這時(shí)井辜,我們的新主庫中并沒有mykey這條數(shù)據(jù),若此時(shí)另外一個(gè)client執(zhí)行 setnx mykey hisvalue , 也會(huì)成功管闷,即也能得到鎖粥脚。這就意味著,此時(shí)有兩個(gè)client獲得了鎖渐北。這不是我們希望看到的阿逃,雖然這個(gè)情況發(fā)生的記錄很小,只會(huì)在主從failover的時(shí)候才會(huì)發(fā)生赃蛛,大多數(shù)情況下恃锉、大多數(shù)系統(tǒng)都可以容忍,但是不是所有的系統(tǒng)都能容忍這種瑕疵呕臂。

6.redis分布式鎖的優(yōu)化

為了解決故障轉(zhuǎn)移情況下的缺陷破托,Antirez 發(fā)明了 Redlock 算法,使用redlock算法歧蒋,需要多個(gè)redis實(shí)例土砂,加鎖的時(shí)候,它會(huì)想多半節(jié)點(diǎn)發(fā)送 setex mykey myvalue 命令谜洽,只要過半節(jié)點(diǎn)成功了萝映,那么就算加鎖成功了。釋放鎖的時(shí)候需要想所有節(jié)點(diǎn)發(fā)送del命令阐虚。這是一種基于【大多數(shù)都同意】的一種機(jī)制序臂。感興趣的可以查詢相關(guān)資料。在實(shí)際工作中使用的時(shí)候实束,我們可以選擇已有的開源實(shí)現(xiàn)奥秆,python有redlock-py咸灿,java 中有Redisson redlock构订。

redlock確實(shí)解決了上面所說的“不靠譜的情況”。但是避矢,它解決問題的同時(shí)悼瘾,也帶來了代價(jià)。你需要多個(gè)redis實(shí)例审胸,你需要引入新的庫 代碼也得調(diào)整分尸,性能上也會(huì)有損。所以歹嘹,果然是不存在“完美的解決方案”,我們更需要的是能夠根據(jù)實(shí)際的情況和條件把問題解決了就好孔庭。

至此尺上,我大致講清楚了redis分布式鎖方面的問題(日后如果有新的領(lǐng)悟就繼續(xù)更新)材蛛。


** 分布式鎖對(duì)比**

數(shù)據(jù)庫分布式鎖實(shí)現(xiàn)

缺點(diǎn):

  1. db操作性能較差,并且有鎖表的風(fēng)險(xiǎn)
  2. 非阻塞操作失敗后怎抛,需要輪詢卑吭,占用cpu資源
  3. 長時(shí)間不commit或者長時(shí)間輪詢,可能會(huì)占用較多連接資源

Redis分布式鎖實(shí)現(xiàn)

缺點(diǎn):

  1. 鎖刪除失敗 過期時(shí)間不好控制
  2. 非阻塞马绝,操作失敗后豆赏,需要輪詢,占用cpu資源

ZK分布式鎖實(shí)現(xiàn)

缺點(diǎn):

  1. 性能不如redis實(shí)現(xiàn)富稻,主要原因是寫操作(獲取鎖釋放鎖)都需要在Leader上執(zhí)行掷邦,然后同步到follower。

總結(jié)

從理解的難易程度角度(從低到高)數(shù)據(jù)庫 > Redis >Zookeeper
從實(shí)現(xiàn)的復(fù)雜性角度(從低到高)Zookeeper >= Redis > 數(shù)據(jù)庫
從性能角度(從高到低)緩存 > Zookeeper >= 數(shù)據(jù)庫
從可靠性角度(從高到低)Zookeeper > Redis > 數(shù)據(jù)庫

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末椭赋,一起剝皮案震驚了整個(gè)濱河市抚岗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哪怔,老刑警劉巖宣蔚,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異认境,居然都是意外死亡胚委,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門叉信,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亩冬,“玉大人,你說我怎么就攤上這事茉盏〖矗” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵鸠姨,是天一觀的道長铜秆。 經(jīng)常有香客問我,道長讶迁,這世上最難降的妖魔是什么连茧? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮巍糯,結(jié)果婚禮上啸驯,老公的妹妹穿的比我還像新娘。我一直安慰自己祟峦,他們只是感情好罚斗,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宅楞,像睡著了一般针姿。 火紅的嫁衣襯著肌膚如雪袱吆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天距淫,我揣著相機(jī)與錄音绞绒,去河邊找鬼。 笑死榕暇,一個(gè)胖子當(dāng)著我的面吹牛蓬衡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播彤枢,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼狰晚,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了堂污?” 一聲冷哼從身側(cè)響起家肯,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盟猖,沒想到半個(gè)月后讨衣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡式镐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年反镇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娘汞。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡歹茶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出你弦,到底是詐尸還是另有隱情惊豺,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布禽作,位于F島的核電站尸昧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏旷偿。R本人自食惡果不足惜烹俗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望萍程。 院中可真熱鬧幢妄,春花似錦、人聲如沸茫负。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忍法。三九已至潮尝,卻和暖如春无虚,著一層夾襖步出監(jiān)牢的瞬間徘铝,已是汗流浹背聂宾。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國打工瓜饥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人戴质。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像踢匣,于是被迫代替她去往敵國和親告匠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

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