自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

字節(jié)二面:Spring Boot Redis 可重入分布式鎖實(shí)現(xiàn)原理?

數(shù)據(jù)庫 Redis
當(dāng)一個(gè)線程執(zhí)行一段代碼成功獲取鎖之后,繼續(xù)執(zhí)行時(shí),又遇到加鎖的代碼,可重入性就就保證線程能繼續(xù)執(zhí)行,而不可重入就是需要等待鎖釋放之后,再次獲取鎖成功,才能繼續(xù)往下執(zhí)行。

我是碼哥,可以叫我靚仔。

書接上回,碼哥上一篇《糾正誤區(qū):這才是 SpringBoot Redis 分布式鎖的正確實(shí)現(xiàn)方式》分享了分布式鎖如何從錯(cuò)誤到殘缺,再到青銅版本的高性能 Redis 分布式鎖代碼實(shí)戰(zhàn),讓你一飛沖天。

這是我們最常用的分布式鎖方案,今天碼哥給你來一個(gè)進(jìn)階。

Chaya:「碼哥,上次的分布式鎖版本雖然好,但是不支持可重入獲取鎖,還差一點(diǎn)點(diǎn)意思?!?/p>

Chaya 別急,今日碼哥給你帶來一個(gè)高性能可重入 Redis 分布式鎖解決方案,直搗黃龍,一笑破蒼穹。

什么是可重入鎖

當(dāng)一個(gè)線程執(zhí)行一段代碼成功獲取鎖之后,繼續(xù)執(zhí)行時(shí),又遇到加鎖的代碼,可重入性就就保證線程能繼續(xù)執(zhí)行,而不可重入就是需要等待鎖釋放之后,再次獲取鎖成功,才能繼續(xù)往下執(zhí)行。

public synchronized void a() {
    b();
}
public synchronized void b() {
    // doWork
}

假設(shè) X 線程在 a 方法獲取鎖之后,繼續(xù)執(zhí)行 b 方法,如果此時(shí)不可重入,線程就必須等待鎖釋放,再次爭(zhēng)搶鎖。

鎖明明是被 X 線程擁有,卻還需要等待自己釋放鎖,然后再去搶鎖,這看起來就很奇怪,我釋放我自己~

可重入鎖實(shí)現(xiàn)原理

Chaya:「Redis String 數(shù)據(jù)結(jié)構(gòu)無法滿足可重入鎖,key 表示鎖定的資源,value 是客戶端唯一標(biāo)識(shí),可重入沒地方放了?!?/p>

我們可以使用 Redis hash 結(jié)構(gòu)實(shí)現(xiàn),key 表示被鎖的共享資源, hash 結(jié)構(gòu)的 fieldKey 存儲(chǔ)客戶端唯一標(biāo)識(shí),fieldKey 的 value 則保存加鎖的次數(shù)。

加鎖原理

可重入鎖加鎖的過程中有以下場(chǎng)景需要考慮。

  • 鎖已經(jīng)被 A 客戶端獲取,客戶端 B 獲取鎖失敗。
  • 鎖已經(jīng)被客戶端 A 獲取,客戶端 A 多次執(zhí)行獲取鎖操作。
  • 鎖沒有被其他客戶端獲取,那么此刻獲取鎖的客戶端可以獲取成功。

按照之前的經(jīng)驗(yàn),多個(gè)操作的原子性可以用 lua 腳本實(shí)現(xiàn)??芍厝腈i加鎖 lua 腳本如下。

