redis——緩存雙寫一致性問題

image

緩存雙寫一致性

如果redis中有數據

  • 需要和數據庫中的值相同

如果redis中無數據

  • 數據庫中的值是最新值,且準備回寫redis

    • 緩存按照操作分

      • 只讀緩存

      • 讀寫緩存

        • 同步直寫策略

          • 寫數據庫後也同步寫 redis 緩存,緩存中的數據和數據中的一致
          • 對於讀寫緩存來說,要想保證緩存和數據庫中的數據一致
        • 異步緩寫策略

          • 正常業務運行中,mysql數據變動了,但是可以在業務上容許出現一定時間後才作用於redis,比如倉庫、物流系統
          • 異常情況出現了,不得不講失敗的動作重新修補,有可能需要藉助kafka或者RabbitMQ等消息中間件,實現重寫重試

image

採用雙檢加鎖策略

  • 多個線程同時去查詢數據庫的這條數據,就在第一個查詢數據的請求上使用一個互斥鎖來鎖住他。

  • 其他線程獲取不到鎖就一直等待,等第一個線程查詢到了數據,然後做了緩存

  • 後面的線程進來發現已經有了緩存,就直接走緩存

package com.lv.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lv.User;
import com.lv.mapper.UserMapper;
import com.lv.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author 曉風殘月Lx
 * @date 2023/3/27 12:39
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    public static final String CACHE_KEY_USER = "user:";
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;


    /**
     *  業務邏輯沒有寫錯,對於小廠中廠(QPS《=1000)可以使用,但是大廠不行
     * @param id
     * @return
     */
    public User findUserById1(Long id){
        User user = null;

        String key = CACHE_KEY_USER + id;

        // 1.先從redis中查詢,如果有直接返回結果,沒有再去查詢 mysql
        user = (User) redisTemplate.opsForValue().get(key);

        if (user == null){
            // 2. redis中沒有,查詢mysql
             user = userMapper.selectById(id);
             if (user == null){
                 // 3.1 redis + mysql 都無數據
                 // 具體細化,防止多次穿透,業務規定,記錄下導致穿透的這個key回寫redis
                 return user;
             }else {
                 // 3.2 mysql有,需要回寫到redis,保證下一次的緩存命中率
                 redisTemplate.opsForValue().set(key,user);
             }
        }
        return user;
    }

    /**
     * 加強補充,避免突然key失效了,打爆mysql,做一下預防,儘量不出現擊穿的情況
     * @param id
     * @return
     */
    public User findUserById2(Long id){
        User user = null;
        String key = CACHE_KEY_USER + id;

        // 1.先從redis裏面查詢,如果有直接返回結果,如果沒有再去查詢mysql
        // 第一次查詢redis,加鎖前
        user = (User) redisTemplate.opsForValue().get(key);
        if (user == null){
            // 2.對於高QPS的優化,進來就先加鎖,保證一個請求操作,讓外面的redis等待一下,避免擊穿mysql
            synchronized (UserServiceImpl.class){
                // 第二次查詢redis,加鎖後
                user = (User) redisTemplate.opsForValue().get(key);
                // 3. 二次查redis還是null,可以去查mysql了(mysql默認有數據)
                if (user == null) {
                    //4 查詢mysql拿數據(mysql默認有數據)
                    user = userMapper.selectById(id);
                    if (user == null) {
                        return null;
                    } else {
                        // 5. mysql裏面有數據的,需要回寫redis,完成數據一致性的同步工作
                        redisTemplate.opsForValue().setIfAbsent(key, user, 7L, TimeUnit.DAYS);
                    }
                }
            }
        }
        return user;
    }
}

數據庫和緩存一致性的幾種更新策略

目的

  • 達到最終一致性
    給緩存設置過期時間,定期清理緩存並回寫,是保證最終一致性的解決方案。
    我們可以對存入緩存的數據設置過期時間,所有的寫操作以數據庫爲準,對緩存操作只是盡最大努力即可。也就是說如果數據庫寫成功,緩存更新失敗,那麼只要到達過期時間,則後面的讀請求自然會從數據庫中讀取新值然後回填緩存,達到一致性,切記,要以mysql的數據庫寫入庫爲準。

可以停機的情況

基本上怎麼處理都可以

  • 掛牌報錯
  • 凌晨升級
  • 服務降級
  • 溫馨提示
  • 最好單線程操作(對於重量級的數據操作)

不可停機的情況

  • 先更新數據庫, 在更新緩存

    • 異常情況1: 線程1先更新數據庫, 然後更新緩存出錯了, 則會導致後續線程讀取到舊數據
    • 異常情況2: 高併發情況下, 數據A, 線程1 更新成B, 線程2 更新成C, 有可能更新緩存先更新C, 在更新B, 導致數據庫是C, redis是B ,不一致了
  • 先更新緩存, 在更新數據庫

    • 同上同樣問題
    • 一般數據庫的數據爲準, 把它做爲底庫
  • * 先刪除緩存, 在更新數據庫

    • 異常情況: 線程1 先刪除了緩存, 然後更新數據庫, 可能在這個過程中 有線程2 出現讀取到了髒數據 又寫回了緩存

    • 解決:

      • 延時雙刪

        • 線程1 刪除了緩存, 然後更新數據庫, 可能在這個過程中 有線程2 出現讀取到了髒數據 又寫回了緩存, 線程1 更新完後再刪除一次(保證我是更新完之後在刪除)
        • 爲什麼要延遲: 有可能第二次刪除, 另一個線程正準備把髒數據給寫入, 所以需要延遲一會(延遲的時間是 線程2 讀到舊數據並寫入的時間)
      • 延時雙刪 又會帶來一些問題

        • 延時的時間不好把控

          • 1、測試業務耗時時間, 加上一個幾百毫秒這種
          • 2、看門狗機制
        • 延時會帶來一些性能問題, 降低吞吐量

          • 異步去做第二次刪除 CompleteFuture
  • ** 先更新數據庫, 在刪除緩存

    • 異常情況:

      • 1、更新數據庫, 刪除緩存異常了
      • 2、線程1更新數據庫, 此時還沒刪除 線程2進行讀取, 讀取到的是舊值
    • 如何解決呢

      • 針對1 如果緩存異常了, 沒辦法 只能保證最終一致性 放MQ, 讓它去保證最終一致性
      • 針對2 無法避免, 但是隻有少部分線程讀取到舊值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章