Redis在項目中的運用總結

1 概述
Redis作爲一款性能優異的內存數據庫,在互聯網公司有着多種應用場景,本文介紹筆者在項目中使用Redis的場景。主要從以下幾個方面介紹:

分佈式鎖
接口限流器
訂單緩存
Redis和DB數據一致性處理
防止緩存穿透和雪崩
分佈式session共享
2 分佈式鎖
Redis實現分佈式鎖

3 接口限流器
Redis實現限流器

4 訂單緩存
整個訂單的存儲結構如下:

使用Redis的zset數據結構存儲每個用戶的訂單,按照下單時間倒序排列,用戶唯一標識作爲key,用戶的訂單集合作爲value,使用訂單創建時間的時間戳+訂單號後三位作爲分數
爲什麼不直接使用下單時間的時間戳作爲分數?因爲下單時間只精確到秒,同一秒可能出現多個訂單情況,這樣就會出現相同的分數,而加上訂單號後三位就能基本上避免這種情景。
只放用戶的前N條訂單即可,因爲很少有用戶會查看很久以前的訂單,這樣做會節省很多空間。如果有用戶需要查看前N條之後的訂單,再從數據庫中查詢即可,當然這種概率就比較小了。
5 Redis和DB數據一致性處理
只要有多份數據,就會涉及到數據一致性的問題。Redis和數據庫的數據一致性,也是必然要面對的問題。我們這邊的訂單數據是先更新數據庫,數據庫更新成功後,再更新緩存,若數據庫操作成功,緩存操作失敗了,就出現了數據不一致的情況。保證數據一致性我們前後使用過兩種方式:

方式一

循環5次更新緩存操作,直到更新成功退出循環,這一步主要能減小由於網絡瞬間抖動導致的更新緩存失敗的概率,對於緩存接口長時間不可用,靠循環調用更新接口是不能補救接口調用失敗的。
如果循環5次還沒有更新成功,就通過worker去定時掃描出數據庫的數據,去和緩存中的數據進行比較,對緩存中的狀態不正確的數據進行糾正。
方式二

跟方式一的第一步操作一樣
若循環更新5次仍不成功,則發一個緩存更新失敗的mq,通過消費mq去更新緩存,會比通過定時任務掃描更及時,也不會有掃庫的耗時操作。此方式也是我們現在使用的方式,下面是示例代碼:

for (int i = 0; i < 5; i++) {
    try {
        // 入緩存操作
        addOrderListRedis(key, score, orderListVO);
        break;
    } catch (Exception e) {
        log.error("{}IOrderRedisCache.putOrderList2OrderListRedis--->>jdCacheCloud.zAdd exception:", logSid, e);
        if (i == 4) sendUpOrderCacheMQ(orderListVO, logSid); // 如果循環5次,仍添加緩存失敗,發送MQ,通過MQ繼續更新緩存
    }
}
6 防止緩存穿透和雪崩
緩存爲我們擋住了80-90%甚至更多的流量,然而當緩存中的大量熱點數據恰巧在差不多的時間過期時,或者當有人惡意僞造一些緩存中根本沒有的數據瘋狂刷接口時,就會有大量的請求直接穿透緩存訪問到數據庫(因爲查詢數據策略是緩存沒有命中,就查數據庫),給數據庫造成巨大壓力,甚至使數據庫崩潰,這肯定是我們系統不允許出現的情況。我們需要針對這種情況進行處理。下圖是處理流程圖: 


示例代碼:

// 代碼段1
// 鎖的數量 鎖的數量越少 每個用戶對鎖的競爭就越激烈,直接打到數據庫的流量就越少,對數據庫的保護就越好,如果太小,又會影響系統吞吐量,可根據實際情況調整鎖的個數
public static final String[] LOCKS = new String[128];
// 在靜態塊中將128個鎖先初始化出來
static {
    for (int i = 0; i < 128; i++) {
        LOCKS[i] = "lock_" + i;
    }
}

// 代碼段2
public List<OrderVOList> getOrderVOList(String userId) {
    List<OrderVOList> list = null;
    // 1.先判斷緩存中是否有這個用戶的數據,有就直接從緩存中查詢並返回
    if (orderRedisCache.isOrderListExist(userId)) {
        return  getOrderListFromCache(userId); 
    }
    // 2.緩存中沒有,就先上鎖,鎖的粒度是根據用戶Id的hashcode和127取模
    String[] locks = OrderRedisKey.LOCKS;
    int index = userId.hashCode() & (locks.length - 1);
    try {
        // 3.此處加鎖很有必要,加鎖會保證獲取同一個用戶數據的所有線程中,只有一個線程可以訪問數據庫,從而起到減小數據庫壓力的作用
        orderRedisCache.lock(locks[index]);
        // 4.上鎖之後再判斷緩存是否存在,爲了防止再獲得鎖之前,已經有別的線程將數據加載到緩存,就不允許再查詢數據庫了。
        if (orderRedisCache.isOrderListExist(userId)) {
            return getOrderListFromCache(userId); 
        }
        // 查詢數據庫
        list = getOrderListFromDb(userId);
        // 如果數據庫沒有查詢出來數據,則在緩存中放入NULL,標識這個用戶真的沒有數據,等有新訂單入庫時,會刪掉此標識,並放入訂單數據
        if(list == null || list.size() == 0) {
            jdCacheCloud.zAdd(OrderRedisKey.getListKey(userId), 0, null);
        } else {
            jdCacheCloud.zAdd(OrderRedisKey.getListKey(userId), list);
        }
        return list;
    } finally {
        orderRedisCache.unlock(locks[index]);
    }
}
防止穿透和雪崩的關鍵地方在於使用分佈式鎖和鎖的粒度控制。首先初始化了128(0-127)個鎖,然後讓所有緩存沒命中的用戶去競爭這128個鎖,得到鎖後並且再一次判斷緩存中依然沒有數據的,纔有權利去查詢數據庫。沒有將鎖粒度限制到用戶級別,是因爲如果粒度太小的話,某一個時間點有太多的用戶去請求,同樣會有很多的請求打到數據庫。比如:在時間點T1有10000個用戶的緩存數據失效了,恰恰他們又在時間點T1都請求數據,如果鎖粒度是用戶級別,那麼這10000個用戶都會有各自的鎖,也就意味着他們都可以去訪問數據庫,同樣會對數據庫造成巨大壓力。而如果是通過用戶id去hashcode和127取模,意味着最多會產生128個鎖,最多會有128個併發請求訪問到數據庫,其他的請求會由於沒有競爭到鎖而阻塞,待第一批獲取到鎖的線程釋放鎖之後,剩下的請求再進行競爭鎖,但此次競爭到鎖的線程,在執行代碼段2中第4步時:orderRedisCache.isOrderListExist(userId),緩存中有可能已經有數據了,就不用再查數據庫了,依次類推,從而可以擋住很多數據庫請求,起到很好的保護數據庫的作用。
--------------------- 
作者:秦霜 
來源:CSDN 
原文:https://blog.csdn.net/wang258533488/article/details/78901124 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章