1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        【110期】面試官:Redis分布式鎖如何解決鎖超時問題?

        共 9691字,需瀏覽 20分鐘

         ·

        2021-01-05 03:59

        程序員的成長之路
        互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享?
        關(guān)注


        閱讀本文大概需要 6.5 分鐘。

        來自:www.jianshu.com/p/39b3570d3b56

        一、前言

        關(guān)于redis分布式鎖, 查了很多資料, 發(fā)現(xiàn)很多只是實現(xiàn)了最基礎(chǔ)的功能, 但是, 并沒有解決當鎖已超時而業(yè)務(wù)邏輯還未執(zhí)行完的問題, 這樣會導致: A線程超時時間設(shè)為10s(為了解決死鎖問題), 但代碼執(zhí)行時間可能需要30s, 然后redis服務(wù)端10s后將鎖刪除, 此時, B線程恰好申請鎖, redis服務(wù)端不存在該鎖, 可以申請, 也執(zhí)行了代碼, 那么問題來了, A、B線程都同時獲取到鎖并執(zhí)行業(yè)務(wù)邏輯, 這與分布式鎖最基本的性質(zhì)相違背: 在任意一個時刻, 只有一個客戶端持有鎖, 即獨享。
        為了解決這個問題, 本文將用完整的代碼和測試用例進行驗證, 希望能給小伙伴帶來一點幫助

        二、準備工作

        壓測工具jmeter
        https://pan.baidu.com/share/init?surl=NN0c0tDYQjBTTPA-WTT3yg
        提取碼: 8f2a
        redis-desktop-manager客戶端
        https://pan.baidu.com/share/init?surl=NoJtZZZOXsk45aQYtveWbQ
        提取碼: 9bhf
        postman
        https://pan.baidu.com/share/init?surl=28sGJk4zxoOknAd-47hE7w
        提取碼: vfu7
        也可以直接官網(wǎng)下載, 我這邊都整理到網(wǎng)盤了
        需要postman是因為我還沒找到j(luò)meter多開窗口的辦法, 哈哈

        三、說明

        1、springmvc項目
        2、maven依賴
        ????????
        ????????<dependency>
        ????????????<groupId>org.springframework.datagroupId>
        ????????????<artifactId>spring-data-redisartifactId>
        ????????????<version>1.6.5.RELEASEversion>
        ????????dependency>
        ????????<dependency>
        ????????????<groupId>redis.clientsgroupId>
        ????????????<artifactId>jedisartifactId>
        ????????????<version>2.7.3version>
        ????????dependency>
        3、核心類
        • 分布式鎖工具類: DistributedLock

        • 測試接口類: PcInformationServiceImpl

        • 鎖延時守護線程類: PostponeTask

        四、實現(xiàn)思路

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

        五、實現(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?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;
        ????????});
        ????}

        }
        說明: 就2個方法, 加鎖解鎖, 加鎖使用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();
        ????}
        說明: 測試類很簡單, value隨機生成, 保證唯一, 不會在超時情況下解鎖其他客戶端持有的鎖

        3)、打開redis-desktop-manager客戶端, 刷新緩存, 可以看到, 此時是沒有add_information_lock的key的

        4)、啟動jmeter, 調(diào)用接口測試

        設(shè)置5個線程同時訪問, 在10s的超時時間內(nèi)查看redis, add_information_lock存在, 多次調(diào)接口, 只有一個線程能夠獲取到鎖
        redis
        1-4個請求, 都未獲取到鎖
        第5個請求, 獲取到鎖
        OK, 目前為止, 一切正常, 接下來測試10s之后, A仍在執(zhí)行業(yè)務(wù)邏輯, 看別的線程是否能獲取到鎖
        可以看到, 操作成功, 說明A和B同時執(zhí)行了這段本應(yīng)該獨享的代碼, 需要優(yōu)化。

        2、版本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?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";
        ????//?延時腳本
        ????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)?{
        ????????????//?加鎖成功,?啟動一個延時線程,?防止業(yè)務(wù)邏輯未執(zhí)行完畢就因鎖超時而使鎖釋放
        ????????????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;
        ????????});
        ????}

        ????/**
        ?????*?鎖延時
        ?????*?@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;
        ????????});
        ????}

        }
        說明: 新增了鎖延時方法, lua腳本, 自行腦補相關(guān)語法

        2)、PcInformationServiceImpl不需要改動

        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í)行
        ????????while?(isRunning)?{
        ????????????try?{
        ????????????????Thread.sleep(waitTime);
        ????????????????if?(distributedLock.postpone(key,?value,?expireTime))?{
        ????????????????????System.out.println("延時成功...........................................................");
        ????????????????}?else?{
        ????????????????????this.stop();
        ????????????????}
        ????????????}?catch?(Exception?e)?{
        ????????????????e.printStackTrace();
        ????????????}
        ????????}
        ????}

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

        }
        說明: 調(diào)用lock同時, 立即開啟PostponeTask線程, 線程等待超時時間的2/3時間后, 開始執(zhí)行鎖延時代碼, 如果延時成功, add_information_lock這個key會一直存在于redis服務(wù)端, 直到業(yè)務(wù)邏輯執(zhí)行完畢, 因此在此過程中, 其他線程無法獲取到鎖, 也即保證了線程安全性
        下面是測試結(jié)果
        10s后, 查看redis服務(wù)端, add_information_lock仍存在, 說明延時成功
        此時用postman再次請求, 發(fā)現(xiàn)獲取不到鎖
        看一下控制臺打印
        A線程在19:09:11獲取到鎖, 在10 * 2 / 3 = 6s后進行延時, 成功, 保證了業(yè)務(wù)邏輯未執(zhí)行完畢的情況下不會釋放鎖
        A線程執(zhí)行完畢, 鎖釋放, 其他線程又可以競爭鎖
        OK, 目前為止, 解決了鎖超時而業(yè)務(wù)邏輯仍在執(zhí)行的鎖沖突問題, 還很簡陋, 而最嚴謹?shù)姆绞竭€是使用官方的 Redlock 算法實現(xiàn), 其中 Java 包推薦使用 redisson, 思路差不多其實, 都是在快要超時時續(xù)期, 以保證業(yè)務(wù)邏輯未執(zhí)行完畢不會有其他客戶端持有鎖

        推薦閱讀:

        【109期】面試官:我們說StringBuilder是線程不安全的,是什么原因呢?

        【108期】面試官:你真的知道 Java 類是如何被加載的嗎?

        【107期】談?wù)劽嬖嚤貑柕腏ava內(nèi)存區(qū)域(運行時數(shù)據(jù)區(qū)域)和內(nèi)存模型(JMM)

        5T技術(shù)資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機,樹莓派,等等。在公眾號內(nèi)回復「2048」,即可免費獲取??!

        微信掃描二維碼,關(guān)注我的公眾號

        朕已閱?

        瀏覽 77
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产白丝精品91爽爽久久 | 激情五月天色 | 日本欧美AAAA无码视频 | 小早川怜子无码流出 | 香蕉视频在线观看免费 | a级片在线| 乱伦小说视频 | 精品无码国产成人网站尤物app | 青青草视频成人 | 五月停停开心无码网站 |