主頁 > 軟體設計 > R2M分布式鎖原理及實踐

R2M分布式鎖原理及實踐

2023-02-07 14:57:51 軟體設計

作者:京東科技 張石磊

1 案例引入

名詞簡介:

資源:可以理解為一條內容,或者圖+文字+鏈接的載體,

檔位ID: 資源的分類組,資源必須歸屬于檔位,

問題描述:當同一個檔位下2條資源同時審批通過時,收到擎天審批系統2條訊息,消費者應用部署了2臺機器,此時正好由2臺機器分別消費,在并發消費時,先更新資源狀態,然后寫快取,每次取前100條資源,類似select * from resource where gear_id=xxx limit 100 order by id desc;

在寫檔位快取,此時事務未提交,并發查詢時根據檔位Id查詢時查詢不到對方的資料,全量寫快取時導致后寫的快取覆寫了先寫的快取,即快取被覆寫,導致投放資源缺失,

方案思考 :

方案1:一臺機器消費mq–單點問題

方案2:將同檔位ID的資源路由到同一個queue,需要審批系統配合根據檔位Id做路由,審批系統發的訊息不只是cms審批資料,此方案不適用,

方案3:在檔位級別加分布式鎖,

經比較,最終采用方案3是合適的方案.

2 鎖說明和分布式鎖選擇

synchronized鎖的粒度是JVM行程維度,集群模式下,不能對共享資源加鎖,此時需要跨JVM行程的分布式鎖,

分布式鎖方式核心實作方式優點缺點分析

1 資料庫:

悲觀鎖,lock

樂觀鎖,通過版本號實作version

實作簡單,不依賴中間件

資料庫IO瓶頸,性能差

單實體存在單點問題,主從架構存在資料不一致,主從切換時其他客戶端可重復加鎖,

2 zookeeper

創建臨時節點

CP模型,可靠性高,不存在主從切換不一致問題

頻繁創建和銷毀臨時節點,且

集群模式下,leader資料需要同步到follower才算加鎖成功,性能不如redis

主從切換服務不可用

3 redis集群

setnx+expire

性能高

有封裝好的框架redission

支持超時自動洗掉鎖

集群支持高可用,AP模型

主從切換時其他客戶端可重復加鎖,

R2M是基于開源的Redis cluster(Redis 3.0以上版本)構建的高性能分布式快取系統,我們系統一直在使用,3.2.5版本開始支持分布式鎖,

3 r2m分布式鎖原理

示例代碼:

String lockKey = CacheKeyHelper.getGearLockKey(EnvEnum.getEnvFlagEnum(envCode),resource.getGearId());

R2mLock lock = (R2mLock) r2mClusterClient.getLock(lockKey);

//獲取鎖,鎖的默認有效期30s,獲取不到鎖就阻塞

lock.lock();

try {

    //業務代碼

    resourceService.afterApprovedHandle(resource);

}  finally {

    //釋放鎖

    lock.unlock();

}



1 加鎖核心流程:

加鎖流程圖:

1):嘗試獲取鎖,通過執行加鎖Lua腳本來做;

2):若第一步未獲取到鎖,則去redis訂閱解鎖訊息

3):一旦持有鎖的執行緒釋放了鎖,就會廣播解鎖訊息,其他執行緒自旋重新嘗試獲取鎖,

核心加鎖原理:使用lua腳本封裝了hset和pexpire命令,保證是一個原子操作, KEYS[1]是加鎖的key,argv[2]是加鎖的客戶端ID(UUID+執行緒ID),ARGV[1]是鎖的有效期,默認30s.

private Object acquireInternal(List<String> args) {

if (!this.setLocked() && this.getHolderId() != Thread.currentThread().getId()) {

return -1L;

} else {

try {

//hash結構,hash的key是加鎖的key,鍵值對的key為客戶端的UUID+執行緒id,value為鎖的重入計數器值,

return this.lockSha() != null ? this.executor.evalR2mLockSha(this.lockSha(),

"if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;

return -2;", Collections.singletonList(this.lockName), args) : this.executor. == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return -2;", Collections.singletonList(this.lockName), args);

} catch (Exception var3) {

this.setUnlocked();

throw new R2mLockException("Failed to acquire lock " + this.lockName + ".", var3);

}

}

}



args引數

private List<String> acquireArgs(long leaseTime) {

        List<String> args = new ArrayList();

        if (leaseTime != -1L) {

            args.add(String.valueOf(leaseTime));

        } else {

             //默認30s

            args.add(String.valueOf(this.internalLockLeaseTime()));

        }

        //UUID+當前執行緒id

        args.add(this.currentThreadLockId(Thread.currentThread().getId()));

        return args;

    }

?



獲取鎖失敗訂閱鎖的channel

//獲取鎖失敗,訂閱釋放鎖的訊息

 private boolean failedAcquire() {

            this.subLock();

            return false;

        }

 

 private void subLock() {

            CompletableFuture<Void> cf = R2mLock.this.executor.lockSubscribe(R2mLock.this.lockPubSub(), R2mLock.this.getLockChannelName(), R2mLock.this);

            if (cf != null) {

                cf.handleAsync(this::reSubIfEx);

            }

 

        }



鎖釋放后,訂閱者通過自旋嘗試獲取鎖,

//tryAcquire獲取鎖,!tryAcquire就是獲取鎖失敗,鎖釋放后,通知執行緒喚醒后回傳false,然后通過自旋,嘗試獲取鎖,

public final void acquire(long arg) {

        if (!tryAcquire(arg) &&

            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

            selfInterrupt();

    }

 

 final boolean acquireQueued(final Node node, long arg) {

        boolean failed = true;

        try {

            boolean interrupted = false;

            //內部自旋獲取鎖

            for (;;) {

                final Node p = node.predecessor();

                if (p == head && tryAcquire(arg)) {

                    setHead(node);

                    p.next = null; // help GC

                    failed = false;

                    return interrupted;

                }

                if (shouldParkAfterFailedAcquire(p, node) &&

                    parkAndCheckInterrupt())

                    interrupted = true;

            }

        } finally {

            if (failed)

                cancelAcquire(node);

        }

    }



2 釋放鎖核心邏輯:

1)洗掉分布式鎖key(如果可重入鎖計數為0)

  1. 發釋放鎖的廣播訊息

3)取消watchdog

private Object unlockInternal(List<String> args) {

        logger.debug("{} trying to unlock.", Thread.currentThread().getId());

 

        Object var2;

        try {

     //判斷鎖 key 是否存在,如果存在,然后遞減hash的value值,當value值為0時再洗掉鎖key,并且廣播釋放鎖的訊息

            if (this.unlockSha() == null) {

                var2 = this.executor. == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.lockName, this.getLockChannelName()), args);

                return var2;

            }

 

            var2 = this.executor.evalR2mLockSha(this.unlockSha(), "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.lockName, this.getLockChannelName()), args);

        } catch (Exception var6) {

            throw new R2mLockException("Failed to unlock " + this.lockName + ".", var6);

        } finally {

            this.finalizeRelease();

        }

 

        return var2;

    }

 

//取消當前執行緒的watchdog

 private void finalizeRelease() {

        long threadId = Thread.currentThread().getId();

        R2mLock.ExpirableEntry entry = (R2mLock.ExpirableEntry)this.entryCache.get(threadId);

        if (entry != null) {

            entry.release(threadId);

            if (entry.isReleased()) {

                //取消這個執行緒watchdog定時任務

                entry.getExtTask().cancel();

                this.expEntry.compareAndSet(entry, (Object)null);

                //從快取watchdog執行緒的map中洗掉該執行緒

                this.entryCache.remove(threadId);

            }

        }

 

    }



3 鎖的健壯性思考

1 業務沒執行完,鎖超時過期怎么辦?

客戶端加鎖默認有效期30s,超過有效期后如果業務沒執行完,還需要持有這把鎖,r2m客戶端提供了續期機制,也就是watchdog機制,

watchdog原理:客戶端執行緒維度(UUID+執行緒ID,客戶端維護一個MAP,key就是UUID+執行緒ID)的后臺定時執行緒,獲取鎖成功后,如果客戶端還持有當前鎖,每隔10s(this.internalLockLeaseTime() / 3L),去延長鎖的有效期(internalLockLeaseTime)

//watchdog核心機制 ,internalLockLeaseTime默認30s

private void extendLock(long holderId) {

        if (this.expEntry.get() != null) {

            R2mLock.ExpirableEntry holderEntry = (R2mLock.ExpirableEntry)this.entryCache.get(holderId);

            if (holderEntry != null) {

                 //每隔10s,如果當前執行緒持有鎖,則續期30s

                if (this.expEntry.compareAndSet(holderEntry, holderEntry)) {

                    Timeout task = this.timer().newTimeout((timeout) -> {

                        if (this.extendLockInternal(holderId)) {

                            this.extendLock(holderId);

                        }

 

                    }, this.internalLockLeaseTime() / 3L, TimeUnit.MILLISECONDS);

                    if (this.expEntry.get() != null) {

                        ((R2mLock.ExpirableEntry)this.expEntry.get()).setExtTask(task);

                    }

                }

 

            }

        }

    }

 

 //執行續期lua腳本

 private boolean extendLockInternal(long threadId) {

        Object result;

        try {

           //只續期

            if (this.extendLockSha() != null) {

                result = this.executor.evalR2mLockSha(this.extendLockSha(), "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.lockName), this.extendLockArgs(threadId));

            } else {

                result = this.executor. == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.lockName), this.extendLockArgs(threadId));

            }

        } catch (Exception var5) {

            return false;

        }

 

        return Long.parseLong(result.toString()) == 1L;

    }



2 客戶端宕機,鎖如何釋放?

分布式鎖是有效期的,客戶端宕機后,watchdog機制失效,鎖過期自動失效,

3 redis分布式鎖集群模式下缺陷

r2m集群模式,極端情況,master加鎖成功,宕機,還未來得及同步到slave,主從切換,slave切換成master,可以繼續加鎖,對于非及其嚴格加鎖場景,該方案可滿足,屬于AP;對于嚴格場景下的分布式鎖,可采用基于zookeeper的分布式鎖,屬于CP,leader宕機,folllower選舉時不可用,性能上redis更優,

4 鎖的釋放問題

注意鎖的釋放在finally中釋放,必須由鎖的持有者釋放,不能由其他執行緒釋放別人的鎖,示例代碼中lock放到try的外面,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/543203.html

標籤:架構設計

上一篇:構建億級別的訊息推送基礎模型

下一篇:返回列表

