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

redis cluster集群模式下實(shí)現(xiàn)批量可重入鎖

 更新時(shí)間:2024年02月28日 09:32:17   作者:RachelHwang  
本文主要介紹了使用redis cluster集群版所遇到的問題解決方案及redis可重入鎖是否會(huì)有死鎖的問題等,具有一定的參考價(jià)值,感興趣的可以了解一下

一、redis cluster 集群版

在Redis 3.0版本以后,Redis發(fā)布了Redis Cluster。該集群主要支持搞并發(fā)和海量數(shù)據(jù)處理等優(yōu)勢,當(dāng) Redis 在集群模式下運(yùn)行時(shí),它處理數(shù)據(jù)存儲(chǔ)的方式與作為單個(gè)實(shí)例運(yùn)行時(shí)不同。這是因?yàn)樗鼞?yīng)該準(zhǔn)備好跨多個(gè)節(jié)點(diǎn)分發(fā)數(shù)據(jù),從而實(shí)現(xiàn)水平可擴(kuò)展性。具體能力表現(xiàn)為:

  • 自動(dòng)分割數(shù)據(jù)到不同的節(jié)點(diǎn)上
  • 整個(gè)集群的部分節(jié)點(diǎn)失敗或者不可達(dá)的情況下能夠繼續(xù)處理命令

Redis沒有使用一致性hash,而是引入哈希槽的概念,也就是 Hash Slot。Redis集群由16384個(gè)哈希槽slot,每個(gè)key通過CRC16校驗(yàn)后對16384取模來決定放置那個(gè)槽,集群的每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分hash槽,也就是說數(shù)據(jù)存放在hash槽里,而每個(gè)節(jié)點(diǎn)只負(fù)責(zé)部分hash槽(這樣數(shù)據(jù)就存放在不同的節(jié)點(diǎn))。

例如:node1、node2、node3三個(gè)節(jié)點(diǎn),node1節(jié)點(diǎn)負(fù)責(zé)0到5500號hash槽,node2節(jié)點(diǎn)負(fù)責(zé)5501到11000號hash槽,node3節(jié)點(diǎn)負(fù)責(zé)11001到16384號hash槽。這種結(jié)構(gòu)很容易添加或者刪除節(jié)點(diǎn),比如如果我想新添加個(gè)節(jié)點(diǎn)node4, 我需要從節(jié)點(diǎn) node1, node2, node3中得部分槽到node4上. 如果我想移除節(jié)點(diǎn)node1,需要將node1中的槽移到node2和node3節(jié)點(diǎn)上,然后將沒有任何槽的node1節(jié)點(diǎn)從集群中移除即可. 由于從一個(gè)節(jié)點(diǎn)將哈希槽移動(dòng)到另一個(gè)節(jié)點(diǎn)并不會(huì)停止服務(wù),所以無論添加刪除或者改變某個(gè)節(jié)點(diǎn)的哈希槽的數(shù)量都不會(huì)造成集群不可用的狀態(tài)。

在這里插入圖片描述

在某些集群方案中,涉及多個(gè)key的操作會(huì)被限制在一個(gè)slot中,如Redis Cluster中的mget/mset操作。這里就會(huì)涉及到 哈希標(biāo)簽 Hash Tag 的概念。

