如何使用Redis實(shí)現(xiàn)一個(gè)安全可靠的分布式鎖

這篇文章給大家分享的是有關(guān)如何使用redis實(shí)現(xiàn)一個(gè)安全可靠的分布式鎖的內(nèi)容。小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧。

創(chuàng)新互聯(lián)建站是一家專注于網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作與策劃設(shè)計(jì),高港網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)建站做網(wǎng)站,專注于網(wǎng)站建設(shè)十載,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:高港等地區(qū)。高港做網(wǎng)站價(jià)格咨詢:028-86922220

并發(fā)場(chǎng)景下多個(gè)進(jìn)程或線程共享資源的讀寫(xiě),需要保證對(duì)資源的訪問(wèn)互斥。在單機(jī)系統(tǒng)中,我們可以使用Java并發(fā)包中的API、synchronized關(guān)鍵字等方式來(lái)解決;但是在分布式系統(tǒng)下,這些方式不再適用,我們需要自己實(shí)現(xiàn)分布式鎖。

常見(jiàn)的分布式鎖的實(shí)現(xiàn)方案有:基于數(shù)據(jù)庫(kù)、基于Redis、基于Zookeeper等。作為Redis專題的一部分,本文將基于Redis聊一聊分布式鎖的實(shí)現(xiàn)方案。

分析與實(shí)現(xiàn)


問(wèn)題分析

分布式鎖與JVM內(nèi)置的鎖有著共同的目的:讓?xiě)?yīng)用程序以預(yù)期的順序訪問(wèn)或操作共享的資源,防止多個(gè)線程同時(shí)對(duì)同一資源操作,導(dǎo)致系統(tǒng)運(yùn)行紊亂、不可控。常常用于商品庫(kù)存扣減、優(yōu)惠券扣減等場(chǎng)景。

理論上來(lái)講,為了保證鎖的安全性和有效性,分布式鎖至少需要滿足以下條件:

  • 互斥性:在同一時(shí)間內(nèi),僅有一個(gè)線程能夠獲得鎖;

  • 無(wú)死鎖:線程獲取鎖后,必須保證能夠釋放,即使線程獲取鎖后應(yīng)用程序宕機(jī),也能在限定時(shí)間內(nèi)釋放;

  • 加鎖和解鎖必須是同一個(gè)線程;

在實(shí)現(xiàn)方式上,分布式鎖大體分為三個(gè)步驟:

  • a-獲取資源的操作權(quán);

  • b-對(duì)資源執(zhí)行操作;

  • c-釋放資源的操作權(quán);

無(wú)論是Java內(nèi)置的鎖,還是分布式鎖,也無(wú)論使用哪種分布式實(shí)現(xiàn)方案,都是圍繞a、c兩個(gè)步驟展開(kāi)。Redis對(duì)于實(shí)現(xiàn)分布式鎖天然友好,原因如下:

  • 命令處理階段Redis使用單線程處理,同一個(gè)key同時(shí)只有一個(gè)線程能夠處理,沒(méi)有多線程競(jìng)態(tài)問(wèn)題。

  • SET key value NX PX milliseconds命令在不存在key的情況下添加具有過(guò)期時(shí)間的key,為安全加鎖提供支持。

  • Lua腳本和DEL命令為安全解鎖提供可靠支撐。

代碼實(shí)現(xiàn)
  • Maven依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  	<version>${your-spring-boot-version}</version>
</dependency>
  • 配置文件

在application.properties增加以下內(nèi)容,單機(jī)版Redis實(shí)例。

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
  • RedisConfig

@Configuration
public class RedisConfig {