標籤雲
其他(152429) Python(37775) JavaScript(25183) Java(17368) C(15188) 區塊鏈(8246) C#(7972) AI(7469) 爪哇(7425) MySQL(6944) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5847) 数组(5741) R(5405) Linux(5296) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4499) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2421) ASP.NET(2399) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) Web開發(1951) python-3.x(1918) .NET技术(1916) 弹簧靴(1913) xml(1889) PostgreSQL(1861) C++(1852) 谷歌表格(1846) Unity3D(1843) for循环(1842) HtmlCss(1835) .NETCore(1834)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • R2M分布式鎖原理及實踐

    R2M分布式鎖原理可以理解為一條內容或者圖片+文字+鏈接的載體,常見的案例有鎖說明和分布式鎖選擇、r2m分布式鎖選擇、r2m分布式鎖原理,加鎖核心流程。 ......

    uj5u.com 2023-02-07 14:57:51 more
  • 構建億級別的訊息推送基礎模型

    問題場景:當用戶量增大的時候,系統內的訊息推送(比如 系統內短信推送,微信,釘釘,極光個推推送)將是我們常見業務場景。當系統中的用戶量逐漸增大時候,群發用戶訊息的推送也將成為系統中致命的性能瓶頸。 訊息推送緩沖佇列模型 意義: 構建大批量訊息推送佇列,來解決大批量資料通過拉長時間佇列模式來換取性能提 ......

    uj5u.com 2023-02-07 12:48:07 more
  • 構建億級別的訊息推送基礎模型

    問題場景:當用戶量增大的時候,系統內的訊息推送(比如 系統內短信推送,微信,釘釘,極光個推推送)將是我們常見業務場景。當系統中的用戶量逐漸增大時候,群發用戶訊息的推送也將成為系統中致命的性能瓶頸。 訊息推送緩沖佇列模型 意義: 構建大批量訊息推送佇列,來解決大批量資料通過拉長時間佇列模式來換取性能提 ......

    uj5u.com 2023-02-07 12:32:16 more
  • 一篇文章帶你了解設計模式——行為型模式

    一篇文章帶你了解設計模式——行為型模式 在之前的文章我們已經介紹了設計模式中的創建者模式和結構型模式,下面我們來介紹最后一部分行為型模式 行為型模式用于描述程式在運行時復雜的流程控制,即描述多個類或物件之間怎樣相互協作共同完成單個物件都無法單獨完成的任務 行為型模式分為類行為模式和物件行為模式,前者 ......

    uj5u.com 2023-02-05 07:06:51 more
  • 一篇文章帶你了解設計模式——行為型模式

    一篇文章帶你了解設計模式——行為型模式 在之前的文章我們已經介紹了設計模式中的創建者模式和結構型模式,下面我們來介紹最后一部分行為型模式 行為型模式用于描述程式在運行時復雜的流程控制,即描述多個類或物件之間怎樣相互協作共同完成單個物件都無法單獨完成的任務 行為型模式分為類行為模式和物件行為模式,前者 ......

    uj5u.com 2023-02-05 07:01:06 more
  • keycloak~JWT各欄位說明及擴展欄位的方法

    jwt擴展欄位介紹 為了實作業務的定制化需求和開發人員使用的便利,對kc的JWT進行了擴展,這個擴展欄位在決議JWT之后都可以看到。 jwt的payload { "exp": 1675329802, "iat": 1675329622, "jti": "2a80e925-b9ce-464f-822d ......

    uj5u.com 2023-02-04 07:28:02 more
  • 《領域驅動設計》:從領域視角深入倉儲(Repository)的設計和實作

    “ DDD設計的目標是關注領域模型而并非技術來創建更好的軟體,假設開發人員構建了一個SQL,并將它傳遞給基礎設施層中的某個查詢服務然后根據表資料的結構集取出所需資訊,最后將這些資訊提供給建構式或者Factory,開發人員在做這一切的時候早已不把模型看做重點了,這個整個程序就變成了資料處理的風格 ”... ......

    uj5u.com 2023-02-04 07:27:37 more
  • 函式式編程思維讀后總結與感想

    1. 基本資訊 函式式編程思維 Functional Thinking [美] 福特(Neal Ford) 著,郭曉剛 譯 人民郵電出版社,2015年8月出版 1.1. 讀薄率 書籍總字數242千字,筆記總字數9983字。 讀薄率9983÷242000≈4.13% 統計口徑恢復到著作權頁書籍總字數,讀 ......

    uj5u.com 2023-02-04 07:27:17 more
  • 函式式編程思維讀后總結與感想

    1. 基本資訊 函式式編程思維 Functional Thinking [美] 福特(Neal Ford) 著,郭曉剛 譯 人民郵電出版社,2015年8月出版 1.1. 讀薄率 書籍總字數242千字,筆記總字數9983字。 讀薄率9983÷242000≈4.13% 統計口徑恢復到著作權頁書籍總字數,讀 ......

    uj5u.com 2023-02-04 07:26:58 more
  • 《領域驅動設計》:從領域視角深入倉儲(Repository)的設計和實作

    “ DDD設計的目標是關注領域模型而并非技術來創建更好的軟體,假設開發人員構建了一個SQL,并將它傳遞給基礎設施層中的某個查詢服務然后根據表資料的結構集取出所需資訊,最后將這些資訊提供給建構式或者Factory,開發人員在做這一切的時候早已不把模型看做重點了,這個整個程序就變成了資料處理的風格 ”... ......

    uj5u.com 2023-02-04 07:26:25 more