欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Redis結(jié)合Lua腳本實(shí)現(xiàn)分布式鎖詳解

 更新時(shí)間:2024年02月28日 10:11:17   作者:coffee_baby  
Lua?是一種輕量小巧的腳本語(yǔ)言,用標(biāo)準(zhǔn)C語(yǔ)言編寫(xiě)并以源代碼形式開(kāi)放,?本文主要為大家介紹了Redis如何結(jié)合Lua腳本實(shí)現(xiàn)分布式鎖,需要的可以參考下

先講一下為什么使用分布式鎖

在傳統(tǒng)的單體應(yīng)用中,我們可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或synchronized)來(lái)實(shí)現(xiàn)對(duì)共享資源的互斥控制,確保在高并發(fā)情況下同一時(shí)間只有一個(gè)線程能夠執(zhí)行特定方法。然而,隨著業(yè)務(wù)的發(fā)展,單體應(yīng)用逐漸演化為分布式系統(tǒng),多線程、多進(jìn)程分布在不同機(jī)器上,這導(dǎo)致了原有的單機(jī)部署下的并發(fā)控制策略失效。為了解決這一問(wèn)題,我們需要引入一種跨JVM的互斥機(jī)制來(lái)管理共享資源的訪問(wèn),這就是分布式鎖所要解決的核心問(wèn)題。

Lua介紹

Lua 是一種輕量小巧的腳本語(yǔ)言,用標(biāo)準(zhǔn)C語(yǔ)言編寫(xiě)并以源代碼形式開(kāi)放, 其設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。

為什么要用Lua呢

Redis采用單線程架構(gòu),可以保證單個(gè)命令的原子性,但是無(wú)法保證一組命令在高并發(fā)場(chǎng)景下的原子性。

在以下場(chǎng)景中:

  • 當(dāng) 事務(wù)1執(zhí)行刪除操作時(shí),查詢到的鎖值確實(shí)相等。
  • 在 事務(wù)1執(zhí)行刪除操作之前,鎖的過(guò)期時(shí)間剛好到達(dá),導(dǎo)致 Redis 自動(dòng)釋放了該鎖。
  • 事務(wù)2獲取了這個(gè)已被釋放的鎖。
  • 當(dāng) 事務(wù)1執(zhí)行刪除操作時(shí),會(huì)意外地刪除掉 事務(wù)2持有的鎖。

上面的刪除情況也無(wú)法保證原子性,只能通過(guò)lua腳本實(shí)現(xiàn)

如果redis客戶端通過(guò)lua腳本把3個(gè)命令一次性發(fā)送給redis服務(wù)器,那么這三個(gè)指令就不會(huì)被其他客戶端指令打斷。Redis 也保證腳本會(huì)以原子性(atomic)的方式執(zhí)行: 當(dāng)某個(gè)腳本正在運(yùn)行的時(shí)候,不會(huì)有其他腳本或 Redis 命令被執(zhí)行。

Lua腳本命令

在Redis中需要通過(guò)eval命令執(zhí)行l(wèi)ua腳本

EVAL script numkeys key [key ...] arg [arg ...]

script:lua腳本字符串,這段Lua腳本不需要(也不應(yīng)該)定義函數(shù)。
numkeys:lua腳本中KEYS數(shù)組的大小
key [key ...]:KEYS數(shù)組中的元素
arg [arg ...]:ARGV數(shù)組中的元素

案列1:動(dòng)態(tài)傳參

EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 5 8 10 30 40 50 60 70 
# 輸出:8 10 60 70

EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 10 20
# 輸出:0

EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 20 10
# 輸出:1

案列2:執(zhí)行redis類(lèi)庫(kù)方法

EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 bbb 20

可重入性

可重入性是指一個(gè)線程在持有鎖的情況下,可以多次獲取同一個(gè)鎖而不會(huì)發(fā)生死鎖或阻塞的特性。在可重入鎖中,線程可以重復(fù)獲取已經(jīng)持有的鎖,每次獲取都會(huì)增加一個(gè)計(jì)數(shù)器,直到計(jì)數(shù)器歸零時(shí)才會(huì)真正釋放鎖。

