分布式鎖續(xù)約問題-續(xù)

萌芽

對于分布式鎖骗露,有一個不錯的客戶端Redisson荡碾,是會自動鎖續(xù)約的荡澎,詳情請自行搜索
但均践,對于這個客戶端的使用方式,個人不是很喜歡摩幔,還是更傾向Lettuce

對于鎖續(xù)約原理 不復雜(參考Redisson)彤委,無非就是用個WatchDog線程,時不時對比一下鎖的超時時間還剩余多少或衡,如果小于某個值就續(xù)約(預設時間30s,已經(jīng)過去20s了焦影,就刷新超時時間)

分析源碼

這里使用的是sping integration包里的分布式鎖(理解成官方),詳情請查閱
我們的目標是獲取存在的分布式鎖封断,通過這些鎖的key進行續(xù)約
代碼片段如下(精簡了部分內(nèi)容)

public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {
    private static final String OBTAIN_LOCK_SCRIPT =
            "local lockClientId = redis.call('GET', KEYS[1])\n" +
                    "if lockClientId == ARGV[1] then\n" +
                    "  redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
                    "  return true\n" +
                    "elseif not lockClientId then\n" +
                    "  redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
                    "  return true\n" +
                    "end\n" +
                    "return false";
    private final Map<String, RedisLock> locks = new ConcurrentHashMap<>();

這里我截取了兩個關鍵部分

  • 申請鎖的指令
  • 鎖的存放位置(私有Map)

那么目標就很明確了

第一個小目標

  • 續(xù)約指令
    分析申請鎖指令可得
1.如果當前發(fā)指令的clientId 就是鎖的持有者斯辰,則續(xù)約
2.如果當前clientId不存在,則鎖上
3.否則鎖失敗

這也是為啥在分布式鎖續(xù)約問題坡疼,我可以使用重入鎖的的方式續(xù)約的原因
那么問題來了彬呻,為啥這里不能直接重入鎖去刷新呢?
因為重入鎖是需要業(yè)務代碼上調(diào)用的,這樣對業(yè)務代碼侵入性太強了闸氮,而WatchDog調(diào)用剪况,本身在不同線程上,就連本地鎖都沒辦法重入(為了性能,分布式鎖也維持了一把本地鎖)

清楚指令意思之后就好辦了蒲跨,編寫續(xù)約代碼

private static final String OBTAIN_LOCK_SCRIPT_VERSION = "aa1fc9ae99657e86372b45452e5d6f71";

private static final String RENEW_LOCK_SCRIPT =
            "local lockClientId = redis.call('GET', KEYS[1])\n" +
                    "if lockClientId == ARGV[1] then\n" +
                    "  redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
                    "  return true\n" +
                    "end\n" +
                    "return false";

代碼意思也是顯然而已的
1.鎖是自己的译断,續(xù)約
2.否則,失敾虮(業(yè)務代碼完成了孙咪,主動釋放鎖了)

當然,為了保險起見巡语,這邊把申請鎖的代碼md5了翎蹈,作為分布式鎖的版本,用來判斷當前的插件代碼是否還合適
  • 侵入RedisLockRegistry捌臊,獲取locks
    這里沒什么好說杨蛋,直接反射,強制讀取就行了(雖然官方已經(jīng)不建議這樣用理澎,但是現(xiàn)在太多太多框架都這樣做了,現(xiàn)在還沒強制曙寡,不是么糠爬?)

完整代碼

上面的是廢話,看代碼就好了

/**
 * (why) 提供【自動續(xù)約】功能
 * (what)本類以【暴力】鎖進行續(xù)約
 * (how)自動初始化举庶,并以插件方式運行
 *
 * @Todo 若 RedisLockRegistry 提供續(xù)約功能执隧,應使用官方功能
 * @author Wind
 */
@Slf4j
public class LockWatchdog {

    /**
     * 這個是RedisLockRegistry的script, 用于確認版本是否正確
     */
    private static final String OBTAIN_LOCK_SCRIPT_VERSION = "aa1fc9ae99657e86372b45452e5d6f71";

    /**
     * renew鎖使用
     * 和 OBTAIN_LOCK_SCRIPT 最大的區(qū)別就是如果lockClientId不存在,不會創(chuàng)建一條
     */
    private static final String RENEW_LOCK_SCRIPT =
            "local lockClientId = redis.call('GET', KEYS[1])\n" +
                    "if lockClientId == ARGV[1] then\n" +
                    "  redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
                    "  return true\n" +
                    "end\n" +
                    "return false";

    /**
     * RedisLockRegistry(static).FIELDS
     */
    private static final String FIELD_OBTAIN_LOCK_SCRIPT = "OBTAIN_LOCK_SCRIPT";

    /**
     * redisLockRegistry(object).FIELDS
     */
    private static final String FIELD_CLIENT_ID = "clientId";
    private static final String FIELD_EXPIRE_AFTER = "expireAfter";
    private static final String FIELD_LOCKS = "locks";

    /**
     * redisLock(object).FIELDS
     */
    private static final String FIELD_LOCK_KEY = "lockKey";
    private static final String FIELD_LOCKED_AT = "lockedAt";

    private final RedisLockRegistry lockRegistry;
    private final StringRedisTemplate redisTemplate;
    private final RedisScript<Boolean> renewLockScript;
    private final String clientId;
    private final long expireAfter;

    public LockWatchdog(RedisLockRegistry lockRegistry, StringRedisTemplate redisTemplate) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //依賴
        this.lockRegistry = lockRegistry;
        this.redisTemplate = redisTemplate;

        //刷新腳本
        this.renewLockScript = new DefaultRedisScript<>(RENEW_LOCK_SCRIPT, Boolean.class);

        //代理類的參數(shù)
        this.clientId = (String) UnsafeBeanUtils.getProperty(lockRegistry, FIELD_CLIENT_ID);
        Assert.hasText(this.clientId, "client id is required!");
        this.expireAfter = (Long) UnsafeBeanUtils.getProperty(lockRegistry, FIELD_EXPIRE_AFTER);
        Assert.notNull(this.expireAfter, "expire after is required!");
        Assert.isTrue(this.expireAfter > 0, "expire after <= 0");

        //check version
        String script = (String) UnsafeBeanUtils.getProperty(RedisLockRegistry.class, FIELD_OBTAIN_LOCK_SCRIPT);
        Assert.isTrue(CipherUtils.md5(script).equalsIgnoreCase(OBTAIN_LOCK_SCRIPT_VERSION),"verion error");
        log.info("init success clientId {}, expireAfter {}", clientId, expireAfter);
    }

    /**
     * 續(xù)約鎖
     * 續(xù)約成功后會修改lockedAt字段,避免鎖被超時回收了
     *
     * @param redisLock
     * @return
     * @throws IllegalAccessException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     */
    private boolean renewLock(Object redisLock) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        // lock key 和 map key不同的户侥,需要重新獲取
        String lockKey = (String) UnsafeBeanUtils.getProperty(redisLock, FIELD_LOCK_KEY);
        if(log.isDebugEnabled()) {
            log.debug("LockWatchdog:try to renew {}", lockKey);
        }
        Boolean success =
                redisTemplate.execute(renewLockScript,
                        Collections.singletonList(lockKey), clientId,
                        String.valueOf(expireAfter));

        boolean result = Boolean.TRUE.equals(success);
        if (result) {
            UnsafeBeanUtils.setProperty(redisLock, FIELD_LOCKED_AT, System.currentTimeMillis());
            if(log.isDebugEnabled()) {
                log.debug("LockWatchdog:{} renew success!", lockKey);
            }
        } else {
            if(log.isDebugEnabled()) {
                log.debug("LockWatchdog:{} renew fail!", lockKey);
            }
        }

        return result;
    }

    /**
     * 定時器(10s執(zhí)行一次續(xù)約)
     * 這里直接獲取到Map里的內(nèi)容嘗試續(xù)約
     * Map里的鎖是會自動刪除的(ExpirableLockRegistry)
     * 而且使用分布式鎖的場景不算多镀琉,所以已經(jīng)解鎖的,也去嘗試續(xù)約也是沒問題的
     *
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    @Scheduled(cron = "*/10 * * * * ?")
    private void scheduled() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        if(log.isDebugEnabled()){
            log.debug("LockWatchdog:renew lock");
        }
        Map<String, Lock> locks = (Map<String, Lock>) UnsafeBeanUtils.getProperty(lockRegistry, FIELD_LOCKS);
        if(log.isDebugEnabled()) {
            log.debug("LockWatchdog:locks {}", locks.size());
        }
        if (MapUtils.isNotEmpty(locks)) {
            Iterator<Map.Entry<String, Lock>> iter = locks.entrySet().iterator();
            Map.Entry<String, Lock> entry = null;
            while (iter.hasNext()) {
                entry = iter.next();
                renewLock(entry.getValue());
            }
        } else {
            if(log.isDebugEnabled()) {
                log.debug("LockWatchdog:not need to renew!");
            }
        }
        if(log.isDebugEnabled()) {
            log.debug("LockWatchdog:renew lock finish!");
        }
    }

    /**
     * 不安全的類操作
     */
    private final static class UnsafeBeanUtils {
        public static Object getProperty(final Class clazz, final String name) throws IllegalAccessException {
            Field field = FieldUtils.getDeclaredField(clazz, name, true);
            return field.get(clazz);
        }

        public static Object getProperty(final Object bean, final String name) throws IllegalAccessException {
            Field field = FieldUtils.getDeclaredField(bean.getClass(), name, true);
            return field.get(bean);
        }

        public static void setProperty(final Object bean, final String name, final Object value) throws IllegalAccessException {
            Field field = FieldUtils.getDeclaredField(bean.getClass(), name, true);
            field.set(bean, value);
        }

    }
}

代碼說明

  • 使用了Scheduled 做定時任務蕊唐,并且10s續(xù)約一次
    對屋摔,沒判斷當前鎖時間,直接10s一次替梨,本身就是臨時解決方案钓试,能達到目的就好了,系統(tǒng)設置了30s的鎖時間副瀑,也就是沒10s自動把超時時間重置為30s弓熏,注意,是重置糠睡,并不是延長挽鞠,所以多次調(diào)用效果是沒太大差別的,并且業(yè)務完成后主動釋放鎖是好習慣,再次信认,一個應用這種排他鎖也就10把8把串稀,多調(diào)用幾次(續(xù)約)影響忽略不計
    有興趣可以優(yōu)化(按Redisson的判斷下鎖定時間,超時時間什么的)
  • 對于已經(jīng)解鎖的分布式鎖狮杨,還會存在map里的母截,所以代碼會出現(xiàn)續(xù)約失敗的情況
    看代碼是會一段時間清理的,目前看也可以在續(xù)約之前試試trylock橄教,成功了 說明對方已經(jīng)解鎖了清寇,不用續(xù)約了,但是成功了還需要解鎖护蝶,多次調(diào)用redis华烟,還不如直接一開始就嘗試續(xù)約好了,等spring自己清理(這邊就自然不會續(xù)了)
  • 臨時方案持灰,臨時方案盔夜,臨時方案,如果官方支持自動續(xù)約了堤魁,就用官方的好了
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喂链,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子妥泉,更是在濱河造成了極大的恐慌椭微,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盲链,死亡現(xiàn)場離奇詭異蝇率,居然都是意外死亡,警方通過查閱死者的電腦和手機刽沾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門本慕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人侧漓,你說我怎么就攤上這事锅尘。” “怎么了火架?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵鉴象,是天一觀的道長。 經(jīng)常有香客問我何鸡,道長纺弊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任骡男,我火速辦了婚禮淆游,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己犹菱,他們只是感情好拾稳,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腊脱,像睡著了一般访得。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上陕凹,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天悍抑,我揣著相機與錄音,去河邊找鬼杜耙。 笑死搜骡,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的佑女。 我是一名探鬼主播记靡,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼团驱!你這毒婦竟也來了摸吠?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤店茶,失蹤者是張志新(化名)和其女友劉穎蜕便,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贩幻,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年两嘴,在試婚紗的時候發(fā)現(xiàn)自己被綠了丛楚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡憔辫,死狀恐怖趣些,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贰您,我是刑警寧澤坏平,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站锦亦,受9級特大地震影響舶替,放射性物質發(fā)生泄漏。R本人自食惡果不足惜杠园,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一顾瞪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦陈醒、人聲如沸惕橙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弥鹦。三九已至,卻和暖如春爷辙,著一層夾襖步出監(jiān)牢的瞬間彬坏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工犬钢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苍鲜,地道東北人。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓玷犹,卻偏偏與公主長得像混滔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子歹颓,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Redis的分布式鎖實現(xiàn)原理網(wǎng)上已經(jīng)有很多文章了坯屿,這里主要記錄一下續(xù)約問題。 一般情況下巍扛,分布式鎖是直接申請固定時...
    Wind哥閱讀 1,064評論 0 1
  • 基于單Redis節(jié)點的分布式鎖 組件依賴 首先我們要通過Maven引入Jedis開源組件领跛,在pom.xml文件加入...
    GeekerLou閱讀 813評論 0 7
  • 目前越來越多的應用使用負載均衡,以往傳統(tǒng)單體應用單機部署的情況下使用的JAVA并發(fā)處理資源競爭方式(J.U.C或s...
    DawnOfTan閱讀 798評論 0 2
  • [TOC] 前言 突然覺得想要安穩(wěn)的度過一生簡直可以稱之為臆想撤奸,想想歷史上的盛世吠昭,大都不過三四十年,如何能保證自己...
    程柏閱讀 420評論 1 0
  • 漸變的面目拼圖要我怎么拼蒲肋? 我是疲乏了還是投降了? 不是不允許自己墜落钝满, 我沒有滴水不進的保護膜兜粘。 就是害怕變得面...
    悶熱當乘涼閱讀 4,238評論 0 13