springmvc單Redis實(shí)例實(shí)現(xiàn)分布式鎖(解決鎖超時(shí)問(wèn)題)

一思瘟、前言

關(guān)于redis分布式鎖, 查了很多資料, 發(fā)現(xiàn)很多只是實(shí)現(xiàn)了最基礎(chǔ)的功能, 但是, 并沒(méi)有解決當(dāng)鎖已超時(shí)而業(yè)務(wù)邏輯還未執(zhí)行完的問(wèn)題, 這樣會(huì)導(dǎo)致: A線程超時(shí)時(shí)間設(shè)為10s(為了解決死鎖問(wèn)題), 但代碼執(zhí)行時(shí)間可能需要30s, 然后redis服務(wù)端10s后將鎖刪除, 此時(shí), B線程恰好申請(qǐng)鎖, redis服務(wù)端不存在該鎖, 可以申請(qǐng), 也執(zhí)行了代碼, 那么問(wèn)題來(lái)了, A须揣、B線程都同時(shí)獲取到鎖并執(zhí)行業(yè)務(wù)邏輯, 這與分布式鎖最基本的性質(zhì)相違背: 在任意一個(gè)時(shí)刻, 只有一個(gè)客戶端持有鎖, 即獨(dú)享

為了解決這個(gè)問(wèn)題, 本文將用完整的代碼和測(cè)試用例進(jìn)行驗(yàn)證, 希望能給小伙伴帶來(lái)一點(diǎn)幫助

二稿黄、準(zhǔn)備工作

  1. 壓測(cè)工具jmeter
    下載鏈接
    提取碼: 8f2a

  2. redis-desktop-manager客戶端
    下載鏈接
    提取碼: 9bhf

  3. postman
    下載鏈接
    提取碼: vfu7

也可以直接官網(wǎng)下載, 我這邊都整理到網(wǎng)盤了

需要postman是因?yàn)槲疫€沒(méi)找到j(luò)meter多開窗口的辦法, 哈哈

三、說(shuō)明

  1. springmvc項(xiàng)目

  2. maven依賴

        <!--redis-->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>1.6.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.7.3</version>
        </dependency>
  1. 核心類
  • 分布式鎖工具類: DistributedLock

  • 測(cè)試接口類: PcInformationServiceImpl

  • 鎖延時(shí)守護(hù)線程類: PostponeTask

四唇撬、實(shí)現(xiàn)思路

  1. 先測(cè)試在不開啟鎖延時(shí)線程的情況下, A線程超時(shí)時(shí)間設(shè)為10s, 執(zhí)行業(yè)務(wù)邏輯時(shí)間設(shè)為30s, 10s后, 調(diào)用接口, 查看是否能夠獲取到鎖, 如果獲取到, 說(shuō)明存在線程安全性問(wèn)題

  2. 同上, 在加鎖的同時(shí), 開啟鎖延時(shí)線程, 調(diào)用接口, 查看是否能夠獲取到鎖, 如果獲取不到, 說(shuō)明延時(shí)成功, 安全性問(wèn)題解決

五拴鸵、實(shí)現(xiàn)

  1. 版本01代碼

1)、DistributedLock

package com.cn.pinliang.common.util;

import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import java.io.Serializable;
import java.util.Collections;

@Component
public class DistributedLock {

    @Autowired
    private RedisTemplate<Serializable, Object> redisTemplate;

    private static final Long RELEASE_SUCCESS = 1L;

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    // 解鎖腳本(lua)
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /**
     * 分布式鎖
     * @param key
     * @param value
     * @param expireTime 單位: 秒
     * @return
     */
    public boolean lock(String key, String value, long expireTime) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (LOCK_SUCCESS.equals(result)) {
                return Boolean.TRUE;
            }
            return Boolean.FALSE;
        });
    }

    /**
     * 解鎖
     * @param key
     * @param value
     * @return
     */
    public Boolean unLock(String key, String value) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
            if (RELEASE_SUCCESS.equals(result)) {
                return Boolean.TRUE;
            }
            return Boolean.FALSE;
        });
    }

}

說(shuō)明: 就2個(gè)方法, 加鎖解鎖, 加鎖使用jedis setnx方法, 解鎖執(zhí)行l(wèi)ua腳本, 都是原子性操作