下面是一個(gè)示例代碼來(lái)說(shuō)明可重入性:

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

假設(shè)線程X在方法a中獲取了鎖后,繼續(xù)執(zhí)行方法b。如果這是一個(gè)不可重入的鎖,線程X在執(zhí)行b方法時(shí)將會(huì)被阻塞,因?yàn)樗呀?jīng)持有了該鎖并且無(wú)法再次獲取。這種情況下,線程X必須等待自己釋放鎖后才能再次爭(zhēng)搶該鎖。

而對(duì)于可重入性的情況,當(dāng)線程X持有了該鎖后,在遇到加鎖方法時(shí)會(huì)直接將加鎖次數(shù)加1,并繼續(xù)執(zhí)行方法邏輯。當(dāng)退出加鎖方法時(shí),加鎖次數(shù)再減1。只有當(dāng)加鎖次數(shù)歸零時(shí),該線程才會(huì)真正釋放該鎖。

因此,可重入性的最大特點(diǎn)就是計(jì)數(shù)器的存在,用于統(tǒng)計(jì)加鎖的次數(shù)。在分布式環(huán)境中實(shí)現(xiàn)可重入分布式鎖時(shí)也需要考慮如何正確統(tǒng)計(jì)和管理加鎖次數(shù)。

加鎖腳本

Redis 提供了 Hash (哈希表)這種可以存儲(chǔ)鍵值對(duì)數(shù)據(jù)結(jié)構(gòu)。所以我們可以使用 Redis Hash 存儲(chǔ)的鎖的重入次數(shù),然后利用 lua 腳本判斷邏輯。

if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1) 
then
    redis.call('hincrby', KEYS[1], ARGV[1], 1);
    redis.call('expire', KEYS[1], ARGV[2]);
    return 1;
else
	return 0;
end

假設(shè)值為:KEYS:[lock], ARGV[uuid, expire]

如果鎖不存在或者這是自己的鎖,就通過(guò)hincrby(不存在就新增并加1,存在就加1)獲取鎖或者鎖次數(shù)加1。

解鎖腳本

-- 判斷 hash set 可重入 key 的值是否等于 0
-- 如果為 nil 代表 自己的鎖已不存在,在嘗試解其他線程的鎖,解鎖失敗
-- 如果為 0 代表 可重入次數(shù)被減 1
-- 如果為 1 代表 該可重入 key 解鎖成功
if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then 
    return nil; 
elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then 
    return 0; 
else 
    redis.call('del', KEYS[1]); 
    return 1; 
end;

如果鎖不存在直接返回null,如果鎖存在就對(duì)數(shù)量進(jìn)行減一,如果減到等于0 就直接刪除此鎖

自動(dòng)續(xù)期

有可能代碼沒(méi)執(zhí)行完畢,鎖就到期了?;谏厦孢@種情況需要對(duì)鎖進(jìn)行續(xù)期。使用定時(shí)器加lua腳本進(jìn)行對(duì)鎖續(xù)期

if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then 
    redis.call('expire', KEYS[1], ARGV[2]); 
    return 1; 
else 
    return 0; 
end

Java代碼實(shí)現(xiàn)

考慮到分布式鎖可能使用多種方式實(shí)現(xiàn),比如Redis、mysql、zookeeper,所以暫時(shí)做成一個(gè)工廠類(lèi),按需使用。

以下是完整代碼:

public class DistributedRedisLock implements Lock {

    private StringRedisTemplate redisTemplate;

    private String lockName;

    private String uuid;

    private long expire = 30;