    // 自己定義了一個(gè) RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory)
        throws UnknownHostException {
        // 我們?yōu)榱俗约洪_(kāi)發(fā)方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String,
            Object>();
        template.setConnectionFactory(factory);
        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
  • RedisLock

@Service
public class RedisLock {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 加鎖,最多等待maxWait毫秒
     *
     * @param lockKey   鎖定key
     * @param lockValue 鎖定value
     * @param timeout   鎖定時(shí)長(zhǎng)(毫秒)
     * @param maxWait   加鎖等待時(shí)間(毫秒)
     * @return true-成功,false-失敗
     */
    public boolean tryAcquire(String lockKey, String lockValue, int timeout, long maxWait) {
        long start = System.currentTimeMillis();

        while (true) {
            // 嘗試加鎖
            Boolean ret = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, timeout, TimeUnit.MILLISECONDS);
            if (!ObjectUtils.isEmpty(ret) && ret) {
                return true;
            }

            // 計(jì)算已經(jīng)等待的時(shí)間
            long now = System.currentTimeMillis();
            if (now - start > maxWait) {
                return false;
            }

            try {
                Thread.sleep(200);
            } catch (Exception ex) {
                return false;
            }
        }
    }

    /**
     * 釋放鎖
     *
     * @param lockKey   鎖定key
     * @param lockValue 鎖定value
     * @return true-成功,false-失敗
     */
    public boolean releaseLock(String lockKey, String lockValue) {
        // lua腳本
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long result = redisTemplate.opsForValue().getOperations().execute(redisScript, Collections.singletonList(lockKey), lockValue);
        return result != null && result > 0L;
    }
}
  • 測(cè)試用例

@SpringBootTest
class RedisDistLockDemoApplicationTests {

    @Resource
    private RedisLock redisLock;

    @Test
    public void testLock() {
        redisLock.tryAcquire("abcd", "abcd", 5 * 60 * 1000, 5 * 1000);
        redisLock.releaseLock("abcd", "abcd");
    }
}
安全隱患

可能很多同學(xué)(也包括我)在日常工作中都是使用上面的實(shí)現(xiàn)方式,看似是穩(wěn)妥的:

  • 使用set命令NX、PX選項(xiàng)進(jìn)行加鎖,保證了加鎖互斥,避免了死鎖;

  • 使用lua腳本解鎖,防止解除其他線程的鎖;

  • 加鎖、解鎖命令都是原子操作;

其實(shí)以上實(shí)現(xiàn)的穩(wěn)妥有個(gè)前提條件:?jiǎn)螜C(jī)版Redis、開(kāi)啟AOF持久化方式并設(shè)置appendfsync=always。

但是在哨兵模式和集群模式下可能存在問(wèn)題,為什么呢?

哨兵模式和集群模式基于主從架構(gòu),主從之間通過(guò)命令傳播實(shí)現(xiàn)數(shù)據(jù)同步,而命令傳播是異步的。

所以就存在主節(jié)點(diǎn)數(shù)據(jù)寫(xiě)入成功,在還未通知從節(jié)點(diǎn)情況下,主節(jié)點(diǎn)就宕機(jī)的可能。

當(dāng)從節(jié)點(diǎn)通過(guò)故障轉(zhuǎn)移提升為新的主節(jié)點(diǎn)后,其他線程就有機(jī)會(huì)重新加鎖成功,導(dǎo)致不滿足分布式鎖的互斥條件。

官方RedLock


集群模式下,若集群所有節(jié)點(diǎn)穩(wěn)定運(yùn)行,不出現(xiàn)故障轉(zhuǎn)移的情況下,安全性是有保障的。但是,沒(méi)有什么系統(tǒng)能夠保證100%穩(wěn)定,基于Redis的分布式鎖必須考慮容錯(cuò)。

由于主從同步基于異步復(fù)制原理,所以哨兵模式和集群模式天生無(wú)法滿足此條件。為此,Redis作者專門(mén)提出了一種解決方案——RedLock(Redis Distribute Lock)。

設(shè)計(jì)思路

根據(jù)官方文檔的說(shuō)明,把RedLock的設(shè)計(jì)思路進(jìn)行介紹。

先說(shuō)環(huán)境要求,需要N(N>=3)個(gè)獨(dú)立部署的Redis實(shí)例,相互之間不需要主從復(fù)制、故障轉(zhuǎn)移等技術(shù)。

為了獲取鎖,客戶端將按照以下流程進(jìn)行操作:

  • 獲取當(dāng)前時(shí)間(毫秒)作為開(kāi)始時(shí)間start;

  • 使用相同的key和隨機(jī)value,按順序向所有N個(gè)節(jié)點(diǎn)發(fā)起獲取鎖的請(qǐng)求。當(dāng)向每個(gè)實(shí)例設(shè)置鎖時(shí),客戶端會(huì)使用一個(gè)過(guò)期時(shí)間(小于鎖的自動(dòng)釋放時(shí)間)。比如鎖的自動(dòng)釋放時(shí)間是10秒,這個(gè)超時(shí)時(shí)間應(yīng)該是5-50毫秒。這是為了防止客戶端在一個(gè)已經(jīng)宕機(jī)的實(shí)例浪費(fèi)太多時(shí)間:如果Redis實(shí)例宕機(jī),客戶端盡快處理下一個(gè)實(shí)例。

  • 客戶端計(jì)算加鎖消耗的時(shí)間cost(cost=start-now)。只有客戶端在半數(shù)以上實(shí)例加鎖成功,并且整個(gè)耗時(shí)小于整個(gè)有效時(shí)間(ttl),才能認(rèn)為當(dāng)前客戶端加鎖成功。

  • 如果客戶端加鎖成功,那么整個(gè)鎖的真正有效時(shí)間應(yīng)該是:validTime=ttl-cost。

  • 如果客戶端加鎖失?。赡苁谦@取鎖成功實(shí)例數(shù)未過(guò)半,也可能是耗時(shí)超過(guò)ttl),那么客戶端應(yīng)該向所有實(shí)例嘗試解鎖(即使剛剛客戶端認(rèn)為加鎖失?。?。

RedLock的設(shè)計(jì)思路延續(xù)了Redis內(nèi)部多種場(chǎng)景的投票方案,通過(guò)多個(gè)實(shí)例分別加鎖解決競(jìng)態(tài)問(wèn)題,雖然加鎖消耗了時(shí)間,但是消除了主從機(jī)制下的安全問(wèn)題。

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

官方推薦Java實(shí)現(xiàn)為Redisson,它具備可重入特性,按照RedLock進(jìn)行實(shí)現(xiàn),支持獨(dú)立實(shí)例模式、集群模式、主從模式、哨兵模式等;API比較簡(jiǎn)單,上手容易。示例如下(直接通過(guò)測(cè)試用例):

    @Test
    public void testRedLock() throws InterruptedException {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        final RedissonClient client = Redisson.create(config);

        // 獲取鎖實(shí)例
        final RLock lock = client.getLock("test-lock");

        // 加鎖
        lock.lock(60 * 1000, TimeUnit.MILLISECONDS);
        try {
            // 假裝做些什么事情
            Thread.sleep(50 * 1000);
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            //解鎖
            lock.unlock();
        }
    }

Redisson封裝的非常好,我們可以像使用Java內(nèi)置的鎖一樣去使用,代碼簡(jiǎn)潔的不能再少了。關(guān)于Redisson源碼的分析,網(wǎng)上有很多文章大家可以找找看。

全文總結(jié)


分布式鎖是我們研發(fā)過(guò)程中常用的的一種解決并發(fā)問(wèn)題的方式,Redis是只是一種實(shí)現(xiàn)方式。

關(guān)鍵的是要弄清楚加鎖、解鎖背后的原理,以及實(shí)現(xiàn)分布式鎖需要解決的核心問(wèn)題,同時(shí)考慮我們所采用的中間件有什么特性可以支撐。了解這些后,實(shí)現(xiàn)起來(lái)就不是什么問(wèn)題了。

感謝各位的閱讀!關(guān)于“如何使用Redis實(shí)現(xiàn)一個(gè)安全可靠的分布式鎖”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!

網(wǎng)頁(yè)標(biāo)題:如何使用Redis實(shí)現(xiàn)一個(gè)安全可靠的分布式鎖
文章源于:http://muchs.cn/article28/jcpdcp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站排名標(biāo)簽優(yōu)化、外貿(mào)網(wǎng)站建設(shè)、定制網(wǎng)站網(wǎng)站制作、做網(wǎng)站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

微信小程序開(kāi)發(fā)