最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Redis Lua腳本實現(xiàn)分布式鎖方式

 更新時間:2026年05月16日 10:05:35   作者:xdpcxq1029  
文章主要介紹了在分布式系統(tǒng)中使用Lua腳本實現(xiàn)Redis分布式鎖的方法,包括可重入鎖的實現(xiàn)和注意事項,以及紅鎖算法在Redis集群狀態(tài)下的潛在問題

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

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

為了解決這一問題,我們需要引入一種跨JVM的互斥機制來管理共享資源的訪問,這就是分布式鎖所要解決的核心問題。

Lua介紹

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

為什么要用Lua呢

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

在以下場景中:

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

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

如果redis客戶端通過lua腳本把3個命令一次性發(fā)送給redis服務(wù)器,那么這三個指令就不會被其他客戶端指令打斷。

Redis 也保證腳本會以原子性(atomic)的方式執(zhí)行: 當(dāng)某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執(zhí)行。

Lua腳本命令

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

highlighter-

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:動態(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類庫方法

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

可重入性

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

下面是一個示例代碼來說明可重入性:

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

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

而對于可重入性的情況,當(dāng)線程X持有了該鎖后,在遇到加鎖方法時會直接將加鎖次數(shù)加1,并繼續(xù)執(zhí)行方法邏輯。當(dāng)退出加鎖方法時,加鎖次數(shù)再減1。只有當(dāng)加鎖次數(shù)歸零時,該線程才會真正釋放該鎖。因此,可重入性的最大特點就是計數(shù)器的存在,用于統(tǒng)計加鎖的次數(shù)。在分布式環(huán)境中實現(xiàn)可重入分布式鎖時也需要考慮如何正確統(tǒng)計和管理加鎖次數(shù)。

加鎖腳本

Redis 提供了 Hash (哈希表)這種可以存儲鍵值對數(shù)據(jù)結(jié)構(gòu)。所以我們可以使用 Redis Hash 存儲的鎖的重入次數(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]

如果鎖不存在或者這是自己的鎖,就通過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,如果鎖存在就對數(shù)量進(jìn)行減一,如果減到等于0 就直接刪除此鎖

自動續(xù)期

有可能代碼沒執(zhí)行完畢,鎖就到期了?;谏厦孢@種情況需要對鎖進(jìn)行續(xù)期。使用定時器加lua腳本進(jìn)行對鎖續(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代碼實現(xiàn)

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

以下是完整代碼:

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);
        }
        // 加鎖成功,返回之前,開啟定時器自動續(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);
    }
}

使用及測試

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

public void deduct() {
    DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lock");
    redisLock.lock();
    try {
        // 1. 查詢庫存信息
        String stock = redisTemplate.opsForValue().get("stock").toString();
        // 2. 判斷庫存是否充足
        if (stock != null && stock.length() != 0) {
            Integer st = Integer.valueOf(stock);
            if (st > 0) {
                // 3.扣減庫存
                redisTemplate.opsForValue().set("stock", String.valueOf(--st));
            }
        }
    } finally {
        redisLock.unlock();
    }
}

測試可重入性:

紅鎖算法

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

  1. 客戶端A從主節(jié)點(master)獲取到了鎖。
  2. 在主節(jié)點將鎖同步到從節(jié)點(slave)之前,主節(jié)點發(fā)生宕機。
  3. 從節(jié)點被晉升為主節(jié)點。
  4. 客戶端B獲取了同一個資源,但是客戶端A已經(jīng)在另一個鎖上獲取了鎖。在這種情況下,由于主節(jié)點宕機導(dǎo)致從節(jié)點晉升為新的主節(jié)點,可能會出現(xiàn)客戶端B誤認(rèn)為資源未被鎖定而獲取了另一個鎖的情況。這可能導(dǎo)致數(shù)據(jù)不一致性或競爭條件的發(fā)生。為了避免這種問題