if ((redis.call('exists', KEYS[1]) == 0) or
   (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
  return nil;
end;
return redis.call('pttl', KEYS[1]);
  • KEYS[1]是 lockKey 表示獲取的鎖資源,比如 lock:168。
  • ARGV[1] 表示表示鎖的有效時(shí)間(單位毫秒)。
  • ARGV[2] 表示客戶端唯一標(biāo)識(shí),在 Redisson 中使用 UUID:ThreadID。

下面我來接下是這段腳本的邏輯。

鎖不存在或者鎖存在且值與客戶端唯一標(biāo)識(shí)匹配,則執(zhí)行 'hincrby' 和 pexpire指令,接著 return nil。表示的含義就是鎖不存在就設(shè)置鎖并設(shè)置鎖重入計(jì)數(shù)值為 1,設(shè)置過期時(shí)間;鎖存在且唯一標(biāo)識(shí)匹配表明當(dāng)前加鎖請(qǐng)求是鎖重入請(qǐng)求,鎖從如計(jì)數(shù) +1,重新鎖超時(shí)時(shí)間。

  • redis.call('exists', KEYS[1]) == 0判斷鎖是否存在,0 表示不存在。
  • redis.call('hexists', KEYS[1], ARGV[2]) == 1)鎖存在的話,判斷 hash 結(jié)構(gòu)中 fieldKey 與客戶端的唯一標(biāo)識(shí)是否相等。相等表示當(dāng)前加鎖請(qǐng)求是鎖重入。
  • redis.call('hincrby', KEYS[1], ARGV[2], 1)將存儲(chǔ)在 hash 結(jié)構(gòu)的 ARGV[2] 的值 +1,不存在則支持成 1。
  • redis.call('pexpire', KEYS[1], ARGV[1])對(duì) KEYS[1] 設(shè)置超時(shí)時(shí)間。

鎖存在,但是唯一標(biāo)識(shí)不匹配,表明鎖被其他線程持有,調(diào)用 pttl返回鎖剩余的過期時(shí)間。

Chaya:「“腳本執(zhí)行結(jié)果返回 nil、鎖剩余過期時(shí)間有什么目的?”」

當(dāng)且僅當(dāng)返回 nil才表示加鎖成功;客戶端需要感知鎖是否成功的結(jié)果。

解鎖原理

解鎖邏輯復(fù)雜一些,不僅要保證不能刪除別人的鎖。還要確保,重入次數(shù)為 0 才能解鎖。

解鎖代碼執(zhí)行方式與加鎖類似,三個(gè)返回值含義如下。

  • 1 代表解鎖成功,鎖被釋放。
  • 0 代表可重入次數(shù)被減 1。
  • nil 代表其他線程嘗試解鎖,解鎖失敗。
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then
    return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 0;
else
    redis.call('del', KEYS[1]);
    return 1;
end;
return nil;
  • KEYS[1]是 lockKey,表示鎖的資源,比如 lock:order:pay。
  • ARGV[1],鎖的超時(shí)時(shí)間。
  • ARGV[2],Hash 表的 FieldKey。

首先使用 hexists 判斷 Redis 的 Hash 表是否存在 fileKey,如果不存在則直接返回 nil解鎖失敗。

若存在的情況下,且唯一標(biāo)識(shí)匹配,使用 hincrby 對(duì) fileKey 的值 -1,然后判斷計(jì)算之后可重入次數(shù)。當(dāng)前值 > 0 表示持有的鎖存在重入情況,重新設(shè)置超時(shí)時(shí)間,返回值 1;

若值小于等于 0,表明鎖釋放了,執(zhí)行 del釋放鎖。

Chaya:“可重入鎖很好,依然存在的一個(gè)問題是:加鎖后,業(yè)務(wù)邏輯執(zhí)行耗時(shí)超過了 lockKey 的過期時(shí)間,lockKey 會(huì)被 Reids 刪除?!?/p>

這個(gè)時(shí)間不能瞎寫,一般要根據(jù)在測(cè)試環(huán)境多次測(cè)試,然后壓測(cè)多輪之后,比如計(jì)算出接口平均執(zhí)行時(shí)間 200 ms。那么鎖的超時(shí)時(shí)間就放大為平均執(zhí)行時(shí)間的 3~5 倍。

Chaya:“鎖的超時(shí)時(shí)間怎么計(jì)算合適呢?”

這個(gè)時(shí)間不能瞎寫,一般要根據(jù)在測(cè)試環(huán)境多次測(cè)試,然后壓測(cè)多輪之后,比如計(jì)算出接口平均執(zhí)行時(shí)間 200 ms。那么鎖的超時(shí)時(shí)間就放大為平均執(zhí)行時(shí)間的 3~5 倍。

Chaya:“為啥要放大呢?”

因?yàn)槿绻i的操作邏輯中有網(wǎng)絡(luò) IO 操作、JVM FullGC 等,線上的網(wǎng)絡(luò)不會(huì)總一帆風(fēng)順,我們要給網(wǎng)絡(luò)抖動(dòng)留有緩沖時(shí)間。

Chaya:“有沒有完美的方案呢?不管時(shí)間怎么設(shè)置都不大合適?!?/p>