    public DistributedRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.uuid = uuid + ":" + Thread.currentThread().getId();
    }

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 加鎖方法
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time != -1){
            this.expire = unit.toSeconds(time);
        }
        String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
                "then " +
                "   redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
                "   redis.call('expire', KEYS[1], ARGV[2]) " +
                "   return 1 " +
                "else " +
                "   return 0 " +
                "end";
        while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))){
            Thread.sleep(50);
        }
        // 加鎖成功,返回之前,開(kāi)啟定時(shí)器自動(dòng)續(xù)期
        this.renewExpire();
        return true;
    }

    /**
     * 解鎖方法
     */
    @Override
    public void unlock() {
        String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
                "then " +
                "   return nil " +
                "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
                "then " +
                "   return redis.call('del', KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";
        Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
        if (flag == null){
            throw new IllegalMonitorStateException("this lock doesn't belong to you!");
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    private void renewExpire(){
        String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
                "then " +
                "   return redis.call('expire', KEYS[1], ARGV[2]) " +
                "else " +
                "   return 0 " +
                "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
                    renewExpire();
                }
            }
        }, this.expire * 1000 / 3);
    }
}

DistributedLockClient

@Component
public class DistributedLockClient {
    @Autowired
    private StringRedisTemplate redisTemplate;

    private String uuid;

    public DistributedLockClient() {
        this.uuid = UUID.randomUUID().toString();
    }

    public DistributedRedisLock getRedisLock(String lockName){
        return new DistributedRedisLock(redisTemplate, lockName, uuid);
    }
}

使用及測(cè)試:

在業(yè)務(wù)代碼中使用:

public void deduct() {
    DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lock");
    redisLock.lock();

    try {
        // 1. 查詢庫(kù)存信息
        String stock = redisTemplate.opsForValue().get("stock").toString();

        // 2. 判斷庫(kù)存是否充足
        if (stock != null && stock.length() != 0) {
            Integer st = Integer.valueOf(stock);
            if (st > 0) {
                // 3.扣減庫(kù)存
                redisTemplate.opsForValue().set("stock", String.valueOf(--st));
            }
        }
    } finally {
        redisLock.unlock();
    }
}

測(cè)試可重入性:

紅鎖算法

在Redis集群狀態(tài)下可能出現(xiàn)的問(wèn)題如下:

1.客戶端A從主節(jié)點(diǎn)(master)獲取到了鎖。

2.在主節(jié)點(diǎn)將鎖同步到從節(jié)點(diǎn)(slave)之前,主節(jié)點(diǎn)發(fā)生宕機(jī)。

3.從節(jié)點(diǎn)被晉升為主節(jié)點(diǎn)。

4.客戶端B獲取了同一個(gè)資源,但是客戶端A已經(jīng)在另一個(gè)鎖上獲取了鎖。

在這種情況下,由于主節(jié)點(diǎn)宕機(jī)導(dǎo)致從節(jié)點(diǎn)晉升為新的主節(jié)點(diǎn),可能會(huì)出現(xiàn)客戶端B誤認(rèn)為資源未被鎖定而獲取了另一個(gè)鎖的情況。這可能導(dǎo)致數(shù)據(jù)不一致性或競(jìng)爭(zhēng)條件的發(fā)生。

為了避免這種問(wèn)題

安全失效!

解決集群下鎖失效,參照redis官方網(wǎng)站針對(duì)redlock文檔:https://redis.io/topics/distlock

實(shí)現(xiàn)步驟:

  • 客戶端向N個(gè)Redis節(jié)點(diǎn)發(fā)送請(qǐng)求獲取鎖。
  • 每個(gè)Redis節(jié)點(diǎn)生成一個(gè)獨(dú)立的隨機(jī)值作為鎖值,并設(shè)置相同的過(guò)期時(shí)間。
  • 客戶端等待大部分節(jié)點(diǎn)(如大多數(shù)節(jié)點(diǎn)的一半以上)返回獲取成功的響應(yīng)。
  • 如果大部分節(jié)點(diǎn)返回獲取成功,則認(rèn)定為成功獲取了分布式鎖;否則認(rèn)定為未獲取到分布式鎖。

