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

開篇思考

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

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

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

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


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


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

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

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

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

Redis 簡介

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

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


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


內存

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

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


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


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

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

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

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

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

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

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


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


以上是一個簡單的系統訪問流程圖,先執行的接口因為沒有對應的 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
  • 一旦服務不可用,即使恢復服務,也無法恢復 session
  • session 管理困難

因此引入 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 獲取數據, 不會有請求到達數據庫層。

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

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

怎麼解決?

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

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

思考題

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

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

喜歡文章請關注我

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


分享到:


相關文章: