開篇思考
- 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 是僞代碼,具體使用根據實際情況。
/**
* 嘗試獲取鎖 ,並返回結果
* @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;
}
redis 管理分佈式共享 session
在分佈式系統中,因爲我們的服務是集羣部署,服務可能不是在同一臺機器上面。這時候就會發現 session 引發的問題: * 如果請求是鏈路結構,請求可能會分發到不同的機器不同的服務上,多個服務無法共享 session
- 一旦服務不可用,即使恢復服務,也無法恢復 session
- session 管理困難
因此引入 session 共享被廣泛的應用,redis 就是非常好的一種選擇,而且據說和 spring session 完美結合。
這個非常簡單,以前使用 springboot 1.5 的時候是通過引入依賴,添加配置進行的,這裏簡單貼下代碼,
springboot 2.X 的應該差不多,支持應該只會更好、更簡單的配置。
<!-- spring session redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
# ============ srping session ============
spring.session.store-type=redis
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=madmin
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 10800)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); } }
redis 在架構中的緩存中間件
redis 因爲高併發、快速的特性,還被廣泛應用在系統的緩存架構中。 在流量分佈式系統中,我們的請求如果全部訪問數據庫將會是一場災難, 數據庫很可能會因爲不堪重負被幹趴,而數據庫的不可用會造成更嚴重的服務不可用甚至雪崩效應。
因此在系統架構設計都會加入緩存中間件來緩解數據庫壓力,減少請求直接到數據庫,提高系統性能。 尤其在大流量的系統設計的時候,例如秒殺系統,緩存中間件就必不可少。 redis 的特性天然的成爲了緩存中間件的首選。
那麼 redis 裏到底存什麼呢?下面我以秒殺系統爲例列出:
- 秒殺商品具體信息
- 秒殺商品熱門排行榜列表
- 秒殺商品庫存信息
在秒殺系統中,大部分會請求會去查詢商品信息,排行榜等信息,這些信息並不會經常變動,也不會要求非常高的一致性,
因此十分適合放入緩存中。那麼怎麼接口中如何設計呢?
接口設計的時候,用戶請求的數據,全部都在 redis 中獲取,如果 redis 中沒有,纔去數據庫中獲取,然後更新 redis。 這樣在請求接口的時候,理想的狀態,如果商品全部緩存成功在 redis 裏,那麼用戶只會從 redis 獲取數據,
不會有請求到達數據庫層。
但是理想狀態只能是理想狀態,實際上我們會遇到一些問題,比如緩存擊穿、緩存穿透:
- 緩存擊穿:熱點數據失效,就像就像瞬間高壓電擊一樣擊穿了 redis 緩存,緩存失效直接訪問數據庫
- 緩存穿透:redis 裏面沒有數據,DB 中也沒有數據,所有請求直接訪問 DB,造成緩存穿透
- 緩存雪崩:說有緩存集體失效,導致服務不可用。
怎麼解決?
- 緩存擊穿:定時任務後臺刷新;設置長久模式;加分佈式鎖;
- 緩存穿透:緩存空值,即使沒有數據也做緩存;布隆過濾器,;
- 緩存雪崩:預熱數據;redis 高可用;redis 限流;
如果對布隆過濾器不是很瞭解的,可以看下這篇文章
《高併發架構中一定要考慮的Bloom Filter 布隆過濾器》
思考題
用了緩存技術,那麼我們更新數據的時候,是先更新緩存還是先更新數據庫呢?建議大家把情況列出來然後逐一分析問題。 也歡迎大家在評論區寫出自己的答案。
今天就寫到這裏了,晚上我還有十幾個億的生意要談。。。再會!
喜歡文章請關注我
程序領域
點擊關注+轉發,私信發送【面試】或者【資料】可以收穫更多資源