成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

基于 Redis 的分布式鎖

fasss / 1754人閱讀

摘要:首先談到分布式鎖自然也就聯(lián)想到分布式應(yīng)用。如基于的唯一索引?;诘呐R時(shí)有序節(jié)點(diǎn)。這里主要基于進(jìn)行討論。該命令可以保證的原子性。所以最好的方式是在每次解鎖時(shí)都需要判斷鎖是否是自己的。總結(jié)至此一個(gè)基于的分布式鎖完成,但是依然有些問題。

前言

分布式鎖在分布式應(yīng)用中應(yīng)用廣泛,想要搞懂一個(gè)新事物首先得了解它的由來,這樣才能更加的理解甚至可以舉一反三。

首先談到分布式鎖自然也就聯(lián)想到分布式應(yīng)用。

在我們將應(yīng)用拆分為分布式應(yīng)用之前的單機(jī)系統(tǒng)中,對一些并發(fā)場景讀取公共資源時(shí)如扣庫存,賣車票之類的需求可以簡單的使用同步或者是加鎖就可以實(shí)現(xiàn)。

但是應(yīng)用分布式了之后系統(tǒng)由以前的單進(jìn)程多線程的程序變?yōu)榱硕噙M(jìn)程多線程,這時(shí)使用以上的解決方案明顯就不夠了。

因此業(yè)界常用的解決方案通常是借助于一個(gè)第三方組件并利用它自身的排他性來達(dá)到多進(jìn)程的互斥。如:

基于 DB 的唯一索引。

基于 ZK 的臨時(shí)有序節(jié)點(diǎn)。

基于 Redis 的 NX EX 參數(shù)。

這里主要基于 Redis 進(jìn)行討論。

實(shí)現(xiàn)

既然是選用了 Redis,那么它就得具有排他性才行。同時(shí)它最好也有鎖的一些基本特性:

高性能(加、解鎖時(shí)高性能)

可以使用阻塞鎖與非阻塞鎖。

不能出現(xiàn)死鎖。

可用性(不能出現(xiàn)節(jié)點(diǎn) down 掉后加鎖失敗)。

這里利用 Redis set key 時(shí)的一個(gè) NX 參數(shù)可以保證在這個(gè) key 不存在的情況下寫入成功。并且再加上 EX 參數(shù)可以讓該 key 在超時(shí)之后自動(dòng)刪除。

所以利用以上兩個(gè)特性可以保證在同一時(shí)刻只會(huì)有一個(gè)進(jìn)程獲得鎖,并且不會(huì)出現(xiàn)死鎖(最壞的情況就是超時(shí)自動(dòng)刪除 key)。

加鎖

實(shí)現(xiàn)代碼如下:

    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    
    public  boolean tryLock(String key, String request) {
        String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);

        if (LOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

注意這里使用的 jedis 的

String set(String key, String value, String nxxx, String expx, long time);

api。

該命令可以保證 NX EX 的原子性。

一定不要把兩個(gè)命令(NX EX)分開執(zhí)行,如果在 NX 之后程序出現(xiàn)問題就有可能產(chǎn)生死鎖。

阻塞鎖

同時(shí)也可以實(shí)現(xiàn)一個(gè)阻塞鎖:

    //一直阻塞
    public void lock(String key, String request) throws InterruptedException {

        for (;;){
            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                break ;
            }
                
              //防止一直消耗 CPU     
            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }

    }
    
     //自定義阻塞時(shí)間
     public boolean lock(String key, String request,int blockTime) throws InterruptedException {

        while (blockTime >= 0){

            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                return true ;
            }
            blockTime -= DEFAULT_SLEEP_TIME ;

            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }
        return false ;
    }
解鎖

解鎖也很簡單,其實(shí)就是把這個(gè) key 刪掉就萬事大吉了,比如使用 del key 命令。

但現(xiàn)實(shí)往往沒有那么 easy。

如果進(jìn)程 A 獲取了鎖設(shè)置了超時(shí)時(shí)間,但是由于執(zhí)行周期較長導(dǎo)致到了超時(shí)時(shí)間之后鎖就自動(dòng)釋放了。這時(shí)進(jìn)程 B 獲取了該鎖執(zhí)行很快就釋放鎖。這樣就會(huì)出現(xiàn)進(jìn)程 B 將進(jìn)程 A 的鎖釋放了。

所以最好的方式是在每次解鎖時(shí)都需要判斷鎖是否是自己的。

這時(shí)就需要結(jié)合加鎖機(jī)制一起實(shí)現(xiàn)了。

加鎖時(shí)需要傳遞一個(gè)參數(shù),將該參數(shù)作為這個(gè) key 的 value,這樣每次解鎖時(shí)判斷 value 是否相等即可。