Hash Tag是用于計(jì)算哈希槽時(shí)的一個(gè)特殊場景,是一種確保多個(gè)鍵分配到同一個(gè)哈希槽中的方法。這是為了在Redis集群中實(shí)現(xiàn)多鍵操作而使用的。為了實(shí)現(xiàn)Hash Tag,在某些情況下,會(huì)以稍微不同的方式計(jì)算key的哈希槽。如果key包含"{…}"模式,則僅對{和}之間的子字符串進(jìn)行散列以獲取哈希槽。但由于可能存在多個(gè){或}出現(xiàn),因此該算法遵循以下規(guī)則:

  • 如果key包含字符 {
  • 并且如果 } 字符位于 { 的右側(cè)
  • 并且在第一個(gè) { 和第一個(gè) } 之間存在一個(gè)或多個(gè)字符

對于符合上述規(guī)則的key,則不會(huì)對整個(gè)key進(jìn)行散列處理,而只會(huì)對第一次出現(xiàn) { 和隨后第一次出現(xiàn) } 之間的內(nèi)容進(jìn)行散列。否則,對整個(gè)key進(jìn)行散列處理。

在這里插入圖片描述

不使用hash tag批量設(shè)置不同名稱的key:

127.0.0.1:6379> mset name name1 name2 name3
(error) CROSSSLOT Keys in request don't hash to the same slot

顯示錯(cuò)誤信息:CROSSSLOT 請求中的key沒有哈希到同一個(gè)插槽。這個(gè)問題是因?yàn)槎噫I操作的時(shí)候每個(gè)鍵對應(yīng)的slot可能不是一個(gè),客戶端沒法做move操作。

解決思路就是采用redis cluster的hashTag,當(dāng)redis的key加上hashTag時(shí),集群算key的slot是按照hashTag進(jìn)行計(jì)算,即可保證hashTag一致的key能分配到相同的stlot中。:

127.0.0.1:6379> mset name {name} {name}1 {name}2 {name}3

二、redis 分布式鎖

Redis鎖使用起來比較簡單,既可以鎖定單個(gè)鍵,也可以批量鎖定多個(gè)鍵,以實(shí)現(xiàn)更大規(guī)模的操作。它也是分布式應(yīng)用中使用最廣泛的分布式鎖實(shí)施方式,可以有效解決單點(diǎn)故障、死鎖和負(fù)載失衡等問題。

大規(guī)模鎖定Redis,實(shí)現(xiàn)批量操作,一般通過以下實(shí)現(xiàn):

  • 使用Redis的消息訂閱機(jī)制,創(chuàng)建消息頻道,用于鎖定指定鍵之間的多個(gè)鍵。消息頻道的名字稱為鎖名,它代表鎖定的范圍和跨度。
  • 然后,通過Redis的SUBSCRIBE命令訂閱消息頻道名字,比如“ lock_key”,并調(diào)用Redis BLPOP,將鎖定的鍵占據(jù),以實(shí)現(xiàn)批量鎖定。
  • 此外,也可以使用Redis的Lua腳本實(shí)現(xiàn)批量鎖定。獲取帶鎖的Key數(shù)組,這里以數(shù)組形式表示。同時(shí),以原子的形式執(zhí)行多個(gè)SETNX命令,一旦全部執(zhí)行成功,則實(shí)現(xiàn)批量鎖定:
local locks = red:lrange("lock_keys", 1, -1)
for i, v in iprs(locks) do
    if redis.call("setnx", v, field) == 1 then
        red.lpush("locked_keys", v)
    end
end

釋放鎖定的鍵,實(shí)現(xiàn)批量解鎖,語句如下:

local unlocked_locks = red:lrange("locked_keys",1, -1)
for i, v in iprs(unlocked_locks) do
    red.del(v)
end
red.del("locked_keys")

使用Redis的WATCH功能,防止多個(gè)客戶端同時(shí)更新同一鍵,即如果更新發(fā)生樂觀鎖的沖突的情況下,返回失敗給客戶端,從而保證了鎖定的原子性:

-- 使用Redis watch,開始監(jiān)聽
red.watch("lock_keys")
-- 進(jìn)行具體操作
-- …
-- 解鎖操作
red.unwatch()

Redis鎖使用起來非常簡單,可以用于單個(gè)鍵鎖定和大規(guī)模鎖定,從而實(shí)現(xiàn)批量操作,有效解決分布式應(yīng)用中的死鎖、負(fù)載失衡、單點(diǎn)故障等問題。

三、如何使用 redis 實(shí)現(xiàn)批量可重入鎖?

1、方案一:Lua腳本批量加鎖

