jlzzjlzz亚洲乱熟在线播放

系統城裝機大師 - 唯一官網:www.farandoo.com!

當前位置:首頁 > 數據庫 > Redis > 詳細頁面

利用Redis進行數據緩存的項目實踐

時間:2022-06-09來源:www.farandoo.com作者:電腦系統城

1. 引言

緩存有啥用?

  • 降低對數據庫的請求,減輕服務器壓力
  • 提高了讀寫效率

緩存有啥缺點?

  • 如何保證數據庫與緩存的數據一致性問題?
  • 維護緩存代碼
  • 搭建緩存一般是以集群的形式進行搭建,需要運維的成本

2. 將信息添加到緩存的業務流程

在這里插入圖片描述

上圖可以清晰的了解Redis在項目中所處的位置,是數據庫與客戶端之間的一個中間件,也是數據庫的保護傘。有了Redis可以幫助數據庫進行請求的阻擋,阻止請求直接打入數據庫,提高響應速率,極大的提升了系統的穩定性。

3. 實現代碼

在這里插入圖片描述

下面將根據查詢商鋪信息來作為背景進行代碼書寫,具體的流程圖如上所示。

3.1 代碼實現(信息添加到緩存中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static final String SHOPCACHEPREFIX = "cache:shop:";
     
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
 
    // JSON工具
    ObjectMapper objectMapper = new ObjectMapper();
 
    @Override
    public Result queryById(Long id) {
        //從Redis查詢商鋪緩存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);
 
        //判斷緩存中數據是否存在
        if (!StringUtil.isNullOrEmpty(cacheShop)) {
            //緩存中存在則直接返回
            try {
                // 將子字符串轉換為對象
                Shop shop = objectMapper.readValue(cacheShop, Shop.class);
                return Result.ok(shop);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
 
        //緩存中不存在,則從數據庫里進行數據查詢
        Shop shop = getById(id);
 
        //數據庫里不存在,返回404
        if (null==shop){
            return Result.fail("信息不存在");
        }
        //數據庫里存在,則將信息寫入Redis
        try {
            String shopJSon = objectMapper.writeValueAsString(shop);
          stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        //返回
        return Result.ok(shop);
    }

3.2 緩存更新策略

數據庫與緩存數據一致性問題,當數據庫信息修改后,緩存的信息應該如何處理?

  內存淘汰 超時剔除 主動更新
說明 不需要自己進行維護,利用Redis的淘汰機制進行數據淘汰 給緩存數據添加TTL 編寫業務邏輯,在修改數據庫的同時更新緩存
一致性 差勁 一般
維護成本

這里其實是需要根據業務場景來進行選擇

  • 高一致性:選主動更新
  • 低一致性:內存淘汰和超時剔除

3.3 實現主動更新

此時需要實現數據庫與緩存一致性問題,在這個問題之中還有多個問題值得深思

刪除緩存還是更新緩存?
當數據庫發生變化時,我們如何處理緩存中無效的數據,是刪除它還是更新它?
更新緩存:每次更新數據庫都更新緩存,無效寫操作較多
刪除緩存:更新數據庫時刪除緩存,查詢時再添加緩存
由此可見,選擇刪除緩存是高效的。

如何保證緩存與數據庫的操作的同時成功或失???
單體架構:單體架構中采用事務解決
分布式架構:利用分布式方案進行解決

先刪除緩存還是先操作數據庫?

在這里插入圖片描述

在并發情況下,上述情況是極大可能會發生的,這樣子會導致緩存與數據庫數據庫不一致。

請添加圖片描述

先操作數據庫,在操作緩存這種情況,在緩存數據TTL剛好過期時,出現一個A線程查詢緩存,由于緩存中沒有數據,則向數據庫中查詢,在這期間內有另一個B線程進行數據庫更新操作和刪除緩存操作,當B的操作在A的兩個操作間完成時,也會導致數據庫與緩存數據不一致問題。

完蛋?。?!兩種方案都會造成數據庫與緩存一致性問題的發生,那么應該如何來進行選擇呢?

雖然兩者方案都會造成問題的發生,但是概率上來說還是先操作數據庫,再刪除緩存發生問題的概率低一些,所以可以選擇先操作數據庫,再刪除緩存的方案。

個人見解:
如果說我們在先操作數據庫,再刪除緩存方案中線程B刪除緩存時,我們利用java來刪除緩存會有Boolean返回值,如果是false,則說明緩存已經不存在了,緩存不存在了,則會出現上圖的情況,那么我們是否可以根據刪除緩存的Boolean值來進行判斷是否需要線程B來進行緩存的添加(因為之前是需要查詢的線程來添加緩存,這里考慮線程B來添加緩存,線程B是操作數據庫的緩存),如果線程B的添加也在線程A的寫入緩存之前完成也會造成數據庫與緩存的一致性問題發生。那么是否可以延時一段時間(例如5s,10s)再進行數據的添加,這樣子雖然最終會統一數據庫與緩存的一致性,但是若是在這5s,10s內又有線程C,D等等來進行緩存的訪問呢?C,D線程的訪問還是訪問到了無效的緩存信息。
所以在數據庫與緩存的一致性問題上,除非在寫入正確緩存之前拒絕相關請求進行服務器來進行訪問才能避免用戶訪問到錯誤信息,但是拒絕請求對用戶來說是致命的,極大可能會導致用戶直接放棄使用應用,所以我們只能盡可能的減少問題可能性的發生。(個人理解,有問題可以在評論區留言賜教)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
  @Transactional
  public Result updateShop(Shop shop) {
      Long id = shop.getId();
      if (null==id){
          return Result.fail("店鋪id不能為空");
      }
      //更新數據庫
      boolean b = updateById(shop);
      //刪除緩存
      stringRedisTemplate.delete(SHOPCACHEPREFIX+shop.getId());
      return Result.ok();
  }

4. 緩存穿透

緩存穿透是指客戶端請求的數據在緩存中和數據庫中都不存在,這樣緩存永遠不會生效,這些請求都會打到數據庫。

解決方案:

緩存空對象

在這里插入圖片描述

缺點:

  • 空間浪費
  • 如果緩存了空對象,在空對象的有效期內,我們后臺在數據庫新增了和空對象相同id的數據,這樣子就會造成數據庫與緩存一致性問題

布隆過濾器

在這里插入圖片描述

優點:

內存占用少

缺點:

  • 實現復雜
  • 存在誤判的可能(存在的數據一定會判斷成功,但是不存在的數據也有可能會放行進來,有幾率造成緩存穿透)

4.1 解決緩存穿透(使用空對象進行解決)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static final String SHOPCACHEPREFIX = "cache:shop:";
 
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
 
    // JSON工具
    ObjectMapper objectMapper = new ObjectMapper();
 
    @Override
    public Result queryById(Long id) {
        //從Redis查詢商鋪緩存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);
 
        //判斷緩存中數據是否存在
        if (!StringUtil.isNullOrEmpty(cacheShop)) {
            //緩存中存在則直接返回
            try {
                // 將子字符串轉換為對象
                Shop shop = objectMapper.readValue(cacheShop, Shop.class);
                return Result.ok(shop);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
 
        // 因為上面判斷了cacheShop是否為空,如果進到這個方法里面則一定是空,直接過濾,不打到數據庫
        if (null != cacheShop){
            return Result.fail("信息不存在");
        }
 
        //緩存中不存在,則從數據庫里進行數據查詢
        Shop shop = getById(id);
 
        //數據庫里不存在,返回404
        if (null==shop){
            // 緩存空對象
            stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,"",2,TimeUnit.MINUTES);
            return Result.fail("信息不存在");
        }
        //數據庫里存在,則將信息寫入Redis
        try {
            String shopJSon = objectMapper.writeValueAsString(shop);
            stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        //返回
        return Result.ok(shop);
    }

上述方案終究是被動方案,我們可以采取一些主動方案,例如

  • 給id加復雜度
  • 權限
  • 熱點參數的限流

5. 緩存雪崩

緩存雪崩是指在同一時段大量的緩存key同時失效或者Redis服務宕機,導致大量請求到達數據庫,帶來巨大壓力。

解決方案:

  • 給不同的Key的TTL添加隨機值
    大量的Key同時失效,極大可能是TTL相同,我們可以隨機給TTL
  • 利用Redis集群提高服務的可用性
  • 給緩存業務添加降級限流策略
  • 給業務添加多級緩存

6. 緩存擊穿

緩存擊穿問題也叫熱點Key問題,就是一個被高并發訪問并且緩存重建業務較復雜的key突然失效了,無數的請求訪問會在瞬間給數據庫帶來巨大的沖擊。

常見的解決方案:

  • 互斥鎖
  • 邏輯過期

互斥鎖:

在這里插入圖片描述

即采用鎖的方式來保證只有一個線程去重建緩存數據,其余拿不到鎖的線程休眠一段時間再重新重頭去執行查詢緩存的步驟

優點:

  • 沒有額外的內存消耗(針對下面的邏輯過期方案)
  • 保證了一致性

缺點:

  • 線程需要等待,性能受到了影響
  • 可能會產生死鎖

邏輯過期:

在這里插入圖片描述

邏輯過期是在緩存數據中額外添加一個屬性,這個屬性就是邏輯過期的屬性,為什么要使用這個來判斷是否過期而不使用TTL呢?因為使用TTL的話,一旦過期,就獲取不到緩存中的數據了,沒有拿到鎖的線程就沒有舊的數據可以返回。

它與互斥鎖最大的區別就是沒有線程的等待了,誰先獲取到鎖就去重建緩存,其余線程沒有獲取到鎖就返回舊數據,不去做休眠,輪詢去獲取鎖。

重建緩存會新開一個線程去執行重建緩存,目的是減少搶到鎖的線程的響應時間。

優點:

線程無需等待,性能好

缺點:

  • 不能保證一致性
  • 緩存中有額外的內存消耗
  • 實現復雜

兩個方案各有優缺點:一個保證了一致性,一個保證了可用性,選擇與否主要看業務的需求是什么,側重于可用性還是一致性。

6.1 互斥鎖代碼

互斥鎖的鎖用什么?

使用Redis命令的setnx命令。

首先實現獲取鎖和釋放鎖的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 嘗試獲取鎖
 *
 * @param key
 * @return
 */
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}
 
/**
 * 刪除鎖
 *
 * @param key
 */
private void unLock(String key) {
    stringRedisTemplate.delete(key);
}

在這里插入圖片描述

代碼實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public Shop queryWithMutex(Long id) throws InterruptedException {
        //從Redis查詢商鋪緩存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);
 
        //判斷緩存中數據是否存在
        if (!StringUtil.isNullOrEmpty(cacheShop)) {
            //緩存中存在則直接返回
            try {
                // 將子字符串轉換為對象
                Shop shop = objectMapper.readValue(cacheShop, Shop.class);
                return shop;
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
 
        // 因為上面判斷了cacheShop是否為空,如果進到這個方法里面則一定是空,直接過濾,不打到數據庫
        if (null != cacheShop) {
            return null;
        }
 
        Shop shop = new Shop();
        // 緩存擊穿,獲取鎖
        String lockKey = "lock:shop:" + id;
        try{
            boolean b = tryLock(lockKey);
            if (!b) {
                // 獲取鎖失敗了
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //緩存中不存在,則從數據庫里進行數據查詢
           shop = getById(id);
 
            //數據庫里不存在,返回404
            if (null == shop) {
                // 緩存空對象
                stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, "", 2, TimeUnit.MINUTES);
                return null;
            }
            //數據庫里存在,則將信息寫入Redis
            try {
                String shopJSon = objectMapper.writeValueAsString(shop);
                stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, shopJSon, 30, TimeUnit.MINUTES);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }catch (Exception e){
 
        }finally {
            // 釋放互斥鎖
            unLock(lockKey);
        }
 
        //返回
        return shop;
 
    }

6.2 邏輯過期實現

邏輯過期不設置TTL

在這里插入圖片描述

代碼實現

1
2
3
4
5
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

由于是熱點key,所以key基本都是手動導入到緩存,代碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
   * 邏輯過期時間對象寫入緩存
   * @param id
   * @param expireSeconds
   */
  public void saveShopToRedis(Long id,Long expireSeconds){
      // 查詢店鋪數據
      Shop shop = getById(id);
      // 封裝為邏輯過期
      RedisData redisData = new RedisData();
      redisData.setData(shop);
      redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
      // 寫入Redis
      stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));
  }

邏輯過期代碼實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
     * 緩存擊穿:邏輯過期解決
     * @param id
     * @return
     * @throws InterruptedException
     */
    public Shop queryWithPassLogicalExpire(Long id) throws InterruptedException {
        //1. 從Redis查詢商鋪緩存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);
 
        //2. 判斷緩存中數據是否存在
        if (StringUtil.isNullOrEmpty(cacheShop)) {
            // 3. 不存在
            return null;
        }
        // 4. 存在,判斷是否過期
        RedisData redisData = JSONUtil.toBean(cacheShop, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
 
        // 5. 判斷是否過期
        if (expireTime.isAfter(LocalDateTime.now())){
            // 5.1 未過期
            return shop;
        }
        // 5.2 已過期
        String lockKey = "lock:shop:"+id;
        boolean flag = tryLock(lockKey);
        if (flag){
            // TODO 獲取鎖成功,開啟獨立線程,實現緩存重建,建議使用線程池去做
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    // 重建緩存
                    this.saveShopToRedis(id,1800L);
                }catch (Exception e){
                     
                }finally {
                    // 釋放鎖
                    unLock(lockKey);
                }
              
            });
 
        }
        // 獲取鎖失敗,返回過期的信息
        return shop;
    }
 
    /**
     * 線程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

到此這篇關于利用Redis進行數據緩存的項目實踐的文章就介紹到這了

分享到:

相關信息

系統教程欄目

欄目熱門教程

人氣教程排行

站長推薦

熱門系統下載