所以解鎖代碼就不能是簡單的 del了。

    public  boolean unlock(String key,String request){
        //lua script
        String script = "if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end";

        Object result = null ;
        if (jedis instanceof Jedis){
            result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else if (jedis instanceof JedisCluster){
            result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else {
            //throw new RuntimeException("instance is error") ;
            return false ;
        }

        if (UNLOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

這里使用了一個(gè) lua 腳本來判斷 value 是否相等,相等才執(zhí)行 del 命令。

使用 lua 也可以保證這里兩個(gè)操作的原子性。

因此上文提到的四個(gè)基本特性也能滿足了:

使用 Redis 可以保證性能。

阻塞鎖與非阻塞鎖見上文。

利用超時(shí)機(jī)制解決了死鎖。

Redis 支持集群部署提高了可用性。

使用

我自己有擼了一個(gè)完整的實(shí)現(xiàn),并且已經(jīng)用于了生產(chǎn),有興趣的朋友可以開箱使用:

maven 依賴:


    top.crossoverjie.opensource
    distributed-redis-lock
    1.0.0

配置 bean :

@Configuration
public class RedisLockConfig {

    @Bean
    public RedisLock build(){
        RedisLock redisLock = new RedisLock() ;
        HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ;
        JedisCluster jedisCluster = new JedisCluster(hostAndPort) ;
        // Jedis 或 JedisCluster 都可以
        redisLock.setJedisCluster(jedisCluster) ;
        return redisLock ;
    }

}

使用:

    @Autowired
    private RedisLock redisLock ;

    public void use() {
        String key = "key";
        String request = UUID.randomUUID().toString();
        try {
            boolean locktest = redisLock.tryLock(key, request);
            if (!locktest) {
                System.out.println("locked error");
                return;
            }


            //do something

        } finally {
            redisLock.unlock(key,request) ;
        }

    }

使用很簡單。這里主要是想利用 Spring 來幫我們管理 RedisLock 這個(gè)單例的 bean,所以在釋放鎖的時(shí)候需要手動(dòng)(因?yàn)檎麄€(gè)上下文只有一個(gè) RedisLock 實(shí)例)的傳入 key 以及 request(api 看起來不是特別優(yōu)雅)。

也可以在每次使用鎖的時(shí)候 new 一個(gè) RedisLock 傳入 key 以及 request,這樣倒是在解鎖時(shí)很方便。但是需要自行管理 RedisLock 的實(shí)例。各有優(yōu)劣吧。

項(xiàng)目源碼在:

https://github.com/crossoverJie/distributed-lock-redis

歡迎討論。

單測

在做這個(gè)項(xiàng)目的時(shí)候讓我不得不想提一下單測。

因?yàn)檫@個(gè)應(yīng)用是強(qiáng)依賴于第三方組件的(Redis),但是在單測中我們需要排除掉這種依賴。比如其他伙伴 fork 了該項(xiàng)目想在本地跑一遍單測,結(jié)果運(yùn)行不起來:

有可能是 Redis 的 ip、端口和單測里的不一致。

Redis 自身可能也有問題。

也有可能是該同學(xué)的環(huán)境中并沒有 Redis。

所以最好是要把這些外部不穩(wěn)定的因素排除掉,單測只測我們寫好的代碼。

于是就可以引入單測利器 Mock 了。

它的想法很簡答,就是要把你所依賴的外部資源統(tǒng)統(tǒng)屏蔽掉。如:數(shù)據(jù)庫、外部接口、外部文件等等。

使用方式也挺簡單,可以參考該項(xiàng)目的單測:

    @Test
    public void tryLock() throws Exception {
        String key = "test";
        String request = UUID.randomUUID().toString();
        Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");

        boolean locktest = redisLock.tryLock(key, request);
        System.out.println("locktest=" + locktest);

        Assert.assertTrue(locktest);

        //check
        Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong());
    }

這里只是簡單演示下,可以的話下次仔細(xì)分析分析。

它的原理其實(shí)也挺簡單,debug 的話可以很直接的看出來:

這里我們所依賴的 JedisCluster 其實(shí)是一個(gè) cglib 代理對象。所以也不難想到它是如何工作的。

比如這里我們需要用到 JedisCluster 的 set 函數(shù)并需要它的返回值。

Mock 就將該對象代理了,并在實(shí)際執(zhí)行 set 方法后給你返回了一個(gè)你自定義的值。

這樣我們就可以隨心所欲的測試了,完全把外部依賴所屏蔽了

總結(jié)

至此一個(gè)基于 Redis 的分布式鎖完成,但是依然有些問題。

如在 key 超時(shí)之后業(yè)務(wù)并沒有執(zhí)行完畢但卻自動(dòng)釋放鎖了,這樣就會(huì)導(dǎo)致并發(fā)問題。

就算 Redis 是集群部署的,如果每個(gè)節(jié)點(diǎn)都只是 master 沒有 slave,那么 master 宕機(jī)時(shí)該節(jié)點(diǎn)上的所有 key 在那一時(shí)刻都相當(dāng)于是釋放鎖了,這樣也會(huì)出現(xiàn)并發(fā)問題。就算是有 slave 節(jié)點(diǎn),但如果在數(shù)據(jù)同步到 salve 之前 master 宕機(jī)也是會(huì)出現(xiàn)上面的問題。

感興趣的朋友還可以參考 Redisson 的實(shí)現(xiàn)。

號外

最近在總結(jié)一些 Java 相關(guān)的知識(shí)點(diǎn),感興趣的朋友可以一起維護(hù)。

地址: https://github.com/crossoverJie/Java-Interview

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/68886.html

相關(guān)文章

  • 基于Redis實(shí)現(xiàn)布式

    摘要:本篇博客將介紹第二種方式,基于的實(shí)現(xiàn)分布式鎖??偨Y(jié)本文主要介紹了如何使用代碼正確實(shí)現(xiàn)分布式鎖,對于加鎖和解鎖也分別給出了兩個(gè)比較經(jīng)典的錯(cuò)誤示例。其實(shí)想要通過實(shí)現(xiàn)分布式鎖并不難,只要保證能滿足可靠性里的四個(gè)條件。 前言 分布式鎖一般有三種實(shí)現(xiàn)方式:1.數(shù)據(jù)庫樂觀鎖;2、基于Redis的分布式鎖;3.基于Zookeeper的分布式鎖。本篇博客將介紹第二種方式,基于Redis的實(shí)現(xiàn)分布式鎖。...

    jonh_felix 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<