正經的聊聊分佈式架構中的 redis

開篇思考

Redis 為什麼在系統中使用?解決了哪些問題?Redis 如何保證和數據庫同步?Redis 緩存操作是在操作數據庫前還是操作數據庫後?

話還得從上次報稅說起,耳邊還回繞這殘留的芬芳:“SX系統,這也不能點,那也不能用!”, 身為程序員的我聽到總是百感交集,程序員背鍋是免不了了。。。

上線至今都能用的系統,突然就不行了,為什麼?問題就在穩定性和系統架構上,發現問題就要吸取經驗和血的教訓。

我也特別喜歡吐槽,我覺得正確的吐槽姿勢有助於系統的良性發展,就像父母的愛強烈扎刺著程序員面臨崩潰的心靈, 流出的愛的液體澆灌給系統茁壯成長。



系統穩定,快速,美如畫誰都想追求,可是往往美好的東西后面代價也不小。

追求可靠,我們需要+集群部署,容錯容災,那麼就需要更多的機器設施及其他附屬服務。

追求快速,我們需要解決地域限制,全球部署戰鬥機,DNS 快速定位訪問,軟件層面緩存技術。

那麼接下來我們就來扒一扒分佈式系統架構中 Redis 的使用,進入正題,不扯蛋了。 讓我們看看 Redis 給分佈式系統帶來哪些好處和問題的解決方案,看看這些代價是否值得。

Redis 簡介

內存存儲,速度極快key-value 存儲結構支持 string,list,set,zset,hash 類型,其實還有一些不常用的基於 epoll 多路複用,串行執行效率高可以持久化數據,遇到宕機可以快速恢復redis 支持主從模式、哨兵模式使用場景豐富:熱點數據緩存、臨時會話存儲、消息發佈訂閱、網頁計數

上面的介紹中,我基本扒出了 redis 的主要特點,外衣都給你扒了,這麼赤裸的誘惑你們都不要嗎?覺得還是不夠吸引嗎? 那我們就繼續來扒拉扒拉。。。



內存

Redis 都是通過計算機內存來存取的,不用多解釋。它為什麼快?JMM java 中的內存模型大家瞭解吧,java 中每個線程會有自己的內存,要想達成可見性,需要同步主內存,這一操作聽起來 很簡單,但其實裡面數據被拷貝了多次。這裡簡單介紹下傳統的磁盤到網絡的數據拷貝流程:

磁盤到 read buffer, 快read buffer 到 user buffer ,此處很慢,上下文有切換user buffer 到 socket buffer ,快socket buffer 寫入到網卡 buffer 發送,快



好傢伙,不扒不知道,原來底層數據是這麼傳輸的。Redis 為什麼快呢,因為它官方只支持 linux 系統,而 Linux 本身還支持零拷貝技術,並且這裡都是純內存操作,所有的數據操作都非常快。

那麼究竟有多快呢, 一秒真男人:讀 10 w/s;寫 8w/s;當然數據只能是小數據流量的。

零拷貝技術被廣泛應用在 Java NIO,netty,kafka 等。

redis 實現系統的接口冪等控制

每個工程師都應該知道接口冪等的重要性,在分佈式系統中,接口冪等的設計原則貫徹始終。所謂接口冪等就是無論我在某個業務執行過程中調用多少次接口,得到的結果都應該和調用一次接口得到的結果一樣。因此我們知道查詢、刪除這些是天然冪等的,沒有必要再做冪等性控制。那麼一般哪些接口需要實現冪等控制呢?redis 是起了什麼作用?

新增接口更新接口任何內部包含新增、修改操作接口

redis 的串行機制,可以幫助我們輕鬆實現接口冪等性控制。我們在訪問接口的時候,通過設置唯一性的 key token 來判斷, 如果 redis 當前存有該 key 和 token, 那麼就不執行業務邏輯,如果不存在則繼續執行業務邏輯。



以上是一個簡單的系統訪問流程圖,先執行的接口因為沒有對應的 token 值,所以會繼續執行業務, 而另一個接口因為其他的接口沒有執行結束,沒有刪除對應的 key value,所以不會執行資源操作。實際的開發中,我們可能不會在每個接口中都通過這麼一個邏輯來判斷,而是通過攔截器、自定義註解來實現統一的判斷邏輯.

當然 redis 不是唯一的方式來確保接口冪等,接口冪等的設計還可以通過數據庫去重表、表中的狀態機等機制來實現。

redis 實現分佈式鎖

在分佈式集群系統中,我們不能也不會讓所有的請求都在同一個服務上,那麼高併發請求下, 如何給接口上鎖來保證接口的串行執行?redis string 類型有個方法可以在接口中使用, setnx : set if not exit。 通過此函數來設置分佈式鎖。 在接口中通過 setnx 給當前接口設置一個全局唯一的值,可以是 商品Id + 接口信息; 當併發訪問該接口的時候,會再次調用 setnx 來判斷是否存在值:

第一次設值,成功,返回 1 ;有值,設置失敗,返回 0;

下面的例子是基於 lettuce 連接的 RedisTemplete 設置鎖代碼,其中 tryLock 是偽代碼,具體使用根據實際情況。

<code>/** * 嘗試獲取鎖 ,並返回結果 * @param key * @param value * @param expireTime (此處為秒) * @return boolean * @author holy * @date 2020年4月08日 * */ public boolean tryAcquire(String key, String value, long expireTime){ return redisTemplate.opsForValue().setIfAbsent(key,value, Duration.ofSeconds(expireTime)); } /** * 設置分佈式鎖 * @param key * @param value * @param expireTime (此處為秒) * @return boolean * @author holy * @date 2020年4月08日 * */ public boolean tryLock(String key, String value, long expireTime){ boolean tryAcquire = tryAcquire(key, value, expireTime); // 偽代碼,根據實際情況謹慎使用 // 根據實際情況使用,如果不需要自旋,不理解自旋鎖,或者不夠了解 AQS 的不建議使用 // 此處主要是自旋固定 10 次 int i = 10; if (!tryAcquire){ for (;;){ i--; if (tryAcquire){ return Boolean.TRUE; } if (i < 1){ return Boolean.FALSE; } } } return Boolean.TRUE; } /<code>

redis 管理分佈式共享 session

在分佈式系統中,因為我們的服務是集群部署,服務可能不是在同一臺機器上面。這時候就會發現 session 引發的問題:

如果請求是鏈路結構,請求可能會分發到不同的機器不同的服務上,多個服務無法共享 session一旦服務不可用,即使恢復服務,也無法恢復 sessionsession 管理困難

因此引入 session 共享被廣泛的應用,redis 就是非常好的一種選擇,而且據說和 spring session 完美結合。 這個非常簡單,以前使用 springboot 1.5 的時候是通過引入依賴,添加配置進行的,這裡簡單貼下代碼, springboot 2.X 的應該差不多,支持應該只會更好、更簡單的配置。

<code> org.springframework.session spring-session-data-redis /<code>

<code># ============ srping session ============ spring.session.store-type=redis spring.session.redis.flush-mode=on_save spring.session.redis.namespace=madmin /<code>

<code> @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 10800) @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } /<code>



redis 在架構中的緩存中間件

redis 因為高併發、快速的特性,還被廣泛應用在系統的緩存架構中。在流量分佈式系統中,我們的請求如果全部訪問數據庫將會是一場災難, 數據庫很可能會因為不堪重負被幹趴,而數據庫的不可用會造成更嚴重的服務不可用甚至雪崩效應。因此在系統架構設計都會加入緩存中間件來緩解數據庫壓力,減少請求直接到數據庫,提高系統性能。 尤其在大流量的系統設計的時候,例如秒殺系統,緩存中間件就必不可少。 redis 的特性天然的成為了緩存中間件的首選。



那麼 redis 裡到底存什麼呢?下面我以秒殺系統為例列出:

秒殺商品具體信息秒殺商品熱門排行榜列表秒殺商品庫存信息

在秒殺系統中,大部分會請求會去查詢商品信息,排行榜等信息,這些信息並不會經常變動,也不會要求非常高的一致性, 因此十分適合放入緩存中。那麼怎麼接口中如何設計呢?

接口設計的時候,用戶請求的數據,全部都在 redis 中獲取,如果 redis 中沒有,才去數據庫中獲取,然後更新 redis。這樣在請求接口的時候,理想的狀態,如果商品全部緩存成功在 redis 裡,那麼用戶只會從 redis 獲取數據, 不會有請求到達數據庫層。

但是理想狀態只能是理想狀態,實際上我們會遇到一些問題,比如緩存擊穿、緩存穿透:

緩存擊穿:熱點數據失效,就像就像瞬間高壓電擊一樣擊穿了 redis 緩存,緩存失效直接訪問數據庫緩存穿透:redis 裡面沒有數據,DB 中也沒有數據,所有請求直接訪問 DB,造成緩存穿透緩存雪崩:說有緩存集體失效,導致服務不可用。

怎麼解決?

緩存擊穿:定時任務後臺刷新;設置長久模式;加分佈式鎖;緩存穿透:緩存空值,即使沒有數據也做緩存;布隆過濾器,;緩存雪崩:預熱數據;redis 高可用;redis 限流;

如果對布隆過濾器不是很瞭解的,可以看下這篇文章 《高併發架構中一定要考慮的Bloom Filter 布隆過濾器》

思考題

用了緩存技術,那麼我們更新數據的時候,是先更新緩存還是先更新數據庫呢?建議大家把情況列出來然後逐一分析問題。也歡迎大家在評論區寫出自己的答案。

今天就寫到這裡了,晚上我還有十幾個億的生意要談。。。再會!

喜歡文章請關注我

程序領域點擊關注+轉發,私信發送【面試】或者【資料】可以收穫更多資源