日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

日常開發(fā)中,基于 redis 天然支持分布式鎖,大家在線上分布式項(xiàng)目中都使用過 Redis 鎖。本文主要針對日常開發(fā)中加鎖過程中某些異常場景進(jìn)行講解與分析。本文講解示例代碼都在 https://Github.com/wayn111/newbee-mall-pro 項(xiàng)目 test 目錄下 RedisLockTest 類中。

版本聲明:

  • Spring Boot 版本 3.0.2
  • 演示項(xiàng)目地址:https://github.com/wayn111/newbee-mall-pro
  • github地址:http://github.com/wayn111 歡迎大家關(guān)注,點(diǎn)個(gè)star

一、任務(wù)超時(shí),鎖已經(jīng)過期

這個(gè)異常場景說實(shí)話發(fā)生概率很低,大部分情況下加鎖時(shí)任務(wù)執(zhí)行都會很快,鎖還沒到期,任務(wù)自己就會刪除鎖。除非說任務(wù)調(diào)用第三方接口不穩(wěn)定導(dǎo)致超時(shí)、數(shù)據(jù)庫查詢突然變得非常慢就可能會產(chǎn)生這個(gè)異常場景。

那怎么處理這個(gè)異常嘞?大部分人可能都會回答添加一個(gè)定時(shí)任務(wù),在定時(shí)任務(wù)內(nèi)檢測鎖快過期時(shí),進(jìn)行續(xù)期操作。OK,這么做好像是可以解決這個(gè)異常,那么博主在這里給出自己的見解。

1.1 先說一個(gè)暴論:如果料想到有這類異常產(chǎn)生,為什么不在加鎖時(shí),就把加鎖過期時(shí)間設(shè)置大一點(diǎn)

不管所續(xù)期還是增大加鎖時(shí)長,都會導(dǎo)致一個(gè)問題,其他線程會遲遲獲取不到鎖,一直被阻塞。那結(jié)果都一樣,為什么不直接增大加鎖時(shí)間?

?

想法是好的,但是實(shí)際上,加鎖時(shí)間的設(shè)置是我們主觀臆斷的,我們無法保證這個(gè)加鎖代碼的執(zhí)行時(shí)間一定在我們的鎖過期時(shí)間內(nèi)。作為一個(gè)嚴(yán)謹(jǐn)?shù)某绦騿T,我們需要對我們的代碼有客觀認(rèn)知,任務(wù)執(zhí)行可能幾千上億萬次都是正常,但就是那么一次它執(zhí)行超時(shí)了,可能由于外部依賴、當(dāng)前運(yùn)行環(huán)境的異常導(dǎo)致。

?

1.2 直接不設(shè)置過期時(shí)間,任務(wù)不執(zhí)行完,不釋放鎖

如果在加鎖時(shí)就不設(shè)置過期時(shí)間的話,理論上好像是可以解決這個(gè)問題,任務(wù)不執(zhí)行完,鎖就不會釋放。但是作為程序員,總覺得哪里怪怪的,任務(wù)不執(zhí)行完,鎖就不會釋放!

?

仔細(xì)想想,我們一般在 try 中進(jìn)行加鎖 在 finally 進(jìn)行鎖釋放,這個(gè)好像也沒毛病哦。但是實(shí)際針對一些極端異常場景下,如果任務(wù)執(zhí)行過程中,服務(wù)器宕機(jī)、程序突然被殺掉、網(wǎng)絡(luò)斷連等都可能造成這個(gè)鎖釋放不了,另一個(gè)任務(wù)就一直獲取不到鎖。

?

這個(gè)方案程序正常的情況下,可以滿足我們的要求,但是一旦發(fā)生異常將導(dǎo)致鎖無法釋放的后果,也就是說只要我們解決這個(gè)鎖在異常場景下無法釋放的問題,這個(gè)方案還是OK的。博主這里直接給出方案:

在不設(shè)置過期時(shí)間的加鎖操作成功時(shí),給一個(gè)默認(rèn)過期時(shí)間比如三十秒,同時(shí)啟動一個(gè)定時(shí)任務(wù),給我們的鎖進(jìn)行自動續(xù)期,每隔 默認(rèn)過期時(shí)間 / 3 秒后執(zhí)行一次續(xù)期操作,發(fā)生鎖剩余時(shí)長小于 默認(rèn)過期時(shí)間 / 2 就重新賦值過期時(shí)長為三十秒。這樣的話,可以保證鎖必須由任務(wù)執(zhí)行完才能釋放,當(dāng)程序異常發(fā)生時(shí),仍然能保證鎖會在三十秒內(nèi)釋放。

1.3 設(shè)置過期時(shí)間,任務(wù)不執(zhí)行完,不釋放鎖

這個(gè)方案本質(zhì)上與方案二的解決方案相同,還是啟動定時(shí)任務(wù)進(jìn)行續(xù)期操作,流程這里不做多余講述。需要注意的就是加鎖指定過期時(shí)間會比較符合我們的客觀認(rèn)知。實(shí)際上他的底層邏輯跟方案二相同,無非就是定時(shí)任務(wù)執(zhí)行間隔,鎖剩余時(shí)長續(xù)期判斷要根據(jù)過期時(shí)間來計(jì)算。


「綜合來看:方案三會最合適,符合我們的客觀認(rèn)知,跟我們之前對 Redis 的使用邏輯較為相近。」

二、線程B加鎖執(zhí)行中未釋放鎖,線程A釋放了線程B的鎖

?

說實(shí)話我仔細(xì)思考了一下這個(gè)異常場景,發(fā)現(xiàn)這個(gè)異常是個(gè)偽命題,如果線程 B 正在執(zhí)行時(shí),線程 A 怎么能獲取到線程B的鎖!線程 A 獲取不到線程 B 的鎖,談何來去釋放線程 B 的鎖!如果線程 A 能獲取到線程 B 的鎖那么這個(gè)分布式鎖的代碼一開始就已經(jīng)錯(cuò)了。

?

這里回到這個(gè)異常場景本身,我們可以給每個(gè)線程設(shè)置請求ID,加鎖成功將請求ID設(shè)置為加鎖 key 的對應(yīng) value,線程釋放鎖時(shí)需要判斷當(dāng)前線程的請求ID與 加鎖 key 的對應(yīng) value 是否相同,相同則可以釋放鎖,不相同則不允許釋放。

三、線程加鎖成功后繼續(xù)申請加鎖

?

這個(gè)場景主要發(fā)生在加鎖代碼內(nèi)部調(diào)用棧過深,比如說加鎖成功執(zhí)行方法 a,在方法 a 內(nèi)又重復(fù)申請了同一把鎖,導(dǎo)致線程把自己鎖住了,這個(gè)業(yè)界的主流叫法是叫鎖的可重入性。

?

解決方式有兩種,一是修改方法內(nèi)的加鎖邏輯,不要加同一把鎖,修改方法 a 內(nèi)的加鎖 key 名稱。二是針對加鎖邏輯做修改,實(shí)現(xiàn)可重入性。

這里簡單介紹如何實(shí)現(xiàn)可重入性,給每個(gè)線程設(shè)置請求ID,加鎖成功將請求ID設(shè)置為加鎖 key 的對應(yīng) value,針對同一個(gè)線程的重復(fù)加鎖,判斷當(dāng)前線程已存在請求ID的情況下,請求ID直接與加鎖 key 的對應(yīng) value 相比較,相同則直接返回加鎖成功。

四、 代碼實(shí)踐

4.1 加鎖自動續(xù)期實(shí)踐

設(shè)置鎖過期時(shí)間為10秒,然后該任務(wù)執(zhí)行15秒,代碼如下:

?

ps: 以下代碼都可以在 https://github.com/wayn111/newbee-mall-pro 項(xiàng)目 test 目錄下 RedisLockTest 類中找到

?
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisLockTest {
    @Autowired
    private RedisLock redisLock;
    @Test
    @Test
    public void redisLockNeNewTest() {
        String key = "test";
        try {
            log.info("---申請加鎖");
            if (redisLock.lock(key, 10)) {
                // 模擬任務(wù)執(zhí)行15秒
                log.info("---加鎖成功");
                Thread.sleep(15000);
                log.info("---執(zhí)行完畢");
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            redisLock.unLock(key);
        }
    }
}

執(zhí)行如下:

Redis分布式鎖常見坑點(diǎn)分析

可以看出就算任務(wù)執(zhí)行超過過期時(shí)間也能通過自動續(xù)期讓代碼正常執(zhí)行。

4.2 多線程下其他線程無法共同申請到同一把鎖實(shí)踐

啟動兩個(gè)線程,線程 A 先加鎖, 線程 B 后枷鎖

@Test
public void redisLockReleaseSelfTest() throws IOException {
    new Thread(() -> {
        String key = "test";
        try {
            log.info("---申請加鎖");
            if (redisLock.lock(key, 10)) {
                // 模擬任務(wù)執(zhí)行15秒
                log.info("---加鎖成功");
                Thread.sleep(15000);
                log.info("---執(zhí)行完畢");
            } else {
                log.info("---加鎖失敗");
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            redisLock.unLock(key);
        }
    }, "thread-A").start();
    new Thread(() -> {
        String key = "test";
        try {
            Thread.sleep(100L);
            log.info("---申請加鎖");
            if (redisLock.lock(key, 10)) {
                // 模擬任務(wù)執(zhí)行15秒
                log.info("---加鎖成功");
                Thread.sleep(15000);
                log.info("---執(zhí)行完畢");
            } else {
                log.info("---加鎖失敗");
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            redisLock.unLock(key);
        }
    }, "thread-B").start();
    System.in.read();
}

結(jié)果如下:

Redis分布式鎖常見坑點(diǎn)分析

可以看到,線程 A 先申請到鎖,線程 B 后申請鎖,結(jié)果線程 B 申請加鎖失敗。

4.3 鎖得可重入性實(shí)踐

當(dāng)前線程加鎖成功后,在線程執(zhí)行中繼續(xù)申請同一把鎖,代碼如下:

@Test
public void redisLockReEntryTest() {
    String key = "test";
    try {
        log.info("---申請加鎖");
        if (redisLock.lock(key, 10)) {
            // 模擬任務(wù)執(zhí)行15秒
            log.info("---加鎖第一次成功");
            if (redisLock.lock(key, 10)) {
                // 模擬任務(wù)執(zhí)行15秒
                log.info("---加鎖第二次成功");
                Thread.sleep(15000);
                log.info("---加鎖第二次執(zhí)行完畢");
            } else {
                log.info("---加鎖第二次失敗");
            }
            Thread.sleep(15000);
            log.info("---加鎖第一次執(zhí)行完畢");
        } else {
            log.info("---加鎖第一次失敗");
        }
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    } finally {
        redisLock.unLock(key);
    }
}

結(jié)果如下:

Redis分布式鎖常見坑點(diǎn)分析

4.4 加鎖邏輯講解

直接貼出本文最核心 RedisLock 類全部代碼:

@Slf4j
@Component
public class RedisLock {
    @Autowired
    public RedisTemplate redisTemplate;
    /**
     * 默認(rèn)鎖過期時(shí)間20秒
     */
    public static final Integer DEFAULT_TIME_OUT = 30;
    /**
     * 保存線程id-ThreadLocal
     */
    private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    /**
     * 保存定時(shí)任務(wù)(watch-dog)-ThreadLocal
     */
    private ThreadLocal<ExecutorService> executorServiceThreadLocal = new ThreadLocal<>();
    /**
     * 加鎖,不指定過期時(shí)間
     *
     * @param key key名稱
     * @return boolean
     */
    public boolean lock(String key) {
        return lock(key, null);
    }

    /**
     * 加鎖
     *
     * @param key     key名稱
     * @param timeout 過期時(shí)間
     * @return boolean
     */
    public boolean lock(String key, Integer timeout) {
        Integer timeoutTmp;
        if (timeout == null) {
            timeoutTmp = DEFAULT_TIME_OUT;
        } else {
            timeoutTmp = timeout;
        }
        String nanoId;
        if (stringThreadLocal.get() != null) {
            nanoId = stringThreadLocal.get();
        } else {
            nanoId = IdUtil.nanoId();
            stringThreadLocal.set(nanoId);
        }
        RedisScript<Long> redisScript = new DefaultRedisScript<>(buildLuaLockScript(), Long.class);
        Long execute = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), nanoId, timeoutTmp);
        boolean flag = execute != null && execute == 1;
        if (flag) {
            ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
            executorServiceThreadLocal.set(scheduledExecutorService);
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                RedisScript<Long> renewRedisScript = new DefaultRedisScript<>(buildLuaRenewScript(), Long.class);
                Long result = (Long) redisTemplate.execute(renewRedisScript, Collections.singletonList(key), nanoId, timeoutTmp);
                if (result != null && result == 2) {
                    ThreadUtil.shutdownAndAwAItTermination(scheduledExecutorService);
                }
            }, 0, timeoutTmp / 3, TimeUnit.SECONDS);
        }
        return flag;
    }

    /**
     * 釋放鎖
     *
     * @param key key名稱
     * @return boolean
     */
    public boolean unLock(final String key) {
        String nanoId = stringThreadLocal.get();
        RedisScript<Long> redisScript = new DefaultRedisScript<>(buildLuaUnLockScript(), Long.class);
        Long execute = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), nanoId);
        boolean flag = execute != null && execute == 1;
        if (flag) {
            if (executorServiceThreadLocal.get() != null) {
                ThreadUtil.shutdownAndAwaitTermination(executorServiceThreadLocal.get());
            }
        }
        return flag;
    }

    private String buildLuaLockScript() {
        return """
                local key = KEYS[1]
                local value = ARGV[1]
                local time_out = ARGV[2]
                local result = redis.call('get', key)
                if result == value then
                    return 1;
                end
                local lock_result = redis.call('setnx', key, value)
                if tonumber(lock_result) == 1 then
                    redis.call('expire', key, time_out)
                    return 1;
                else
                    return 0;
                end
                """;
    }

    private String buildLuaUnLockScript() {
        return """
                local key = KEYS[1]
                local value = ARGV[1]
                local result = redis.call('get', key)
                if result ~= value then
                    return 0;
                else
                    redis.call('del', key)
                end
                return 1;
                """;
    }

    private String buildLuaRenewScript() {
        return """
                local key = KEYS[1]
                local value = ARGV[1]
                local timeout = ARGV[2]
                local result = redis.call('get', key)
                if result ~= value then
                    return 2;
                end
                local ttl = redis.call('ttl', key)
                if tonumber(ttl) < tonumber(timeout) / 2 then
                    redis.call('expire', key, timeout)
                    return 1;
                else
                    return 0;
                end
                """;
    }
}

加鎖邏輯:這里我把加鎖邏輯分解成三步展示給大家

  • 加鎖前:先判斷當(dāng)前線程是否存在請求ID,不存在則生成,存在就直接使用
  • 加鎖中:通過 lua 腳本執(zhí)行原子加鎖操作, 加鎖時(shí)先判斷當(dāng)前線程ID與加鎖 key 得 value 是否相等,相等則是同一個(gè)線程的鎖重入,直接返加鎖成功。不相等則設(shè)置加鎖 value 為請求ID以及過期時(shí)間。
  • 加鎖后:啟動一個(gè)定時(shí)任務(wù),每隔 過期時(shí)間 / 3 秒后執(zhí)行一次續(xù)期操作,發(fā)現(xiàn)鎖剩余時(shí)間不足 過期時(shí)間 / 2 秒后,通過 lua 腳本進(jìn)行續(xù)期操作。

解鎖邏輯:這里我把解鎖邏輯分解成兩步展示給大家

  • 解鎖中:通過 lua 腳本執(zhí)行解鎖操作,先判斷加鎖 key 的 value 是否與自身請求ID相同,相同則讓解鎖,不相同則不讓解鎖。
  • 解鎖后:刪除定時(shí)任務(wù)。

五、總結(jié)

其實(shí)本文得核心邏輯有許多都是參考 Redission 客戶端而寫,對于這些常見得坑點(diǎn),博主結(jié)合自身思考,業(yè)界知識總結(jié)并自己實(shí)現(xiàn)一個(gè)分布式鎖得工具類。希望大家看了有所收獲,對日常業(yè)務(wù)中 Redis 分布式鎖的使用能有更深的理解。

分享到:
標(biāo)簽:Redis
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定