Lua加鎖腳本處理:

	/**
	 * 加鎖腳本
	 * KEYS[1] key
	 * ARGV[1] value
	 * ARGV[2] expire
	 * 判斷key是否存在,不存在則加鎖,并記錄加鎖次數(shù)+1;若存在,則判斷value是否相等,相等則記錄加鎖次數(shù)+1,不相等則返回0
	 */
	private static final String REENTRANT_LOCK_SCRIPT = "if redis.call('EXISTS', KEYS[1]) == 0 then " +
        "    redis.call('SET', KEYS[1], ARGV[1]) " +
        "    redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
        "    redis.call('INCR', 'lockCount:' .. KEYS[1]) " +
        "    return 1 " +
        "else " +
        "    if redis.call('GET', KEYS[1]) == ARGV[1] then " +
        "        redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
        "        redis.call('INCR', 'lockCount:' .. KEYS[1]) " +
        "        return 1 " +
        "    else " +
        "        return 0 " +
        "    end " +
        "end";
        
    @Bean
    public DefaultRedisScript<Long> reentrantLockRedisScript(){
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(REENTRANT_LOCK_SCRIPT, Long.class);
        try {
            List<Boolean> results= redisTemplate.getConnectionFactory().getConnection().scriptExists(redisScript.getSha1());
            if(Boolean.FALSE.equals(results.get(0)))
            {
                redisTemplate.getConnectionFactory().getConnection().scriptLoad(redisScript.getScriptAsString().getBytes());
                log.info("redis reentrantLockRedisScript load success");
            }

        }catch (Exception ex){
            log.error("redis reentrantLockRedisScript load error",ex);
        }

        return redisScript;
    }

Lua解鎖腳本處理:

    private static final String RELEASE_REENTRANT_LOCK_SCRIPT = "if redis.call('EXISTS', KEYS[1]) == 0 then " +
            "    return 0 " +
            "else " +
            "    if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "        redis.call('DECR', 'lockCount:' .. KEYS[1]) " +
            "        if redis.call('GET', 'lockCount:' .. KEYS[1]) == '0' then " +
            "            redis.call('DEL', KEYS[1]) " +
            "        end " +
            "        return 1 " +
            "    else " +
            "        return 0 " +
            "    end " +
            "end";
    @Bean
    public DefaultRedisScript<Long> releaseReentrantLockRedisScript(){
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_REENTRANT_LOCK_SCRIPT, Long.class);
        try {
            List<Boolean> results= redisTemplate.getConnectionFactory().getConnection().scriptExists(redisScript.getSha1());
            if(Boolean.FALSE.equals(results.get(0)))
            {
                redisTemplate.getConnectionFactory().getConnection().scriptLoad(redisScript.getScriptAsString().getBytes());
                log.info("redis releaseReentrantLockRedisScript load success");
            }

        }catch (Exception ex){
            log.error("redis releaseReentrantLockRedisScript load error",ex);
        }

        return redisScript;
    }

redis set 多個(gè)key場景需要hash tag處理:

private static final String KEY_FORMAT = "%s{%s}";
String slotKey = String.format(KEY_FORMAT, key, key);

結(jié)合以上封裝,批量可重入鎖的方法處理如下:

/**
     * 可重入鎖,value相同的情況下,可重復(fù)加鎖,當(dāng)所有加鎖方都解鎖后才會(huì)釋放鎖
     *
     * @param key           鎖的key
     * @param value         鎖的value
     * @param expireSeconds 鎖過期時(shí)間
     * @param waitSeconds   等待時(shí)間
     * @param process
     * @return
     */
    public boolean reentrantLock(String key, String value, long expireSeconds, long waitSeconds, Runnable process) {
        boolean lock = false;
        try {
            //特殊處理key,為了保證原key和計(jì)數(shù)key落在同一個(gè)slot,將原key拼裝成: key{key}
            String slotKey = String.format(KEY_FORMAT, key, key);
            long start = System.currentTimeMillis();
            while (!(lock = redisUtil.execute(reentrantLockRedisScript, Collections.singletonList(slotKey), value, expireSeconds) == 1)) {
                Thread.sleep(100);
                if ((System.currentTimeMillis() - start) / 1000 > waitSeconds) {
                    break;
                }
            }
            //加鎖成功,執(zhí)行傳入的方法,最后用lua腳本判斷鎖的value是否還是當(dāng)前的value,是則執(zhí)行解鎖
            if (lock) {
                try {
                    process.run();
                } catch (Exception ex) {
                    throw ex instanceof BusinessException ? ex : new BusinessException(ex.getMessage());
                } finally {
                    redisUtil.execute(releaseReentrantLockRedisScript, Collections.singletonList(slotKey), value);
                }
            }

        } catch (BusinessException businessException) {
            throw businessException;
        } catch (Exception e) {
            log.error("redis lockAndRun error!lockKey=" + key, e);
        }
        return lock;
    }

    /**
     * 先獲取可重入鎖,獲取成功后批量加鎖,執(zhí)行傳入的方法
     *
     * @param keys
     * @param reentrantKey
     * @param value
     * @param expireSeconds
     * @param waitSeconds
     * @param process
     * @return
     */
    public boolean batchReentrantLock(Set<String> keys, String reentrantKey, String value, long expireSeconds, long waitSeconds, Runnable process) {
        List<Boolean> result = new ArrayList<>(1);
        boolean reentrantLock = reentrantLock(reentrantKey, value, expireSeconds, waitSeconds, () -> {
            result.add(batchLockAndRun(keys, expireSeconds, waitSeconds, process));
        });
        return reentrantLock && result.get(0);
    }

2、方案二:pipeline批量加鎖

  • 不用lua以避免cross slot error
  • 批量加鎖失敗后立即全部解鎖,防止死鎖
	/**
	 * 使用redisTemplate
	 * @param script
	 * @param keys
	 * @param args
	 * @param <T>
	 */
	public List<Object> executePipelined(String[] keys, String[] values, long time, TimeUnit timeUnit) {
	      return redisTemplate.executePipelined(new SessionCallback<Object>() {
	           @Override
	           public Object execute(RedisOperations operations) throws DataAccessException {
	               for (int i = 0; i < keys.length; i++) {
	                   operations.opsForValue().setIfAbsent(keys[i], values[i], time, timeUnit);
	               }
	               return null;
	           }
	       });
	}
	
	/**
	 * pipeline批量加鎖
	 * @param keys          需要加鎖的key
	 * @param values        鎖的value,用于解鎖時(shí)判斷是否是當(dāng)前線程加的鎖
	 * @param expireSeconds 鎖過期時(shí)間
	 * @return boolean 是否加鎖成功
	 */
	private boolean tryBatchLock(String[] keys, String[] values, long expireSeconds) {
	    List<Object> results = redisUtil.executePipelined(keys, values, expireSeconds, TimeUnit.SECONDS);
	    if (results == null || results.size() != keys.length || results.contains(false)) {
	        //加鎖失敗,立即解鎖
	        redisUtil.executePipelined(releaseReentrantLockRedisScript, keys, values);
	        return false;
	    }
	    return true;
	}

四、總結(jié)