我們可以讓獲得鎖的線程開啟一個(gè)守護(hù)線程,用來給當(dāng)前客戶端快要過期的鎖續(xù)航,續(xù)命的前提是,得判斷是不是當(dāng)前進(jìn)程持有的鎖,如果不是就不進(jìn)行續(xù)。

如果快要過期,但是業(yè)務(wù)邏輯還沒執(zhí)行完成,自動(dòng)對(duì)這個(gè)鎖進(jìn)行續(xù)期,重新設(shè)置過期時(shí)間。

這就是下一篇我要說的超神方案,加入看門狗機(jī)制實(shí)現(xiàn)鎖自動(dòng)續(xù)期。不過鎖自動(dòng)續(xù)期比較復(fù)雜,今天的 Redis 可重入分布式鎖王者方案已經(jīng)可以讓你稱霸武林,接下來上實(shí)戰(zhàn)。

可重入分布式鎖實(shí)戰(zhàn)

關(guān)于 Spring Boot 的環(huán)境搭建以及普通分布式鎖實(shí)戰(zhàn)詳見上一篇《糾正誤區(qū):這才是 SpringBoot Redis 分布式鎖的正確實(shí)現(xiàn)方式》。今天直接上可重入鎖核心代碼。

ReentrantDistributedLock

可重入鎖由ReentrantDistributedLock標(biāo)識(shí),它實(shí)現(xiàn) Lock接口,構(gòu)造方法實(shí)現(xiàn) resourceName 和 StringRedisTemplate 的屬性設(shè)置。

客戶端唯一標(biāo)識(shí)使用uuid:threadId 組成。

public class ReentrantDistributedLock implements Lock {

    /**
     * 鎖超時(shí)時(shí)間,默認(rèn) 30 秒
     */
    protected long internalLockLeaseTime = 30000;

    /**
     * 標(biāo)識(shí) id
     */
    private final String id = UUID.randomUUID().toString();

    /**
     * 資源名稱
     */
    private final String resourceName;

    private final List<String> keys = new ArrayList<>(1);


    /**
     * Redis 客戶端
     */
    private final StringRedisTemplate redisTemplate;

    public ReentrantDistributedLock(String resourceName, StringRedisTemplate redisTemplate) {
        this.resourceName = resourceName;
        this.redisTemplate = redisTemplate;
        keys.add(resourceName);
    }
}

加鎖 tryLock、lock

tryLock 以阻塞等待 waitTime 時(shí)間的方式來嘗試獲取鎖。獲取成功則返回 true,反之 false。

與 tryLock不同的是, lock 一直嘗試自旋阻塞等待獲取分布式鎖,直到獲取成功為止。而 tryLock 只會(huì)阻塞等待 waitTime 時(shí)間。

@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    long threadId = Thread.currentThread().getId();
    // lua 腳本獲取鎖
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return true;
    }

    time -= System.currentTimeMillis() - current;
    // 等待時(shí)間用完,獲取鎖失敗
    if (time <= 0) {
        return false;
    }
    // 自旋獲取鎖
    while (true) {
        long currentTime = System.currentTimeMillis();
        ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return true;
        }

        time -= System.currentTimeMillis() - currentTime;
        if (time <= 0) {
            return false;
        }
    }
}

@Override
public void lock(long leaseTime, TimeUnit unit) {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return;
    }
    do {
        ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
    } while (ttl != null);
}

private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    // 執(zhí)行 lua 腳本
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LuaScript.reentrantLockScript(), Long.class);
    return redisTemplate.execute(redisScript, keys, String.valueOf(unit.toMillis(leaseTime)), getRequestId(threadId));
}

private String getRequestId(long threadId) {
    return id + ":" + threadId;
}

解鎖 unlock

public void unlock() {
        long threadId = Thread.currentThread().getId();

        // 執(zhí)行 lua 腳本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LuaScript.reentrantUnlockScript(), Long.class);
        Long opStatus = redisTemplate.execute(redisScript, keys, String.valueOf(internalLockLeaseTime), getRequestId(threadId));

        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
        }


    }

LuaScript

這個(gè)腳本就是在講解可重入分布式鎖原理具體邏輯已經(jīng)解釋過,這里就不再重復(fù)分析。

public class LuaScript {

    private LuaScript() {

    }

    /**
     * 可重入分布式鎖加鎖腳本
     *
     * @return 當(dāng)且僅當(dāng)返回 `nil`才表示加鎖成功;返回鎖剩余過期時(shí)間是讓客戶端感知鎖是否成功。
     */
    public static String reentrantLockScript() {
        return "if ((redis.call('exists', KEYS[1]) == 0) " +
                "or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +
                "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return nil; " +
                "end; " +
                "return redis.call('pttl', KEYS[1]);";
    }

