3C科技 娛樂遊戲 美食旅遊 時尚美妝 親子育兒 生活休閒 金融理財 健康運動 寰宇綜合

Zi 字媒體

2017-07-25T20:27:27+00:00
加入好友
一個月前——整個優惠券中心分為前端和後端,小灰所負責的是後端RPC介面的開發。介面中包含「查券」和「領券」兩個方法,項目大體結構如下圖:兩周后—— 小灰:看,這是優惠券查詢功能的效果! 小灰:看,這是優惠券領取功能的效果! 三天後——小灰原本的優惠券查詢介面是這樣實現的:優惠券列表在Redis中以List的形式存儲,查詢時的邏輯很簡單:1.查詢緩存,如果緩存存在,返回結果2.緩存不存在,查詢資料庫3.把查詢資料庫的結果循環放入緩存然而,當某個時間點緩存不存在,請求量又很大的時候,會出現緩存併發的問題。也就是多個線程會重複去查詢DB,又重複去更新緩存。(注意,這並不是緩存擊穿,很多人在這兩個概念上混淆。)這其中重複查詢DB是次要問題,而重複更新緩存則是主要問題。假如有兩個線程同時進入上述的第三個階段,各自進行rpush操作,那麼最終會在優惠券列表的緩存中插入兩組同樣的數據。怎麼解決呢?用Java的鎖機制?顯然不行,因為線上環境通常都是多個伺服器組成的集群。於是小灰想到了利用分散式鎖。所謂分散式鎖有很多種,可以利用ZooKeeper、MemCache、Redis來實現。其中Redis的方式比較簡單,無非是利用一個伺服器之間共享的Key,以及Setnx指令。當第一個線程執行Setnx,會存儲對應的鍵值,相當於成功獲得鎖。當後續再有線程對同於的Key執行Setnx指令,則會返回空,相當於搶鎖失敗。同時,為了防止一個線程因意外情況而長久把持著鎖,程序對Key設置了1秒的過期時間。歸納一下修改後的邏輯:1.查詢緩存,如果緩存存在,返回結果2.緩存不存在,查詢資料庫3.爭奪分散式鎖4.成功獲得鎖,把查詢資料庫的結果循環放入緩存5.釋放分散式鎖三天後——詭異的bug又重現了,因為小灰上次的改動仍然存在一個致命的漏洞。在這裡我們假定緩存不存在,剛好有兩個線程A和B一后一先進入到代碼塊。第一階段,線程A剛開始查詢優惠券緩存,線程B正嘗試獲取分散式鎖: 第二階段,由於緩存不存在,線程A開始查詢資料庫,線程B成功獲得鎖,開始更新緩存:第三階段,線程A嘗試獲得分散式鎖,而線程B已經釋放分散式鎖:第四階段,線程A獲得了鎖,又一次更新緩存,而線程B已經成功返回: 就這樣,緩存被重複更新了兩次,所以再次出現數據重複的bug。這種局面如何破解呢?其實不難,只需在線程成功得到鎖以後,再次判斷優惠券緩存的存在:歸納一下修改後的邏輯:1.查詢緩存,如果緩存存在,返回結果2.緩存不存在,查詢資料庫3.爭奪分散式鎖4.成功獲得鎖,再次判斷緩存的存在5.如果緩存仍舊不存在,把查詢資料庫的結果循環放入緩存6.釋放分散式鎖這種二次判斷存在性的機制有一個專門的名字,叫做雙重檢測。該方法在線程安全的單例模式中也常常被用到。小灰的回憶告一段落——幾點補充:1.文中所使用的分散式鎖,其實並不是「正宗」的分散式鎖,當線程爭奪鎖失敗的時候,會直接返回查詢DB的結果,而不會依靠自旋機制來等鎖。2.為什麼優惠券列表的信息要使用List類型來存入緩存,而不是把整個列表存為一個很長的Json字元串?這是由於業務需要,使用List在某些情況下更方便對單個優惠券信息進行修改(LSET指令)。3.為什麼優惠券列表的信息不使用Redis的Set或者Hash數據類型來存儲,實現自動去重呢?對於Set類型,去重前需要對比整個字元串是否完全相同,而每一張優惠券是一個較長的Json字元串,對比的效率會比較低。使用Hash倒是可以實現高效的去重,但並未在根本上解決重複更新的問題。—————END—————微信關注「騰訊課堂」(tencent-class),收穫更多乾貨

本文由yidianzixun提供 原文連結

寫了 5860316篇文章,獲得 23313次喜歡
精彩推薦