以上就是Redis結(jié)合Lua腳本實(shí)現(xiàn)分布式鎖詳解的詳細(xì)內(nèi)容,更多關(guān)于Redis Lua腳本實(shí)現(xiàn)分布式鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 通過(guò)kubesphere部署redis的方法

    通過(guò)kubesphere部署redis的方法

    這篇文章主要介紹了通過(guò)kubesphere部署redis的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-11-11
  • 淺談Redis阻塞的9種情況

    淺談Redis阻塞的9種情況

    本文主要介紹了淺談Redis阻塞的9種情況,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • Redis底層數(shù)據(jù)結(jié)構(gòu)之dict、ziplist、quicklist詳解

    Redis底層數(shù)據(jù)結(jié)構(gòu)之dict、ziplist、quicklist詳解

    本文給大家詳細(xì)介紹了Redis的底層數(shù)據(jù)結(jié)構(gòu):dict、ziplist、quicklist的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2021-09-09
  • Redis之Redisson原理詳解

    Redis之Redisson原理詳解

    Redisson 顧名思義,Redis 的兒子,本質(zhì)上還是 Redis 加鎖,不過(guò)是對(duì) Redis 做了很多封裝,它不僅提供了一系列的分布式的 Java 常用對(duì)象,還提供了許多分布式服務(wù),本文將詳細(xì)給大家介紹Redisson原理
    2023-06-06
  • Redis報(bào)錯(cuò)NOAUTH?Authentication?required簡(jiǎn)單解決辦法

    Redis報(bào)錯(cuò)NOAUTH?Authentication?required簡(jiǎn)單解決辦法

    這篇文章主要給大家介紹了關(guān)于Redis報(bào)錯(cuò)NOAUTH?Authentication?required的簡(jiǎn)單解決辦法,Redis無(wú)密碼報(bào)錯(cuò)NOAUTH Authentication required的原因是客戶端訪問(wèn)Redis時(shí)需要提供密碼,但是沒(méi)有提供或提供的密碼不正確,需要的朋友可以參考下
    2024-05-05
  • Redis緩存異常之緩存雪崩問(wèn)題解讀

    Redis緩存異常之緩存雪崩問(wèn)題解讀

    文章主要介紹了緩存雪崩、擊穿和穿透問(wèn)題,以及針對(duì)這些問(wèn)題的解決方法,包括服務(wù)熔斷、服務(wù)降級(jí)、請(qǐng)求限流和布隆過(guò)濾器等
    2025-01-01
  • Redis的使用模式之計(jì)數(shù)器模式實(shí)例

    Redis的使用模式之計(jì)數(shù)器模式實(shí)例

    這篇文章主要介紹了Redis的使用模式之計(jì)數(shù)器模式實(shí)例,本文講解了匯總計(jì)數(shù)器、按時(shí)間匯總的計(jì)數(shù)器、速度控制、使用 Hash 數(shù)據(jù)類(lèi)型維護(hù)大量計(jì)數(shù)器等內(nèi)容,需要的朋友可以參考下
    2015-03-03
  • Redis從單點(diǎn)到集群部署模式(單機(jī)模式?主從模式?哨兵模式)

    Redis從單點(diǎn)到集群部署模式(單機(jī)模式?主從模式?哨兵模式)

    這篇文章主要為大家介紹了Redis從單點(diǎn)集群部署模式(單機(jī)模式?主從模式?哨兵模式)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11
  • Redis中的慢日志

    Redis中的慢日志

    這篇文章主要介紹了Redis中的慢日志,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • 詳解緩存穿透擊穿雪崩解決方案

    詳解緩存穿透擊穿雪崩解決方案

    在我們?nèi)粘5拈_(kāi)發(fā)中,有時(shí)需要系統(tǒng)在極短的時(shí)間內(nèi)完成成千上萬(wàn)次的讀/寫(xiě)操作,這個(gè)時(shí)候不是數(shù)據(jù)庫(kù)能夠承受的,通常會(huì)引入NoSQL技術(shù)。redis技術(shù)就是NoSQL技術(shù)中的一種,但是引入redis又有可能出現(xiàn)緩存穿透,緩存擊穿,緩存雪崩等問(wèn)題。本文就對(duì)這三種問(wèn)題進(jìn)行較深入剖析。
    2021-05-05

最新評(píng)論