2)撇叁、PcInformationServiceImpl

    public JsonResult add() throws Exception {
        String key = "add_information_lock";
        String value = RandomUtil.produceStringAndNumber(10);
        long expireTime = 10L;

        boolean lock = distributedLock.lock(key, value, expireTime);
        String threadName = Thread.currentThread().getName();
        if (lock) {
            System.out.println(threadName + " 獲得鎖...............................");
            Thread.sleep(30000);
            distributedLock.unLock(key, value);
            System.out.println(threadName + " 解鎖了...............................");
        } else {
            System.out.println(threadName + " 未獲取到鎖...............................");
            return JsonResult.fail("未獲取到鎖");
        }

        return JsonResult.succeed();
    }

說(shuō)明: 測(cè)試類很簡(jiǎn)單, value隨機(jī)生成, 保證唯一, 不會(huì)在超時(shí)情況下解鎖其他客戶端持有的鎖

3)供鸠、打開redis-desktop-manager客戶端, 刷新緩存, 可以看到, 此時(shí)是沒(méi)有add_information_lock的key的

image.png

4)、啟動(dòng)jmeter, 調(diào)用接口測(cè)試
設(shè)置5個(gè)線程同時(shí)訪問(wèn), 在10s的超時(shí)時(shí)間內(nèi)查看redis, add_information_lock存在, 多次調(diào)接口, 只有一個(gè)線程能夠獲取到鎖

redis


image.png

1-4個(gè)請(qǐng)求, 都未獲取到鎖


image.png

第5個(gè)請(qǐng)求, 獲取到鎖


image.png

OK, 目前為止, 一切正常, 接下來(lái)測(cè)試10s之后, A仍在執(zhí)行業(yè)務(wù)邏輯, 看別的線程是否能獲取到鎖


image.png

可以看到, 操作成功, 說(shuō)明A和B同時(shí)執(zhí)行了這段本應(yīng)該獨(dú)享的代碼, 需要優(yōu)化

  1. 版本02代碼
    1)陨闹、DistributedLock
package com.cn.pinliang.common.util;

import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import java.io.Serializable;
import java.util.Collections;

@Component
public class DistributedLock {

    @Autowired
    private RedisTemplate<Serializable, Object> redisTemplate;

    private static final Long RELEASE_SUCCESS = 1L;
    private static final Long POSTPONE_SUCCESS = 1L;

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    // 解鎖腳本(lua)
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    // 延時(shí)腳本
    private static final String POSTPONE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";

    /**
     * 分布式鎖
     * @param key
     * @param value
     * @param expireTime 單位: 秒
     * @return
     */
    public boolean lock(String key, String value, long expireTime) {
        // 加鎖
        Boolean locked = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (LOCK_SUCCESS.equals(result)) {
                return Boolean.TRUE;
            }
            return Boolean.FALSE;
        });

        if (locked) {
            // 加鎖成功, 啟動(dòng)一個(gè)延時(shí)線程, 防止業(yè)務(wù)邏輯未執(zhí)行完畢就因鎖超時(shí)而使鎖釋放
            PostponeTask postponeTask = new PostponeTask(key, value, expireTime, this);
            Thread thread = new Thread(postponeTask);
            thread.setDaemon(Boolean.TRUE);
            thread.start();
        }

        return locked;
    }

    /**
     * 解鎖
     * @param key
     * @param value
     * @return
     */
    public Boolean unLock(String key, String value) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
            if (RELEASE_SUCCESS.equals(result)) {
                return Boolean.TRUE;
            }
            return Boolean.FALSE;
        });
    }

    /**
     * 鎖延時(shí)
     * @param key
     * @param value
     * @param expireTime
     * @return
     */
    public Boolean postpone(String key, String value, long expireTime) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object result = jedis.eval(POSTPONE_LOCK_SCRIPT, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));
            if (POSTPONE_SUCCESS.equals(result)) {
                return Boolean.TRUE;
            }
            return Boolean.FALSE;
        });
    }

}

說(shuō)明: 新增了鎖延時(shí)方法, lua腳本, 自行腦補(bǔ)相關(guān)語(yǔ)法

2)楞捂、PcInformationServiceImpl不需要改動(dòng)

3)、PostponeTask

package com.cn.pinliang.common.thread;

import com.cn.pinliang.common.util.DistributedLock;

public class PostponeTask implements Runnable {

    private String key;
    private String value;
    private long expireTime;
    private boolean isRunning;
    private DistributedLock distributedLock;

    public PostponeTask() {
    }

    public PostponeTask(String key, String value, long expireTime, DistributedLock distributedLock) {
        this.key = key;
        this.value = value;
        this.expireTime = expireTime;
        this.isRunning = Boolean.TRUE;
        this.distributedLock = distributedLock;
    }

