<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          高并發(fā)場(chǎng)景:紅包雨背后的設(shè)計(jì)與實(shí)現(xiàn)

          共 15647字,需瀏覽 32分鐘

           ·

          2022-07-01 02:13


          聊聊紅包雨背后的設(shè)計(jì)與實(shí)現(xiàn),全文很干,耐心看完

          2018年,王思聰?shù)?strong style="color: #222222;font-weight: bold;letter-spacing: 0.5444px;">沖頂大會(huì),西瓜視頻的百萬英雄,再到映客的芝士超人,直播答題火爆全網(wǎng)。

          我服務(wù)的一家電商公司也加入了這次熱潮,技術(shù)團(tuán)隊(duì)研發(fā)了直播答題功能。答題結(jié)束之后,紅包會(huì)以紅包雨的形式落下,用戶點(diǎn)擊屏幕上落下的紅包,若搶到紅包,紅包會(huì)以現(xiàn)金的形式進(jìn)入用戶賬戶。

          紅包雨是一個(gè)典型的高并發(fā)場(chǎng)景,短時(shí)間內(nèi)有海量請(qǐng)求訪問服務(wù)端,技術(shù)團(tuán)隊(duì)為了讓系統(tǒng)運(yùn)行順暢,搶紅包采用了基于 Redis + Lua 腳本的設(shè)計(jì)方案。

          1 整體流程

          我們分析下?lián)尲t包的整體流程 :

          1. 運(yùn)營(yíng)系統(tǒng)配置紅包雨活動(dòng)總金額以及紅包個(gè)數(shù),提前計(jì)算出各個(gè)紅包的金額并存儲(chǔ)到 Redis 中;
          2. 搶紅包雨界面,用戶點(diǎn)擊屏幕上落下的紅包,發(fā)起搶紅包請(qǐng)求;
          3. TCP 網(wǎng)關(guān)接收搶紅包請(qǐng)求后,調(diào)用答題系統(tǒng)搶紅包 dubbo 服務(wù),搶紅包服務(wù)本質(zhì)上就是執(zhí)行 Lua 腳本,將結(jié)果通過 TCP 網(wǎng)關(guān)返回給前端;
          4. 用戶若搶到紅包,異步任務(wù)會(huì)從 Redis 中 獲取搶得的紅包信息,調(diào)用余額系統(tǒng),將金額返回到用戶賬戶。

          2 紅包 Redis 設(shè)計(jì)

          搶紅包有如下規(guī)則:

          • 同一活動(dòng),用戶只能搶紅包一次 ;
          • 紅包數(shù)量有限,一個(gè)紅包只能被一個(gè)用戶搶到。

          如下圖,我們?cè)O(shè)計(jì)三種數(shù)據(jù)類型:

          1. 運(yùn)營(yíng)預(yù)分配紅包列表 ;

          隊(duì)列元素 json 數(shù)據(jù)格式 :

          {
              //紅包編號(hào)
              redPacketId : '365628617880842241
              //紅包金額
              amount : '12.21'          
          }
          1. 用戶紅包領(lǐng)取記錄列表;

          隊(duì)列元素 json 數(shù)據(jù)格式:

          {
              //紅包編號(hào)
              redPacketId : '365628617880842241'
              //紅包金額
              amount : '12.21',
              //用戶編號(hào)
              userId : '265628617882842248'
          }
          1. 用戶紅包防重 Hash 表;

          搶紅包 Redis 操作流程 :

          1. 通過 hexist 命令判斷紅包領(lǐng)取記錄防重 Hash 表中用戶是否領(lǐng)取過紅包 ,若用戶未領(lǐng)取過紅包,流程繼續(xù);
          2. 從運(yùn)營(yíng)預(yù)分配紅包列表 rpop 出一條紅包數(shù)據(jù) ;
          3. 操作紅包領(lǐng)取記錄防重 Hash 表 ,調(diào)用 HSET 命令存儲(chǔ)用戶領(lǐng)取記錄;
          4. 將紅包領(lǐng)取信息 lpush 進(jìn)入用戶紅包領(lǐng)取記錄列表。

          搶紅包的過程 ,需要重點(diǎn)關(guān)注如下幾點(diǎn) :

          • 執(zhí)行多個(gè)命令,是否可以保證原子性 ,  若一個(gè)命令執(zhí)行失敗,是否可以回滾;
          • 在執(zhí)行過程中,高并發(fā)場(chǎng)景下,是否可以保持隔離性;
          • 后面的步驟依賴前面步驟的結(jié)果。

          Redis 支持兩種模式 :  事務(wù)模式Lua 腳本,接下來,我們一一展開。

          3 事務(wù)原理

          Redis 的事務(wù)包含如下命令:

          序號(hào)命令及描述
          1MULTI 標(biāo)記一個(gè)事務(wù)塊的開始。
          2EXEC 執(zhí)行所有事務(wù)塊內(nèi)的命令。
          3DISCARD 取消事務(wù),放棄執(zhí)行事務(wù)塊內(nèi)的所有命令。
          4WATCH key [key ...] 監(jiān)視一個(gè)(或多個(gè)) key ,如果在事務(wù)執(zhí)行之前這個(gè)(或這些) key 被其他命令所改動(dòng),那么事務(wù)將被打斷。
          5UNWATCH 取消 WATCH 命令對(duì)所有 key 的監(jiān)視。

          事務(wù)包含三個(gè)階段:

          1. 事務(wù)開啟,使用 MULTI , 該命令標(biāo)志著執(zhí)行該命令的客戶端從非事務(wù)狀態(tài)切換至事務(wù)狀態(tài) ;
          2. 命令入隊(duì),MULTI 開啟事務(wù)之后,客戶端的命令并不會(huì)被立即執(zhí)行,而是放入一個(gè)事務(wù)隊(duì)列 ;
          3. 執(zhí)行事務(wù)或者丟棄。如果收到 EXEC 的命令,事務(wù)隊(duì)列里的命令將會(huì)被執(zhí)行 ,如果是 DISCARD 則事務(wù)被丟棄。

          下面展示一個(gè)事務(wù)的例子。

          redis> MULTI 
          OK
          redis> SET msg "hello world"
          QUEUED
          redis> GET msg
          QUEUED
          redis> EXEC
          1) OK
          1) hello world

          這里有一個(gè)疑問?在開啟事務(wù)的時(shí)候,Redis key 可以被修改嗎?

          在事務(wù)執(zhí)行 EXEC 命令之前 ,Redis key 依然可以被修改

          在事務(wù)開啟之前,我們可以 watch 命令監(jiān)聽 Redis key 。在事務(wù)執(zhí)行之前,我們修改 key 值 ,事務(wù)執(zhí)行失敗,返回 nil

          通過上面的例子,watch 命令可以實(shí)現(xiàn)類似樂觀鎖的效果

          4 事務(wù)的ACID

          4.1 原子性

          原子性是指:一個(gè)事務(wù)中的所有操作,或者全部完成,或者全部不完成,不會(huì)結(jié)束在中間某個(gè)環(huán)節(jié)。事務(wù)在執(zhí)行過程中發(fā)生錯(cuò)誤,會(huì)被回滾到事務(wù)開始前的狀態(tài),就像這個(gè)事務(wù)從來沒有執(zhí)行過一樣。

          第一個(gè)例子:

          在執(zhí)行 EXEC 命令前,客戶端發(fā)送的操作命令錯(cuò)誤,比如:語法錯(cuò)誤或者使用了不存在的命令。

          redis> MULTI
          OK
          redis> SET msg "other msg"
          QUEUED
          redis> wrongcommand  ### 故意寫錯(cuò)誤的命令
          (error) ERR unknown command 'wrongcommand' 
          redis> EXEC
          (error) EXECABORT Transaction discarded because of previous errors.
          redis> GET msg
          "hello world"

          在這個(gè)例子中,我們使用了不存在的命令,導(dǎo)致入隊(duì)失敗,整個(gè)事務(wù)都將無法執(zhí)行 。

          第二個(gè)例子:

          事務(wù)操作入隊(duì)時(shí),命令和操作的數(shù)據(jù)類型不匹配 ,入隊(duì)列正常,但執(zhí)行 EXEC 命令異常 。

          redis> MULTI  
          OK
          redis> SET msg "other msg"
          QUEUED
          redis> SET mystring "I am a string"
          QUEUED
          redis> HMSET mystring name  "test"
          QUEUED
          redis> SET msg "after"
          QUEUED
          redis> EXEC
          1) OK
          2) OK
          3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
          4) OK
          redis> GET msg
          "after"

          這個(gè)例子里,Redis 在執(zhí)行 EXEC 命令時(shí),如果出現(xiàn)了錯(cuò)誤,Redis 不會(huì)終止其它命令的執(zhí)行,事務(wù)也不會(huì)因?yàn)槟硞€(gè)命令執(zhí)行失敗而回滾 。

          綜上,我對(duì) Redis 事務(wù)原子性的理解如下:

          1. 命令入隊(duì)時(shí)報(bào)錯(cuò), 會(huì)放棄事務(wù)執(zhí)行,保證原子性;

          2. 命令入隊(duì)時(shí)正常,執(zhí)行 EXEC 命令后報(bào)錯(cuò),不保證原子性;

          也就是:Redis 事務(wù)在特定條件下,才具備一定的原子性

          4.2 隔離性

          數(shù)據(jù)庫(kù)的隔離性是指:數(shù)據(jù)庫(kù)允許多個(gè)并發(fā)事務(wù)同時(shí)對(duì)其數(shù)據(jù)進(jìn)行讀寫和修改的能力,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致。

          事務(wù)隔離分為不同級(jí)別 ,分別是:

          • 未提交讀(read uncommitted)
          • 提交讀(read committed)
          • 可重復(fù)讀(repeatable read)
          • 串行化(serializable)

          首先,需要明確一點(diǎn):Redis 并沒有事務(wù)隔離級(jí)別的概念。這里我們討論 Redis 的隔離性是指:并發(fā)場(chǎng)景下,事務(wù)之間是否可以做到互不干擾

          我們可以將事務(wù)執(zhí)行可以分為 EXEC 命令執(zhí)行前EXEC 命令執(zhí)行后兩個(gè)階段,分開討論。

          1. EXEC 命令執(zhí)行前

          在事務(wù)原理這一小節(jié),我們發(fā)現(xiàn)在事務(wù)執(zhí)行之前 ,Redis key 依然可以被修改。此時(shí),可以使用 WATCH 機(jī)制來實(shí)現(xiàn)樂觀鎖的效果。

          1. EXEC 命令執(zhí)行后

          因?yàn)?Redis 是單線程執(zhí)行操作命令, EXEC 命令執(zhí)行后,Redis 會(huì)保證命令隊(duì)列中的所有命令執(zhí)行完 。 這樣就可以保證事務(wù)的隔離性。

          4.3 持久性

          數(shù)據(jù)庫(kù)的持久性是指 :事務(wù)處理結(jié)束后,對(duì)數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會(huì)丟失。

          Redis 的數(shù)據(jù)是否持久化取決于 Redis 的持久化配置模式 。

          1. 沒有配置 RDB 或者 AOF ,事務(wù)的持久性無法保證;
          2. 使用了 RDB模式,在一個(gè)事務(wù)執(zhí)行后,下一次的 RDB 快照還未執(zhí)行前,如果發(fā)生了實(shí)例宕機(jī),事務(wù)的持久性同樣無法保證;
          3. 使用了 AOF 模式;AOF 模式的三種配置選項(xiàng) no 、everysec 都會(huì)存在數(shù)據(jù)丟失的情況  。always 可以保證事務(wù)的持久性,但因?yàn)樾阅芴睿谏a(chǎn)環(huán)境一般不推薦使用。

          綜上,redis 事務(wù)的持久性是無法保證的

          4.4 一致性

          一致性的概念一直很讓人困惑,在我搜尋的資料里,有兩類不同的定義。

          1. 維基百科

          我們先看下維基百科上一致性的定義:

          Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof. This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct. Referential integrity guarantees the primary key – foreign key relationship.

          在這段文字里,一致性的核心是“約束”,“any data written to the database must be valid according to all defined rules ”。

          如何理解約束?這里引用知乎問題 如何理解數(shù)據(jù)庫(kù)的內(nèi)部一致性和外部一致性,螞蟻金服 OceanBase 研發(fā)專家韓富晟回答的一段話:

          “約束”由數(shù)據(jù)庫(kù)的使用者告訴數(shù)據(jù)庫(kù),使用者要求數(shù)據(jù)一定符合這樣或者那樣的約束。當(dāng)數(shù)據(jù)發(fā)生修改時(shí),數(shù)據(jù)庫(kù)會(huì)檢查數(shù)據(jù)是否還符合約束條件,如果約束條件不再被滿足,那么修改操作不會(huì)發(fā)生。

          關(guān)系數(shù)據(jù)庫(kù)最常見的兩類約束是“唯一性約束”和“完整性約束”,表格中定義的主鍵和唯一鍵都保證了指定的數(shù)據(jù)項(xiàng)絕不會(huì)出現(xiàn)重復(fù),表格之間定義的參照完整性也保證了同一個(gè)屬性在不同表格中的一致性。

          “ Consistency in ACID ”是如此的好用,以至于已經(jīng)融化在大部分使用者的血液里了,使用者會(huì)在表格設(shè)計(jì)的時(shí)候自覺的加上需要的約束條件,數(shù)據(jù)庫(kù)也會(huì)嚴(yán)格的執(zhí)行這個(gè)約束條件。

          所以事務(wù)的一致性和預(yù)先定義的約束有關(guān),保證了約束即保證了一致性

          我們細(xì)細(xì)品一品這句話: This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct

          寫到這里可能大家還是有點(diǎn)模糊,我們舉經(jīng)典轉(zhuǎn)賬的案例。

          我們開啟一個(gè)事務(wù),張三和李四賬號(hào)上的初始余額都是1000元,并且余額字段沒有任何約束。張三給李四轉(zhuǎn)賬1200元。張三的余額更新為 -200 , 李四的余額更新為2200。

          從應(yīng)用層面來看,這個(gè)事務(wù)明顯不合法,因?yàn)楝F(xiàn)實(shí)場(chǎng)景中,用戶余額不可能小于 0 , 但是它完全遵循數(shù)據(jù)庫(kù)的約束,所以從數(shù)據(jù)庫(kù)層面來看,這個(gè)事務(wù)依然保證了一致性。

          Redis 的事務(wù)一致性是指:Redis 事務(wù)在執(zhí)行過程中符合數(shù)據(jù)庫(kù)的約束,沒有包含非法或者無效的錯(cuò)誤數(shù)據(jù)。

          我們分三種異常場(chǎng)景分別討論:

          1. 執(zhí)行 EXEC 命令前,客戶端發(fā)送的操作命令錯(cuò)誤,事務(wù)終止,數(shù)據(jù)保持一致性;
          2. 執(zhí)行 EXEC 命令后,命令和操作的數(shù)據(jù)類型不匹配,錯(cuò)誤的命令會(huì)報(bào)錯(cuò),但事務(wù)不會(huì)因?yàn)殄e(cuò)誤的命令而終止,而是會(huì)繼續(xù)執(zhí)行。正確的命令正常執(zhí)行,錯(cuò)誤的命令報(bào)錯(cuò),從這個(gè)角度來看,數(shù)據(jù)也可以保持一致性;
          3. 執(zhí)行事務(wù)的過程中,Redis 服務(wù)宕機(jī)。這里需要考慮服務(wù)配置的持久化模式。
            • 無持久化的內(nèi)存模式:服務(wù)重啟之后,數(shù)據(jù)庫(kù)沒有保持?jǐn)?shù)據(jù),因此數(shù)據(jù)都是保持一致性的;
            • RDB / AOF 模式: 服務(wù)重啟后,Redis 通過 RDB / AOF 文件恢復(fù)數(shù)據(jù),數(shù)據(jù)庫(kù)會(huì)還原到一致的狀態(tài)。

          綜上所述,在一致性的核心是約束的語意下,Redis 的事務(wù)可以保證一致性

          1. 《設(shè)計(jì)數(shù)據(jù)密集型應(yīng)用》

          這本書是分布式系統(tǒng)入門的神書。在事務(wù)這一章節(jié)有一段關(guān)于 ACID 的解釋:

          Atomicity, isolation, and durability are properties of the database,whereas consistency (in the ACID sense) is a property of the application. The application may rely on the database’s atomicity and isolation properties in order to achieve consistency, but it’s not up to the database alone. Thus, the letter C doesn’t really belong in ACID.

          原子性,隔離性和持久性是數(shù)據(jù)庫(kù)的屬性,而一致性(在 ACID 意義上)是應(yīng)用程序的屬性。應(yīng)用可能依賴數(shù)據(jù)庫(kù)的原子性和隔離屬性來實(shí)現(xiàn)一致性,但這并不僅取決于數(shù)據(jù)庫(kù)。因此,字母 C 不屬于 ACID 。

          很多時(shí)候,我們一直在糾結(jié)的一致性,其實(shí)就是指符合現(xiàn)實(shí)世界的一致性,現(xiàn)實(shí)世界的一致性才是事務(wù)追求的最終目標(biāo)。

          為了實(shí)現(xiàn)現(xiàn)實(shí)世界的一致性,需要滿足如下幾點(diǎn):

          1. 保證原子性,持久性和隔離性,如果這些特征都無法保證,那么事務(wù)的一致性也無法保證;
          2. 數(shù)據(jù)庫(kù)本身的約束,比如字符串長(zhǎng)度不能超過列的限制或者唯一性約束;
          3. 業(yè)務(wù)層面同樣需要進(jìn)行保障 。

          4.5 總結(jié)

          我們通常稱 Redis 為內(nèi)存數(shù)據(jù)庫(kù) ,  不同于傳統(tǒng)的關(guān)系數(shù)據(jù)庫(kù),為了提供了更高的性能,更快的寫入速度,在設(shè)計(jì)和實(shí)現(xiàn)層面做了一些平衡,并不能完全支持事務(wù)的 ACID。

          Redis 的事務(wù)具備如下特點(diǎn):

          • 保證隔離性;
          • 無法保證持久性;
          • 具備了一定的原子性,但不支持回滾;
          • 一致性的概念有分歧,假設(shè)在一致性的核心是約束的語意下,Redis 的事務(wù)可以保證一致性。

          另外,在搶紅包的場(chǎng)景下, 因?yàn)槊總€(gè)步驟需要依賴上一個(gè)步驟返回的結(jié)果,需要通過 watch 來實(shí)現(xiàn)樂觀鎖 ,從工程角度來看, Redis 事務(wù)并不適合該業(yè)務(wù)場(chǎng)景。

          5 Lua 腳本

          5.1 簡(jiǎn)介

          “ Lua ” 在葡萄牙語中是“月亮”的意思,1993年由巴西的 Pontifical Catholic University 開發(fā)。

          該語言的設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。

          Lua 腳本可以很容易的被 C/C ++ 代碼調(diào)用,也可以反過來調(diào)用 C/C++ 的函數(shù),這使得 Lua 在應(yīng)用程序中可以被廣泛應(yīng)用。不僅僅作為擴(kuò)展腳本,也可以作為普通的配置文件,代替 XML, Ini 等文件格式,并且更容易理解和維護(hù)。

          Lua 由標(biāo)準(zhǔn) C 編寫而成,代碼簡(jiǎn)潔優(yōu)美,幾乎在所有操作系統(tǒng)和平臺(tái)上都可以編譯,運(yùn)行。

          一個(gè)完整的 Lua 解釋器不過 200 k,在目前所有腳本引擎中,Lua 的速度是最快的。這一切都決定了 Lua 是作為嵌入式腳本的最佳選擇。

          Lua 腳本在游戲領(lǐng)域大放異彩,大家耳熟能詳?shù)摹洞笤捨饔蜪I》,《魔獸世界》都大量使用 Lua 腳本。

          Java 后端工程師接觸過的 api 網(wǎng)關(guān),比如 OpenrestyKong 都可以看到 Lua 腳本的身影。

          從 Redis 2.6.0 版本開始, Redis內(nèi)置的 Lua 解釋器,可以實(shí)現(xiàn)在 Redis 中運(yùn)行 Lua 腳本。

          使用 Lua 腳本的好處 :

          • 減少網(wǎng)絡(luò)開銷。將多個(gè)請(qǐng)求通過腳本的形式一次發(fā)送,減少網(wǎng)絡(luò)時(shí)延。
          • 原子操作。Redis會(huì)將整個(gè)腳本作為一個(gè)整體執(zhí)行,中間不會(huì)被其他命令插入。
          • 復(fù)用。客戶端發(fā)送的腳本會(huì)永久存在 Redis 中,其他客戶端可以復(fù)用這一腳本而不需要使用代碼完成相同的邏輯。

          Redis Lua 腳本常用命令:

          序號(hào)命令及描述
          1EVAL script numkeys key [key ...] arg [arg ...] 執(zhí)行 Lua 腳本。
          2EVALSHA sha1 numkeys key [key ...] arg [arg ...] 執(zhí)行 Lua 腳本。
          3SCRIPT EXISTS script [script ...] 查看指定的腳本是否已經(jīng)被保存在緩存當(dāng)中。
          4SCRIPT FLUSH 從腳本緩存中移除所有腳本。
          5SCRIPT KILL 殺死當(dāng)前正在運(yùn)行的 Lua 腳本。
          6SCRIPT LOAD script 將腳本 script 添加到腳本緩存中,但并不立即執(zhí)行這個(gè)腳本。

          5.2 EVAL 命令

          命令格式:

          EVAL script numkeys key [key ...] arg [arg ...]

          說明:

          • script是第一個(gè)參數(shù),為 Lua 5.1腳本;
          • 第二個(gè)參數(shù)numkeys指定后續(xù)參數(shù)有幾個(gè) key;
          • key [key ...],是要操作的鍵,可以指定多個(gè),在 Lua 腳本中通過KEYS[1], KEYS[2]獲取;
          • arg [arg ...],參數(shù),在 Lua 腳本中通過ARGV[1], ARGV[2]獲取。

          簡(jiǎn)單實(shí)例:

          redis> eval "return ARGV[1]" 0 100 
          "100"
          redis> eval "return {ARGV[1],ARGV[2]}" 0 100 101
          1) "100"
          2) "101"
          redis> eval "return {KEYS[1],KEYS[2],ARGV[1]}" 2 key1 key2 first second
          1) "key1"
          2) "key2"
          3) "first"
          4) "second"

          下面演示下 Lua 如何調(diào)用 Redis 命令 ,通過redis.call()來執(zhí)行了 Redis 命令 。

          redis> set mystring 'hello world'
          OK
          redis> get mystring
          "hello world"
          redis> EVAL "return redis.call('GET',KEYS[1])" 1 mystring
          "hello world"
          redis> EVAL "return redis.call('GET','mystring')" 0
          "hello world"

          5.3 EVALSHA 命令

          使用 EVAL 命令每次請(qǐng)求都需要傳輸 Lua 腳本 ,若 Lua 腳本過長(zhǎng),不僅會(huì)消耗網(wǎng)絡(luò)帶寬,而且也會(huì)對(duì) Redis 的性能造成一定的影響。

          思路是先將 Lua 腳本先緩存起來 ,  返回給客戶端 Lua 腳本的 sha1 摘要。 客戶端存儲(chǔ)腳本的 sha1 摘要 ,每次請(qǐng)求執(zhí)行 EVALSHA  命令即可。

          EVALSHA  命令基本語法如下:

          redis> EVALSHA sha1 numkeys key [key ...] arg [arg ...] 

          實(shí)例如下:

          redis> SCRIPT LOAD "return 'hello world'"
          "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
          redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
          "hello world"

          5.4 事務(wù) VS Lua 腳本

          從定義上來說, Redis 中的腳本本身就是一種事務(wù), 所以任何在事務(wù)里可以完成的事, 在腳本里面也能完成。 并且一般來說, 使用腳本要來得更簡(jiǎn)單,并且速度更快。

          因?yàn)槟_本功能是 Redis 2.6 才引入的, 而事務(wù)功能則更早之前就存在了, 所以 Redis 才會(huì)同時(shí)存在兩種處理事務(wù)的方法。

          不過我們并不打算在短時(shí)間內(nèi)就移除事務(wù)功能, 因?yàn)槭聞?wù)提供了一種即使不使用腳本, 也可以避免競(jìng)爭(zhēng)條件的方法, 而且事務(wù)本身的實(shí)現(xiàn)并不復(fù)雜。

          --  https://redis.io/

          Lua 腳本是另一種形式的事務(wù),他具備一定的原子性,但腳本報(bào)錯(cuò)的情況下,事務(wù)并不會(huì)回滾。Lua 腳本可以保證隔離性,而且可以完美的支持后面的步驟依賴前面步驟的結(jié)果

          綜上,Lua 腳本是搶紅包場(chǎng)景最優(yōu)的解決方案。

          但在編寫 Lua 腳本時(shí),要注意如下兩點(diǎn):

          1. 為了避免 Redis 阻塞,Lua 腳本業(yè)務(wù)邏輯不能過于復(fù)雜和耗時(shí);
          2. 仔細(xì)檢查和測(cè)試 Lua 腳本 ,因?yàn)閳?zhí)行 Lua 腳本具備一定的原子性,不支持回滾。

          6 實(shí)戰(zhàn)準(zhǔn)備

          我選擇 Redisson 3.12.0 版本作為 Redis 的客戶端,在 Redisson 源碼基礎(chǔ)上做一層薄薄的封裝。

          創(chuàng)建一個(gè) PlatformScriptCommand 類, 用來執(zhí)行 Lua 腳本。

          // 加載 Lua 腳本 
          String scriptLoad(String luaScript);
          // 執(zhí)行 Lua 腳本
          Object eval(String shardingkey, 
                      String luaScript, 
                      ReturnType returnType,
                      List<Object> keys, 
                      Object... values)
          ;
          // 通過 sha1 摘要執(zhí)行Lua腳本
          Object evalSha(String shardingkey, 
                         String shaDigest,
                         List<Object> keys, 
                         Object... values)
          ;

          這里為什么我們需要添加一個(gè) shardingkey 參數(shù)呢 ?

          因?yàn)?Redis 集群模式下,我們需要定位哪一個(gè)節(jié)點(diǎn)執(zhí)行 Lua 腳本。

          public int calcSlot(String key) {
              if (key == null) {
                  return 0;
              }
              int start = key.indexOf('{');
              if (start != -1) {
                  int end = key.indexOf('}');
                  key = key.substring(start+1, end);
              }
              int result = CRC16.crc16(key.getBytes()) % MAX_SLOT;
              log.debug("slot {} for {}", result, key);
              return result;
          }

          7 搶紅包腳本

          客戶端執(zhí)行 Lua 腳本后返回 json 字符串。

          • 用戶搶紅包成功
          {
              "code":"0",
              //紅包金額   
              "amount":"7.1",
              //紅包編號(hào)
              "redPacketId":"162339217730846210"
          }
          • 用戶已領(lǐng)取過
          {
              "code":"1"
          }
          • 用戶搶紅包失敗
          {
              "code":"-1"
          }

          Redis Lua 中內(nèi)置了 cjson 函數(shù),用于 json 的編解碼。

          -- KEY[1]: 用戶防重領(lǐng)取記錄
          local userHashKey = KEYS[1];
          -- KEY[2]: 運(yùn)營(yíng)預(yù)分配紅包列表
          local redPacketOperatingKey = KEYS[2];
          -- KEY[3]: 用戶紅包領(lǐng)取記錄 
          local userAmountKey = KEYS[3];
          -- KEY[4]: 用戶編號(hào)
          local userId = KEYS[4];
          local result = {};
          -- 判斷用戶是否領(lǐng)取過 
          if redis.call('hexists', userHashKey, userId) == 1 then
            result['code'] = '1'
            return cjson.encode(result);
          else
             -- 從預(yù)分配紅包中獲取紅包數(shù)據(jù)
             local redPacket = redis.call('rpop', redPacketOperatingKey);
             if redPacket
             then
                local data = cjson.decode(redPacket);
                -- 加入用戶ID信息
                data['userId'] = userId; 
               -- 把用戶編號(hào)放到去重的哈希,value設(shè)置為紅包編號(hào)
                redis.call('hset', userHashKey, userId, data['redPacketId']);
               --  用戶和紅包放到已消費(fèi)隊(duì)列里
                redis.call('lpush', userAmountKey, cjson.encode(data));
               -- 組裝成功返回值
                result['redPacketId'] = data['redPacketId'];
                result['code'] = '0';
                result['amount'] = data['amount'];
                return cjson.encode(result);
             else
                -- 搶紅包失敗
                result['code'] = '-1';
                return cjson.encode(result);
             end 
          end

          腳本編寫過程中,難免會(huì)有疏漏,如何進(jìn)行調(diào)試?

          個(gè)人建議兩種方式結(jié)合進(jìn)行。

          1. 編寫 junit 測(cè)試用例 ;
          2. 從 Redis 3.2 開始,內(nèi)置了 Lua debugger(簡(jiǎn)稱LDB), 可以使用 Lua debugger 對(duì) Lua 腳本進(jìn)行調(diào)試。

          8 異步任務(wù)

          在 Redisson 基礎(chǔ)上封裝了兩個(gè)類 ,簡(jiǎn)化開發(fā)者的使用成本。

          1. RedisMessageConsumer :  消費(fèi)者類,配置監(jiān)聽隊(duì)列名,以及對(duì)應(yīng)的消費(fèi)監(jiān)聽器
          String groupName = "userGroup";
          String queueName = "userAmountQueue";
          RedisMessageQueueBuilder buidler =
                  redisClient.getRedisMessageQueueBuilder();
          RedisMessageConsumer consumer =
                  new RedisMessageConsumer(groupName, buidler);
          consumer.subscribe(queueName, userAmountMessageListener);
          consumer.start();
          1. RedisMessageListener :  消費(fèi)監(jiān)聽器,編寫業(yè)務(wù)消費(fèi)代碼
          public class UserAmountMessageListener implements RedisMessageListener {
            @Override
            public RedisConsumeAction onMessage(RedisMessage redisMessage) {
             try {
              String message = (String) redisMessage.getData();
              // TODO 調(diào)用用戶余額系統(tǒng)
              // 返回消費(fèi)成功
              return RedisConsumeAction.CommitMessage;
             }catch (Exception e) {
              logger.error("userAmountService invoke error:", e);
              // 消費(fèi)失敗,執(zhí)行重試操作
              return RedisConsumeAction.ReconsumeLater;
            }
           }
          }

          9 寫到最后

          "紙上得來終覺淺, 絕知此事要躬行" 。

          學(xué)習(xí) Redis Lua 過程中,查詢了很多資料,一個(gè)例子一個(gè)例子的實(shí)踐,收獲良多。

          非常坦誠(chéng)的講 ,  寫這篇文章之前,我對(duì) Redis Lua 有很多想當(dāng)然的理解,比如 Redis 的事務(wù)不能回滾就讓我驚訝不已。

          所以當(dāng)面對(duì)自己不熟悉的知識(shí)點(diǎn)時(shí),不要輕易下結(jié)論,以謙卑的心態(tài)去學(xué)習(xí),才是一個(gè)工程師需要的心態(tài)。

          同時(shí),沒有任何一項(xiàng)技術(shù)是完美的,在設(shè)計(jì)和編碼之間,有這樣或者那樣的平衡,這才是真實(shí)的世界。


          如果我的文章對(duì)你有所幫助,還請(qǐng)幫忙點(diǎn)贊、在看、轉(zhuǎn)發(fā)一下,你的支持會(huì)激勵(lì)我輸出更高質(zhì)量的文章,非常感謝!

          瀏覽 109
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本五十路の浓厚交尾 | 99热| 欧美成人在线观看网站 | 欧美亚洲一 | 五月天天婷婷 |