以上是使用redis cluster集群版所遇到的問題以及解決方案,主要在業(yè)務(wù)實(shí)現(xiàn)過程中,需要注意redis cluster key會(huì)被劃分到不同的槽中的問題,以及redis可重入鎖是否會(huì)有死鎖的問題等,更多相關(guān)redis cluster 批量可重入鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis 2.8-4.0過期鍵優(yōu)化過程全紀(jì)錄

    Redis 2.8-4.0過期鍵優(yōu)化過程全紀(jì)錄

    這篇文章主要給大家介紹了關(guān)于Redis 2.8-4.0過期鍵優(yōu)化的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • 基于Redis無序集合如何實(shí)現(xiàn)禁止多端登錄功能

    基于Redis無序集合如何實(shí)現(xiàn)禁止多端登錄功能

    這篇文章主要給你大家介紹了關(guān)于基于Redis無序集合如何實(shí)現(xiàn)禁止多端登錄功能的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2018-12-12
  • Redis中的BigKey問題排查與解決思路詳解

    Redis中的BigKey問題排查與解決思路詳解

    Redis是一款性能強(qiáng)勁的內(nèi)存數(shù)據(jù)庫,但是在使用過程中,我們可能會(huì)遇到Big Key問題,這個(gè)問題就是Redis中某個(gè)key的value過大,所以Big Key問題本質(zhì)是Big Value問題,這篇文章主要介紹了Redis中的BigKey問題:排查與解決思路,需要的朋友可以參考下
    2023-03-03
  • 從原理到實(shí)踐分析?Redis?分布式鎖的多種實(shí)現(xiàn)方案

    從原理到實(shí)踐分析?Redis?分布式鎖的多種實(shí)現(xiàn)方案

    在分布式系統(tǒng)中,為了保證多個(gè)進(jìn)程或線程之間的數(shù)據(jù)一致性和正確性,需要使用鎖來實(shí)現(xiàn)互斥訪問共享資源,然而,使用本地鎖在分布式系統(tǒng)中存在問題,這篇文章主要介紹了從原理到實(shí)踐分析?Redis?分布式鎖的多種實(shí)現(xiàn)方案,需要的朋友可以參考下
    2024-07-07
  • 詳解Redis中的List是如何實(shí)現(xiàn)的

    詳解Redis中的List是如何實(shí)現(xiàn)的

    List 的 Redis 中的 5 種主要數(shù)據(jù)結(jié)構(gòu)之一,它是一種序列集合,可以存儲(chǔ)一個(gè)有序的字符串列表,順序是插入的順序,本文將給大家介紹了一下Redis中的List是如何實(shí)現(xiàn)的,需要的朋友可以參考下
    2024-05-05
  • Linux下Redis安裝配置教程

    Linux下Redis安裝配置教程

    這篇文章主要為大家詳細(xì)介紹了Linux下Redis安裝配置教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-11-11
  • 詳解Redis中key的命名規(guī)范和值的命名規(guī)范

    詳解Redis中key的命名規(guī)范和值的命名規(guī)范

    這篇文章主要介紹了詳解Redis中key的命名規(guī)范和值的命名規(guī)范,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • Redis數(shù)據(jù)庫分布式設(shè)計(jì)方案介紹

    Redis數(shù)據(jù)庫分布式設(shè)計(jì)方案介紹

    大家好,本篇文章主要講的是Redis數(shù)據(jù)庫分布式設(shè)計(jì)方案介紹,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下
    2022-01-01
  • Redis?鍵值對(key-value)數(shù)據(jù)庫實(shí)現(xiàn)方法

    Redis?鍵值對(key-value)數(shù)據(jù)庫實(shí)現(xiàn)方法

    Redis 的鍵值對中的 key 就是字符串對象,而 value 可以是字符串對象,也可以是集合數(shù)據(jù)類型的對象,比如 List 對象,Hash 對象、Set 對象和 Zset 對象,這篇文章主要介紹了Redis?鍵值對數(shù)據(jù)庫是怎么實(shí)現(xiàn)的,需要的朋友可以參考下
    2024-05-05
  • 在Redis中如何保存時(shí)間序列數(shù)據(jù)詳解

    在Redis中如何保存時(shí)間序列數(shù)據(jù)詳解

    與發(fā)生時(shí)間相關(guān)的一組數(shù)據(jù),就是時(shí)間序列數(shù)據(jù),這些數(shù)據(jù)的特點(diǎn)是沒有嚴(yán)格的關(guān)系模型,記錄的信息可以表示成鍵和值的關(guān)系,這篇文章主要給大家介紹了關(guān)于在Redis中如何保存時(shí)間序列數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下
    2021-10-10

最新評論