總結(jié)

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 詳解redis中的下載和安裝(最新推薦)

    詳解redis中的下載和安裝(最新推薦)

    本文詳細(xì)介紹了如何在Linux和Docker上安裝、配置、啟動和關(guān)閉Redis,包括下載安裝包、解壓、編譯安裝、配置文件修改、前臺和后臺啟動、關(guān)閉以及Docker容器化部署等步驟,感興趣的朋友一起看看吧
    2025-03-03
  • Python利用redis限制用戶重復(fù)刷新帶來的數(shù)據(jù)問題

    Python利用redis限制用戶重復(fù)刷新帶來的數(shù)據(jù)問題

    在網(wǎng)站開發(fā)中,我們經(jīng)常會遇到需要控制用戶重復(fù)刷新頁面的情況,本文就來介紹了Python利用redis限制用戶重復(fù)刷新帶來的數(shù)據(jù)問題,感興趣的可以了解一下
    2024-03-03
  • Redis集群中節(jié)點更換IP后實現(xiàn)恢復(fù)集群且保留數(shù)據(jù)

    Redis集群中節(jié)點更換IP后實現(xiàn)恢復(fù)集群且保留數(shù)據(jù)

    本文介紹了如何在Redis集群中更改節(jié)點IP地址,并通過修改cluster-config-file文件中的IP地址并重啟所有節(jié)點,成功恢復(fù)集群
    2026-03-03
  • redis中熱key問題該如何解決

    redis中熱key問題該如何解決

    這篇文章主要給大家介紹了關(guān)于redis中熱key問題該如何解決的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用redis具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • Windows下Redis安裝配置簡單教程

    Windows下Redis安裝配置簡單教程

    這篇文章主要為大家詳細(xì)介紹了Windows下Redis安裝配置簡單教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • redis實現(xiàn)分布式session的解決方案

    redis實現(xiàn)分布式session的解決方案

    session存放在服務(wù)器,關(guān)閉瀏覽器不會失效,本文主要介紹了redis實現(xiàn)分布式session的解決方案,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • Windows環(huán)境部署Redis集群

    Windows環(huán)境部署Redis集群

    這篇文章主要為大家詳細(xì)介紹了Windows環(huán)境部署Redis集群的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-05-05
  • Linux服務(wù)器安裝redis數(shù)據(jù)庫圖文教程

    Linux服務(wù)器安裝redis數(shù)據(jù)庫圖文教程

    Redis是一個開源的使用ANSI C語言編寫、遵守BSD協(xié)議、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫,并提供多種語言的API。這篇文章主要介紹了Linux服務(wù)器安裝redis數(shù)據(jù)庫圖文教程,需要的朋友可以參考下
    2018-03-03
  • Redis Cluster集群動態(tài)擴容的實現(xiàn)

    Redis Cluster集群動態(tài)擴容的實現(xiàn)

    本文主要介紹了Redis Cluster集群動態(tài)擴容的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-07-07
  • redis客戶端實現(xiàn)高可用讀寫分離的方式詳解

    redis客戶端實現(xiàn)高可用讀寫分離的方式詳解

    基于sentienl 獲取和動態(tài)感知 master、slaves節(jié)點信息的變化,我們的讀寫分離客戶端就能具備高可用+動態(tài)擴容感知能力了,接下來通過本文給大家分享redis客戶端實現(xiàn)高可用讀寫分離的方式,感興趣的朋友一起看看吧
    2021-07-07

最新評論

凭祥市| 元谋县| 江北区| 神池县| 广灵县| 平泉县| 武清区| 家居| 开化县| 崇明县| 广汉市| 金门县| 辽阳县| 安多县| 卢氏县| 十堰市| 西城区| 通海县| 常德市| 孟州市| 石台县| 且末县| 湟源县| 得荣县| 孙吴县| 抚宁县| 鄂州市| 基隆市| 平凉市| 凤城市| 栾川县| 江北区| 屏边| 南江县| 抚州市| 兴义市| 准格尔旗| 许昌市| 德昌县| 会泽县| 吐鲁番市|