电竞比分网-中国电竞赛事及体育赛事平台

分享

AP模式(Redis)的分布式鎖分析以及實現(xiàn)

 印度阿三17 2020-06-16

分布式CAP理論

在介紹分布式鎖之前,先說一下CAP理論。因為現(xiàn)在提到分布式系統(tǒng)一定離不開CAP理論。C(Consistency)一致性、A(Availability)可用性、P(Partition tolerance)分區(qū)容錯性。三者不能同時存在,由于P是必要因素,所以分為CP和AP兩種模型。下面我們就根據(jù)AP和CP模型來分析一下分布式鎖以及使用場景。

AP模型的分布式鎖

AP模型的分布式鎖是基于Redis來實現(xiàn)的。Redis集群在分布式系統(tǒng)中是一種AP模型,無法保證在主節(jié)點宕機時自動完成數(shù)據(jù)一致性的同步操作,因此在業(yè)務(wù)要求保證一致性的場景中,Redis的分布式鎖會在主節(jié)點宕機的情況下丟失鎖信息而出現(xiàn)重復(fù)上鎖的極端情況。

Redis分布式鎖原理

  1. SETNX:Redis的分布式鎖主要是使用Redis的SETNX命令來完成上鎖的操作。此條命令的官方解釋為:只在鍵 key 不存在的情況下, 將鍵 key 的值設(shè)置為 value 。若鍵 key 已經(jīng)存在, 則 SETNX 命令不做任何動作。在設(shè)置成功時返回 1 , 設(shè)置失敗時返回 0 。
  2. expire:Redis支持key設(shè)置過期時間,因此我們在設(shè)計鎖的時候會設(shè)置一個過期時間來使得key有自動過期的機制。但是單純的只設(shè)置過期時間會有問題,在下一個小結(jié)介紹問題所在。
  3. 續(xù)租:上面提到了單純的設(shè)置過期時間會產(chǎn)生在持有鎖的期間內(nèi)邏輯沒有處理完而自動釋放鎖的問題。例如當前線程在獲得鎖后,設(shè)置了過期時間為1秒,但是由于某些原因?qū)е禄蛘呤谴a的bug使得此次代碼邏輯時間超過了1秒,這時導(dǎo)致鎖被釋放,而此時下一個線程重新獲得了鎖,導(dǎo)致最終業(yè)務(wù)受到影響,波及整個系統(tǒng)的數(shù)據(jù)問題。因此需要續(xù)租的機制來保證在當前線程沒有執(zhí)行完的時候不會自動釋放鎖,從而保證業(yè)務(wù)數(shù)據(jù)的安全。

Redis分布式鎖的實現(xiàn)

一、 基于jedis分布式鎖的實現(xiàn)
為了保證SETNX和expire的原子操作,可以通過redis的一條命令來完成。
在這里插入圖片描述
上圖中的NX和XX參數(shù)介紹一下,NX為如果key不存在則設(shè)置一個value。XX為只在鍵已經(jīng)存在時, 才對鍵進行設(shè)置操作,XX為續(xù)租來做準備。此條命令可以省去了寫lua腳本來保證setnx和expire的原子性操作。
二、基于redisson分布式鎖的實現(xiàn)
redisson是redis的一個客戶端,封裝了基本的redis操作還有對分布式鎖的支持,redisson實現(xiàn)了自動續(xù)租的操作,上手更加容易,操作簡單。redisson的看門狗機制就是實現(xiàn)了對過期時間的自動續(xù)租功能,如果在業(yè)務(wù)中出現(xiàn)了死循環(huán)代碼或者是處理時間過長的問題,會導(dǎo)致看門狗無限續(xù)租的情況出現(xiàn),此時我們需要保證業(yè)務(wù)代碼的健壯性以及增加對鎖的監(jiān)控手段,避免線上出現(xiàn)死鎖問題導(dǎo)致排查困難。

Redis分布式鎖代碼

一、jedis的分布式鎖代碼,注解實現(xiàn)