    @Override
    public void run() {
        long waitTime = expireTime * 1000 * 2 / 3;// 線程等待多長(zhǎng)時(shí)間后執(zhí)行
        while (isRunning) {
            try {
                Thread.sleep(waitTime);
                if (distributedLock.postpone(key, value, expireTime)) {
                    System.out.println("延時(shí)成功...........................................................");
                } else {
                    this.stop();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void stop() {
        this.isRunning = Boolean.FALSE;
    }

}

說(shuō)明: 調(diào)用lock同時(shí), 立即開啟PostponeTask線程, 線程等待超時(shí)時(shí)間的2/3時(shí)間后, 開始執(zhí)行鎖延時(shí)代碼, 如果延時(shí)成功, add_information_lock這個(gè)key會(huì)一直存在于redis服務(wù)端, 直到業(yè)務(wù)邏輯執(zhí)行完畢, 因此在此過(guò)程中, 其他線程無(wú)法獲取到鎖, 也即保證了線程安全性

下面是測(cè)試結(jié)果

10s后, 查看redis服務(wù)端, add_information_lock仍存在, 說(shuō)明延時(shí)成功

image.png

此時(shí)用postman再次請(qǐng)求, 發(fā)現(xiàn)獲取不到鎖


image.png

看一下控制臺(tái)打印


image.png
image.png

A線程在19:09:11獲取到鎖, 在10 * 2 / 3 = 6s后進(jìn)行延時(shí), 成功, 保證了業(yè)務(wù)邏輯未執(zhí)行完畢的情況下不會(huì)釋放鎖

A線程執(zhí)行完畢, 鎖釋放, 其他線程又可以競(jìng)爭(zhēng)鎖

OK, 目前為止, 解決了鎖超時(shí)而業(yè)務(wù)邏輯仍在執(zhí)行的鎖沖突問(wèn)題, 還很簡(jiǎn)陋, 而最嚴(yán)謹(jǐn)?shù)姆绞竭€是使用官方的 Redlock 算法實(shí)現(xiàn), 其中 Java 包推薦使用 redisson, 思路差不多其實(shí), 都是在快要超時(shí)時(shí)續(xù)期, 以保證業(yè)務(wù)邏輯未執(zhí)行完畢不會(huì)有其他客戶端持有鎖

后面學(xué)習(xí)redisson, 看一下大神是怎么實(shí)現(xiàn)的

如果有什么不對(duì)的或者可以優(yōu)化的希望小伙伴多多指教, 留言評(píng)論什么的, 謝謝

參考文章: https://blog.csdn.net/weixin_33943347/article/details/88009397

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末趋厉,一起剝皮案震驚了整個(gè)濱河市蟆技,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌通惫,老刑警劉巖服鹅,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異杈绸,居然都是意外死亡帖蔓,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門瞳脓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)塑娇,“玉大人,你說(shuō)我怎么就攤上這事劫侧÷癯辏” “怎么了哨啃?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)写妥。 經(jīng)常有香客問(wèn)我拳球,道長(zhǎng),這世上最難降的妖魔是什么珍特? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任祝峻,我火速辦了婚禮,結(jié)果婚禮上扎筒,老公的妹妹穿的比我還像新娘莱找。我一直安慰自己,他們只是感情好嗜桌,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布奥溺。 她就那樣靜靜地躺著,像睡著了一般骨宠。 火紅的嫁衣襯著肌膚如雪浮定。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天诱篷,我揣著相機(jī)與錄音壶唤,去河邊找鬼。 笑死棕所,一個(gè)胖子當(dāng)著我的面吹牛闸盔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琳省,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼迎吵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了针贬?” 一聲冷哼從身側(cè)響起击费,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎桦他,沒(méi)想到半個(gè)月后蔫巩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡快压,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年圆仔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蔫劣。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坪郭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脉幢,到底是詐尸還是另有隱情歪沃,我是刑警寧澤嗦锐,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站沪曙,受9級(jí)特大地震影響奕污,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜珊蟀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一菊值、第九天 我趴在偏房一處隱蔽的房頂上張望外驱。 院中可真熱鬧育灸,春花似錦、人聲如沸昵宇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瓦哎。三九已至砸喻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蒋譬,已是汗流浹背割岛。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留犯助,地道東北人癣漆。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像剂买,于是被迫代替她去往敵國(guó)和親惠爽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348