緩存雙寫一致性
如果redis中有數據
- 需要和數據庫中的值相同
如果redis中無數據
-
數據庫中的值是最新值,且準備回寫redis
-
緩存按照操作分
-
只讀緩存
-
讀寫緩存
-
同步直寫策略
- 寫數據庫後也同步寫 redis 緩存,緩存中的數據和數據中的一致
- 對於讀寫緩存來說,要想保證緩存和數據庫中的數據一致
-
異步緩寫策略
- 正常業務運行中,mysql數據變動了,但是可以在業務上容許出現一定時間後才作用於redis,比如倉庫、物流系統
- 異常情況出現了,不得不講失敗的動作重新修補,有可能需要藉助kafka或者RabbitMQ等消息中間件,實現重寫重試
-
-
-
採用雙檢加鎖策略
-
多個線程同時去查詢數據庫的這條數據,就在第一個查詢數據的請求上使用一個互斥鎖來鎖住他。
-
其他線程獲取不到鎖就一直等待,等第一個線程查詢到了數據,然後做了緩存
-
後面的線程進來發現已經有了緩存,就直接走緩存
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 無法避免, 但是隻有少部分線程讀取到舊值
-