@Around("lockPoint()")
    public Object redisDistributedLock(ProceedingJoinPoint pjp) throws Throwable {
        //獲取RedisLock注解信息
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        RedisLock lockInfo = method.getAnnotation(RedisLock.class);
        String lockKey = lockInfo.value();
        if (StringUtils.isBlank(lockKey)) {
            throw new IllegalArgumentException("配置參數(shù)錯誤,lockKey不能為空!");
        }
        boolean lock = false;
        Object obj = null;
        try {
            // 獲取鎖的最大超時時間
            long maxSleepMills = System.currentTimeMillis()   lockInfo.maxSleepMills();
            while (!lock) {
                //持鎖時間
                String keepMills = String.valueOf(System.currentTimeMillis()   lockInfo.keepMills());
                //上鎖
                lock = jedisService.setNX(lockKey, keepMills, lockInfo.keepMills());
                // 得到鎖,沒有人加過相同的鎖
                if (lock) {
                    logger.info("得到鎖...");
                    obj = pjp.proceed();
                }
                // 已過期,并且getAndSet后舊的時間戳依然是過期的,可以認為獲取到了鎖
                else if (System.currentTimeMillis() > jedisService.get(lockKey) &&
                        (System.currentTimeMillis() > jedisService.getAndSet(lockKey, keepMills))) {
                    lock = true;
                    logger.info("得到鎖...");
                    obj = pjp.proceed();
                }
                // 沒有得到任何鎖
                else {
                    // 繼續(xù)等待獲取鎖
                    if (lockInfo.action().equals(RedisLock.LockFailAction.CONTINUE)) {
                        // 如果超過最大等待時間拋出異常
                        logger.info("稍后重新請求鎖...");
                        if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
                            throw new TimeoutException("獲取鎖資源等待超時");
                        }
                        TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
                    } else {
                        // 放棄等待
                        logger.info("放棄鎖...");
                        break;
                    }
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            // 如果獲取到了鎖,釋放鎖
            if (lock) {
                //鎖沒有過期就刪除key
                if (System.currentTimeMillis() < (System.currentTimeMillis()   lockInfo.keepMills())) {
                    logger.info("釋放鎖...");
                    jedisService.delete(lockKey);
                }

            }
        }
        return obj;
    }
public boolean setNX(String key, String value, long time) {
        boolean res = false;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String set = jedis.set(key, value, NX, PX, time);
            if (StringUtils.isBlank(set)) {
                res = true;
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        } finally {
            returnResource(jedis);
        }
        return res;

    }
    private void returnResource(Jedis jedis) {
        if (jedis != null) {
            jedisPool.returnResource(jedis);
        }
    }

一、redisson的分布式鎖代碼,注解實現(xiàn)

@Around("lockPoint()")
    public Object redisDistributedLock(ProceedingJoinPoint pjp) throws Throwable {
        //獲取Lock注解信息
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        RedissonLock lockInfo = method.getAnnotation(RedissonLock.class);
        String lockKey = lockInfo.key();
        if (StringUtils.isBlank(lockKey)) {
            throw new IllegalArgumentException("配置參數(shù)錯誤,lockKey不能為空!");
        }
        long leaseTime = lockInfo.leaseTime();
        long waitTime = lockInfo.waitTime();
        TimeUnit unit = lockInfo.unit();
        Object obj = null;
        boolean lock = false;
        RLock rLock = redissonClient.getLock(lockKey);
        try {
            //嘗試去上鎖
            if (leaseTime > 0) {
                //設(shè)置過期時間,到期自動釋放的鎖
                lock = rLock.tryLock(waitTime, leaseTime, unit);
            } else {
                //不設(shè)置過期時間,看門狗自動續(xù)租的鎖
                lock = rLock.tryLock(waitTime, unit);
            }
            if (lock && rLock.isHeldByCurrentThread()) {
                logger.info("當前線程得到鎖...");
                obj = pjp.proceed();
            }
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            if (lock && rLock.isHeldByCurrentThread()) {
                //當前線程是否持有此鎖,持有就刪除鎖
                logger.info("釋放鎖...");
                rLock.unlock();

            }
        }
        return obj;
    }

AP模式分布式鎖總結(jié)

以上是我對redis實現(xiàn)的分布式鎖的一些介紹。redis鎖的機制理解起來比較簡單,現(xiàn)有的redisson客戶端可以很好的支持分布式鎖的操作,也基本滿足了分布式鎖的場景需要。redis分布式鎖的最致命的問題就是無法保證數(shù)據(jù)的一致性,如果一旦主節(jié)點宕機,數(shù)據(jù)沒有同步到從節(jié)點中,會出現(xiàn)再次上鎖的問題,如果業(yè)務(wù)一定需要數(shù)據(jù)的一致性在高并發(fā)的場景下是不建議選擇redis鎖的實現(xiàn),可以選擇CP模型的zk或者etcd來實現(xiàn)分布式鎖。以上的例子以及代碼都是基于單機的redis來實現(xiàn)的,如有不足望大家指正。

PS:

在文章的最后為大家推薦一個公眾號《架構(gòu)之美》,上面會不定期的推薦一些技術(shù)文章,希望大家喜歡。

來源:https://www./content-2-711551.html

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多