    /**
     * 可重入分布式鎖解鎖腳本
     *
     * @return 當(dāng)且僅當(dāng)返回 `nil`才表示解鎖成功;
     */
    public static String reentrantUnlockScript() {
        return "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then " +
                "return nil;" +
                "end; " +
                "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
                "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return 0; " +
                "else " +
                "redis.call('del', KEYS[1]); " +
                "return 1; " +
                "end; " +
                "return nil;";
    }
}

RedisLockClient

最后,還需要提供一個(gè)客戶端給方便使用。

@Component
public class RedisLockClient {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 獲取可重入分布式鎖
     * @param name
     * @return
     */
    public Lock getReentrantLock(String name) {
        return new ReentrantDistributedLock(name, redisTemplate);
    }

}

單元測(cè)試走一個(gè),驗(yàn)證下分布式鎖是否支持可重入。

@Slf4j
@SpringBootTest(classes = RedisApplication.class)
public class RedisLockTest {

    @Autowired
    private RedisLockClient redisLockClient;

    @Test
    public void testTryReentrantLockSuccess() throws InterruptedException {
        Lock lock = redisLockClient.getReentrantLock("order:pay");
        try {
            boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!isLock) {
                log.warn("加鎖失敗");
                return;
            }
            // 重復(fù)加鎖
            reentrant(lock);

            log.info("業(yè)務(wù)邏輯執(zhí)行完成");
        } finally {
            lock.unlock();
        }

    }

    private void reentrant(Lock lock) throws InterruptedException {
        try {
            boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!isLock) {
                log.warn("加鎖失敗");
                return;
            }

            log.info("業(yè)務(wù)邏輯執(zhí)行完成");
        } finally {
            lock.unlock();
        }
    }

}

有兩個(gè)點(diǎn)需要注意。

  • 釋放鎖的代碼一定要放在 finally{} 塊中。否則一旦執(zhí)行業(yè)務(wù)邏輯過程中拋出異常,程序就無法執(zhí)行釋放鎖的流程。只能干等著鎖超時(shí)釋放。
  • 加鎖的代碼應(yīng)該寫在 try {} 代碼中,放在 try 外面的話,如果執(zhí)行加鎖異常(客戶端網(wǎng)絡(luò)連接超時(shí)),但是實(shí)際指令已經(jīng)發(fā)送到服務(wù)端并執(zhí)行,就會(huì)導(dǎo)致沒有機(jī)會(huì)執(zhí)行解鎖的代碼。

CHaya:“碼哥,這個(gè)方案確實(shí)很王者,大開眼界,接下來的超神版可以實(shí)現(xiàn)看門狗自動(dòng)續(xù)期么?”

鑒于篇幅有限,今天就跟大家介紹 Redis 可重入分布式鎖王者方案,關(guān)注我,下一篇給你分享、超神版分布式鎖解決方案。

責(zé)任編輯:姜華 來源: 碼哥字節(jié)
相關(guān)推薦

2020-07-15 16:50:57

Spring BootRedisJava

2020-06-15 08:15:47

分布式鎖系統(tǒng)

2021-07-08 09:21:17

ZooKeeper分布式鎖 Curator

2021-06-27 21:24:55

RedissonJava數(shù)據(jù)

2022-10-10 14:41:44

RedisJVM數(shù)據(jù)

2021-07-09 06:48:31

ZooKeeperCurator源碼

2024-02-04 09:29:07

Redis數(shù)據(jù)庫

2024-04-26 08:06:58

分布式系統(tǒng)

2024-11-28 15:11:28

2021-07-10 10:02:30

ZooKeeperCurator并發(fā)

2023-08-21 19:10:34

Redis分布式

2022-01-06 10:58:07

Redis數(shù)據(jù)分布式鎖

2022-09-29 08:28:57

SpringRedis分布式

2022-09-22 13:28:34

Redis分布式鎖

2022-12-31 09:42:14

超時(shí)功能

2021-02-28 07:49:28

Zookeeper分布式

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2024-10-07 10:07:31

2019-06-19 15:40:06

分布式鎖RedisJava

2024-04-01 05:10:00

Redis數(shù)據(jù)庫分布式鎖
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)