Redis緩存更新策略詳解
1. 主動(dòng)更新-4種核心緩存更新策略
核心原則:根據(jù)業(yè)務(wù)的 “讀寫比例”“一致性要求”“性能要求” 選擇策略,優(yōu)先保證數(shù)據(jù)一致性,其次優(yōu)化性能。
1.1. Cache-Aside(旁路緩存)
這是最常用、最經(jīng)典的策略,也叫 “先查緩存,再查數(shù)據(jù)庫,更新時(shí)先更庫再刪緩存”。
- 讀取數(shù)據(jù):先查詢緩存,命中則直接返回;未命中則查詢數(shù)據(jù)庫,將結(jié)果寫入緩存并返回。
- 更新數(shù)據(jù):先更新數(shù)據(jù)庫,再刪除緩存(而非更新緩存)。
@Service
public class UserServiceCacheAside {
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
// 1. 查詢緩存
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user; //緩存命中,直接返回
}
// 2. 緩存未命中,查詢數(shù)據(jù)庫
user = userMapper.selectById(id);
if (user != null) {
// 3. 將數(shù)據(jù)庫結(jié)果寫入緩存(設(shè)置過期時(shí)間)
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
}
return user;
}
public void updateUser(User user) {
// 1. 先更新數(shù)據(jù)庫
userMapper.updateById(user);
// 2. 再刪除緩存(而非更新緩存,避免并發(fā)問題)
String cacheKey = "user:" + user.getId();
redisTemplate.delete(cacheKey);
}
}- 優(yōu)點(diǎn)
- 邏輯簡單,易于實(shí)現(xiàn);適合讀多寫少的業(yè)務(wù)場景;
- 避免 “更新緩存” 帶來的并發(fā)數(shù)據(jù)不一致問題(比如兩個(gè)線程同時(shí)更新,緩存可能存舊值)。
- 缺點(diǎn)
- 緩存未命中時(shí)會(huì)有 “緩存穿透” 的風(fēng)險(xiǎn)(可通過布隆過濾器解決);
- 數(shù)據(jù)庫更新后、緩存刪除前,若有讀請求,可能讀到舊值(概率極低,可接受)。
1.2. Write-Through(寫穿透)
更新時(shí) “先更緩存,再更數(shù)據(jù)庫”,讀取時(shí)只查緩存(緩存一定有最新數(shù)據(jù))。
- 讀取數(shù)據(jù):直接從緩存讀取,緩存必然命中(因?yàn)楦聲r(shí)同步寫緩存);
- 更新數(shù)據(jù):先更新緩存;再由緩存同步更新數(shù)據(jù)庫(通常是緩存框架自動(dòng)完成)。
@Service
public class UserWriteThroughService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
/**
* 新增用戶(Write Through:先寫緩存,再寫數(shù)據(jù)庫)
* 加事務(wù)保證緩存和數(shù)據(jù)庫要么都成功,要么都失敗
*/
@Transactional(rollbackFor = Exception.class)
public void addUser(User user) {
// 1. 先寫緩存(設(shè)置過期時(shí)間,兜底)
String cacheKey = "user:write_through:" + user.getId();
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
// 2. 同步寫數(shù)據(jù)庫(若數(shù)據(jù)庫寫入失敗,事務(wù)回滾,緩存也會(huì)被刪除)
int insertCount = userMapper.insertUser(user);
if (insertCount <= 0) {
// 數(shù)據(jù)庫寫入失敗,主動(dòng)刪除緩存,避免臟數(shù)據(jù)
redisTemplate.delete(cacheKey);
throw new RuntimeException("新增用戶到數(shù)據(jù)庫失敗");
}
}
/**
* 更新用戶(Write Through:先更新緩存,再更新數(shù)據(jù)庫)
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(User user) {
String cacheKey = "user:write_through:" + user.getId();
// 1. 先更新緩存(若緩存不存在,先查數(shù)據(jù)庫再更新,保證緩存有數(shù)據(jù))
User oldUser = (User) redisTemplate.opsForValue().get(cacheKey);
if (oldUser == null) {
oldUser = userMapper.selectUserById(user.getId());
if (oldUser == null) {
throw new RuntimeException("用戶不存在,ID: " + user.getId());
}
}
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
// 2. 同步更新數(shù)據(jù)庫
int updateCount = userMapper.updateUser(user);
if (updateCount <= 0) {
// 數(shù)據(jù)庫更新失敗,回滾緩存(恢復(fù)舊值)
redisTemplate.opsForValue().set(cacheKey, oldUser, 1, TimeUnit.HOURS);
throw new RuntimeException("更新用戶到數(shù)據(jù)庫失敗,ID: " + user.getId());
}
}
/**
* 讀取用戶(Write Through:只查緩存,不查數(shù)據(jù)庫)
*/
public User getUserById(Long userId) {
String cacheKey = "user:write_through:" + userId;
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user == null) {
// 理論上 Write Through 策略下緩存一定有數(shù)據(jù),此處僅做異常兜底
user = userMapper.selectUserById(userId);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
}
}
return user;
}
}- 優(yōu)點(diǎn)
- 讀取性能極高,無需訪問數(shù)據(jù)庫;數(shù)據(jù)一致性強(qiáng),緩存與數(shù)據(jù)庫同步更新。
- 缺點(diǎn)
- 寫入性能低(每次寫都要操作緩存 + 數(shù)據(jù)庫);
- 數(shù)據(jù)庫寫入失敗會(huì)導(dǎo)致緩存與數(shù)據(jù)庫不一致(需加事務(wù) / 重試)。
1.3. Write-Behind(寫回)
也叫 “延遲更新”,更新時(shí)只更緩存,不立即更數(shù)據(jù)庫,而是等緩存過期 / 淘汰時(shí),再批量同步到數(shù)據(jù)庫。
- 更新流程:更新緩存,并標(biāo)記緩存為 “臟數(shù)據(jù)”;緩存過期 / 被淘汰時(shí),異步將 “臟數(shù)據(jù)” 批量寫入數(shù)據(jù)庫。
- 讀取流程:與 Cache Aside 一致(先查緩存,未命中查庫)。
/**
* 業(yè)務(wù)場景:用戶點(diǎn)贊數(shù)(寫多讀少,允許短時(shí)間緩存與數(shù)據(jù)庫不一致)
*/
@Service
public class LikeCountWriteBackService {
// Redis Key前綴:用戶點(diǎn)贊數(shù)緩存
private static final String CACHE_LIKE_COUNT_KEY = "like:count:";
// Redis Key:臟數(shù)據(jù)標(biāo)記(記錄需要同步到數(shù)據(jù)庫的用戶ID)
private static final String DIRTY_DATA_SET_KEY = "like:dirty:user:ids";
// 緩存過期時(shí)間(兜底,避免臟數(shù)據(jù)永久不刷新)
private static final long CACHE_EXPIRE_TIME = 24 * 60 * 60;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private LikeCountMapper likeCountMapper;
/**
* 核心操作:更新用戶點(diǎn)贊數(shù)(只更緩存,標(biāo)記臟數(shù)據(jù))
* @param userId 用戶ID
* @param increment 增加的點(diǎn)贊數(shù)(正數(shù))
*/
public void updateLikeCount(Long userId, int increment) {
String cacheKey = CACHE_LIKE_COUNT_KEY + userId;
try {
// 1. 原子更新Redis緩存中的點(diǎn)贊數(shù)(避免并發(fā)問題)
redisTemplate.opsForValue().increment(cacheKey, increment);
// 設(shè)置緩存過期時(shí)間(兜底)
redisTemplate.expire(cacheKey, CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
// 2. 將用戶ID加入臟數(shù)據(jù)集(標(biāo)記為需要同步到數(shù)據(jù)庫)
// 使用ZSet存儲(chǔ),score為當(dāng)前時(shí)間戳,便于后續(xù)按時(shí)間篩選
redisTemplate.opsForZSet().add(DIRTY_DATA_SET_KEY, userId, System.currentTimeMillis());
} catch (Exception e) {
// 異常時(shí)降級:直接更新數(shù)據(jù)庫(避免數(shù)據(jù)丟失)
fallbackUpdateDb(userId, increment);
}
}
/**
* 讀取用戶點(diǎn)贊數(shù)(先查緩存,未命中查庫并回填緩存)
* @param userId 用戶ID
* @return 最新點(diǎn)贊數(shù)
*/
public Long getLikeCount(Long userId) {
String cacheKey = CACHE_LIKE_COUNT_KEY + userId;
// 1. 先查緩存
Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (cacheValue != null) {
return Long.parseLong(cacheValue.toString());
}
// 2. 緩存未命中:查數(shù)據(jù)庫
Long dbCount = likeCountMapper.selectLikeCountByUserId(userId);
if (dbCount == null) {
dbCount = 0L;
}
// 3. 回填緩存(并標(biāo)記為臟數(shù)據(jù),避免后續(xù)同步時(shí)覆蓋)
redisTemplate.opsForValue().set(cacheKey, dbCount);
redisTemplate.expire(cacheKey, CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
redisTemplate.opsForZSet().add(DIRTY_DATA_SET_KEY, userId, System.currentTimeMillis());
return dbCount;
}
/**
* 核心異步任務(wù):定時(shí)將臟數(shù)據(jù)同步到數(shù)據(jù)庫(Write Back核心)
* 定時(shí)規(guī)則:每5分鐘執(zhí)行一次(可根據(jù)業(yè)務(wù)調(diào)整)
*/
@Scheduled(cron = "0 */5 * * * ?")
@Transactional(rollbackFor = Exception.class)
public void syncDirtyDataToDb() {
ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
// 1. 批量獲取臟數(shù)據(jù)集中的用戶ID(最多取1000條,避免單次同步過多)
Set<Object> dirtyUserIds = zSetOps.range(DIRTY_DATA_SET_KEY, 0, 999);
if (dirtyUserIds == null || dirtyUserIds.isEmpty()) {
return;
}
// 2. 遍歷臟數(shù)據(jù),同步到數(shù)據(jù)庫
List<Long> failUserIds = new ArrayList<>(); // 記錄同步失敗的用戶ID
for (Object userIdObj : dirtyUserIds) {
Long userId = Long.parseLong(userIdObj.toString());
String cacheKey = CACHE_LIKE_COUNT_KEY + userId;
try {
// 2.1 獲取緩存中的最新點(diǎn)贊數(shù)
Object cacheCountObj = redisTemplate.opsForValue().get(cacheKey);
if (cacheCountObj == null) {
zSetOps.remove(DIRTY_DATA_SET_KEY, userId); // 移除臟數(shù)據(jù)標(biāo)記
continue;
}
Long cacheCount = Long.parseLong(cacheCountObj.toString());
// 2.2 更新數(shù)據(jù)庫
likeCountMapper.updateLikeCountByUserId(userId, cacheCount);
// 2.3 同步成功:移除臟數(shù)據(jù)標(biāo)記
zSetOps.remove(DIRTY_DATA_SET_KEY, userId);
} catch (Exception e) {
failUserIds.add(userId); // 記錄失敗ID,后續(xù)重試
}
}
// 3. 處理同步失敗的用戶ID(簡單重試:重新加入臟數(shù)據(jù)集)
if (!failUserIds.isEmpty()) {
for (Long failUserId : failUserIds) {
zSetOps.add(DIRTY_DATA_SET_KEY, failUserId, System.currentTimeMillis());
}
}
}
/**
* 降級策略:緩存更新失敗時(shí),直接更新數(shù)據(jù)庫
*/
private void fallbackUpdateDb(Long userId, int increment) {
try {
Long currentCount = likeCountMapper.selectLikeCountByUserId(userId);
if (currentCount == null) {
currentCount = 0L;
}
likeCountMapper.updateLikeCountByUserId(userId, currentCount + increment);
} catch (Exception e) {
// 可進(jìn)一步接入消息隊(duì)列/告警,保證數(shù)據(jù)不丟失
}
}
}- 優(yōu)點(diǎn):
- 寫入性能極高(只需操作緩存,數(shù)據(jù)庫異步批量更新);
- 適合寫多讀少的場景(如計(jì)數(shù)器、點(diǎn)贊數(shù))。
- 缺點(diǎn)
- 數(shù)據(jù)一致性差(緩存未同步到數(shù)據(jù)庫時(shí),服務(wù)宕機(jī)會(huì)丟失數(shù)據(jù));
- 實(shí)現(xiàn)復(fù)雜(需處理臟數(shù)據(jù)標(biāo)記、異步同步、數(shù)據(jù)恢復(fù))。
1.4. 刷新過期(Refresh-Ahead)
本質(zhì)是 Cache-Aside(旁路緩存)的優(yōu)化 / 增強(qiáng)版
- 更新流程:與 Cache Aside 一致(先更新數(shù)據(jù)庫,再刪除緩存)。
- 讀取流程:先查詢緩存,未命中則查詢數(shù)據(jù)庫,將結(jié)果寫入緩存并返回;命中則檢查緩存剩余過期時(shí)間,若剩余過期時(shí)間 ≥ 閾值:直接返回緩存中的舊值;若剩余過期時(shí)間 < 閾值:異步觸發(fā)緩存刷新(后臺(tái)查數(shù)據(jù)庫最新數(shù)據(jù) → 重寫緩存并重置 TTL),當(dāng)前請求仍返回緩存舊值。
/**
* Refresh-Ahead(提前刷新)策略實(shí)現(xiàn)示例
* 核心邏輯:訪問緩存時(shí)檢查剩余過期時(shí)間,若小于閾值則異步刷新緩存,當(dāng)前請求仍返回舊值
*/
@Service
public class RefreshAheadCacheService {
// 緩存過期時(shí)間(示例:30分鐘)
private static final long CACHE_TTL_SECONDS = 30 * 60;
// Refresh-Ahead 觸發(fā)閾值(過期時(shí)間剩余10%時(shí)觸發(fā),示例:3分鐘)
private static final long REFRESH_THRESHOLD_SECONDS = CACHE_TTL_SECONDS / 10;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private ProductCategoryMapper productCategoryMapper;
/**
* 獲取商品分類數(shù)據(jù)(核心Refresh-Ahead邏輯)
*/
public ProductCategory getCategoryWithRefreshAhead(Long categoryId) {
String cacheKey = "category:" + categoryId;
ValueOperations<String, Object> valueOps = redisTemplate.opsForValue();
// 1. 先查緩存
ProductCategory category = (ProductCategory) valueOps.get(cacheKey);
if (category == null) {
// 緩存未命中:查庫 + 寫入緩存(常規(guī)Cache Aside邏輯)
category = productCategoryMapper.selectById(categoryId);
if (category != null) {
redisTemplate.opsForValue().set(cacheKey, category, CACHE_TTL_SECONDS, TimeUnit.SECONDS);
}
return category;
}
// 2. 緩存命中:檢查剩余過期時(shí)間,判斷是否觸發(fā)Refresh-Ahead
Long remainExpireSeconds = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
// 剩余時(shí)間小于閾值 且 緩存未過期(避免已過期的情況)
if (remainExpireSeconds != null && remainExpireSeconds > 0
&& remainExpireSeconds < REFRESH_THRESHOLD_SECONDS) {
// 3. 異步刷新緩存(不阻塞當(dāng)前請求)
asyncRefreshCategoryCache(categoryId, cacheKey);
}
// 當(dāng)前請求仍返回舊值,異步刷新不影響響應(yīng)速度
return category;
}
/**
* 異步刷新緩存(核心:不阻塞主線程)
*/
@Async("refreshExecutor") // 指定自定義異步線程池(避免用默認(rèn)線程池)
public void asyncRefreshCategoryCache(Long categoryId, String cacheKey) {
try {
// 1. 從數(shù)據(jù)庫查詢最新數(shù)據(jù)
ProductCategory latestCategory = productCategoryMapper.selectById(categoryId);
if (latestCategory != null) {
// 2. 重新設(shè)置緩存(覆蓋舊值 + 重置過期時(shí)間)
redisTemplate.opsForValue().set(cacheKey, latestCategory, CACHE_TTL_SECONDS, TimeUnit.SECONDS);
}
} catch (Exception e) {
System.err.println("Refresh-Ahead刷新緩存失?。篕ey=" + cacheKey + ",原因:" + e.getMessage());
}
}
}線程池配置
@Configuration
@EnableAsync // 開啟異步功能
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor refreshExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("cache-refresh-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}- 優(yōu)點(diǎn)
- 保證數(shù)據(jù)一致性;避免 “緩存穿透” 的風(fēng)險(xiǎn)。
- 僅在 “緩存快過期且被訪問” 時(shí)刷新,比定時(shí)刷新更精準(zhǔn),避免無意義的全量刷新,節(jié)省資源;
- 缺點(diǎn)
- 需額外開發(fā) “過期時(shí)間預(yù)判”“異步刷新”“線程池管理” 邏輯,增加開發(fā)和維護(hù)成本;
- 觸發(fā)刷新后、異步更新完成前,客戶端仍會(huì)讀取到舊值(窗口極短,通??山邮埽?;
- 異步線程池耗盡、數(shù)據(jù)庫查詢失敗等,可能導(dǎo)致刷新失敗,需增加重試 / 日志監(jiān)控機(jī)制;
- 刷新閾值(如總 TTL 的 10%)設(shè)置不合理時(shí):過大→頻繁刷新浪費(fèi)資源,過小→刷新完成前緩存已過期。
2. 3種補(bǔ)充策略
2.1. Read-Through(讀穿透)
Read-Through是Cache Aside的 “封裝版 / 框架版”,核心是封裝緩存讀取邏輯,讓業(yè)務(wù)層聚焦業(yè)務(wù)而非緩存操作。
只需要將Cache Aside是緩存的邏輯封裝,所有業(yè)務(wù)復(fù)用即可。
- 優(yōu)點(diǎn) :
- 封裝性好,應(yīng)用代碼無需關(guān)心緩存邏輯
- 集中處理緩存加載,減少冗余代碼
- 適合只讀或讀多寫少的數(shù)據(jù)
- 缺點(diǎn):
- 緩存未命中時(shí)引發(fā)數(shù)據(jù)庫請求,可能導(dǎo)致數(shù)據(jù)庫負(fù)載增加
- 無法直接處理寫操作,需要與其他策略結(jié)合使用
- 需要額外維護(hù)一個(gè)緩存管理層
- 適用場景
- 讀操作頻繁的業(yè)務(wù)系統(tǒng)
- 需要集中管理緩存加載邏輯的應(yīng)用
- 復(fù)雜的緩存預(yù)熱和加載場景
2.2. 最終一致性(Eventual Consistency)
最終一致性策略基于分布式事件系統(tǒng)實(shí)現(xiàn)數(shù)據(jù)同步:
- 數(shù)據(jù)變更時(shí)發(fā)布事件到消息隊(duì)列
- 緩存服務(wù)訂閱相關(guān)事件并更新緩存
- 即使某些操作暫時(shí)失敗,最終系統(tǒng)也會(huì)達(dá)到一致狀態(tài) 首先定義數(shù)據(jù)變更事件:
@Data
@AllArgsConstructor
public class DataChangeEvent {
private String entityType;
private String entityId;
private String operation; // CREATE, UPDATE, DELETE
private String payload; // JSON格式的實(shí)體數(shù)據(jù)
}實(shí)現(xiàn)事件發(fā)布者:
@Component
public class DataChangePublisher {
@Autowired
private KafkaTemplate<String, DataChangeEvent> kafkaTemplate;
private static final String TOPIC = "data-changes";
public void publishChange(String entityType, String entityId, String operation, Object entity) {
try {
// 將實(shí)體序列化為JSON
String payload = new ObjectMapper().writeValueAsString(entity);
// 創(chuàng)建事件
DataChangeEvent event = new DataChangeEvent(entityType, entityId, operation, payload);
// 發(fā)布到Kafka
kafkaTemplate.send(TOPIC, entityId, event);
} catch (Exception e) {
log.error("Failed to publish data change event", e);
throw new RuntimeException("Failed to publish event", e);
}
}
}實(shí)現(xiàn)事件消費(fèi)者更新緩存:
@Component
@Slf4j
public class CacheUpdateConsumer {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final long CACHE_EXPIRATION = 30;
@KafkaListener(topics = "data-changes")
public void handleDataChangeEvent(DataChangeEvent event) {
try {
String cacheKey = buildCacheKey(event.getEntityType(), event.getEntityId());
switch (event.getOperation()) {
case "CREATE":
case "UPDATE":
// 解析JSON數(shù)據(jù)
Object entity = parseEntity(event.getPayload(), event.getEntityType());
// 更新緩存
redisTemplate.opsForValue().set(
cacheKey, entity, CACHE_EXPIRATION, TimeUnit.MINUTES);
log.info("Updated cache for {}: {}", cacheKey, event.getOperation());
break;
case "DELETE":
// 刪除緩存
redisTemplate.delete(cacheKey);
log.info("Deleted cache for {}", cacheKey);
break;
default:
log.warn("Unknown operation: {}", event.getOperation());
}
} catch (Exception e) {
log.error("Error handling data change event: {}", e.getMessage(), e);
// 失敗處理:可以將失敗事件放入死信隊(duì)列等
}
}
private String buildCacheKey(String entityType, String entityId) {
return entityType.toLowerCase() + ":" + entityId;
}
private Object parseEntity(String payload, String entityType) throws JsonProcessingException {
// 根據(jù)實(shí)體類型選擇反序列化目標(biāo)類
Class<?> targetClass = getClassForEntityType(entityType);
return new ObjectMapper().readValue(payload, targetClass);
}
private Class<?> getClassForEntityType(String entityType) {
switch (entityType) {
case "User": return User.class;
case "Product": return Product.class;
// 其他實(shí)體類型
default: throw new IllegalArgumentException("Unknown entity type: " + entityType);
}
}
}使用示例:
@Service
@Transactional
public class UserServiceEventDriven {
@Autowired
private UserRepository userRepository;
@Autowired
private DataChangePublisher publisher;
public User createUser(User user) {
// 1. 保存用戶到數(shù)據(jù)庫
User savedUser = userRepository.save(user);
// 2. 發(fā)布創(chuàng)建事件
publisher.publishChange("User", savedUser.getId().toString(), "CREATE", savedUser);
return savedUser;
}
public User updateUser(User user) {
// 1. 更新用戶到數(shù)據(jù)庫
User updatedUser = userRepository.save(user);
// 2. 發(fā)布更新事件
publisher.publishChange("User", updatedUser.getId().toString(), "UPDATE", updatedUser);
return updatedUser;
}
public void deleteUser(Long userId) {
// 1. 從數(shù)據(jù)庫刪除用戶
userRepository.deleteById(userId);
// 2. 發(fā)布刪除事件
publisher.publishChange("User", userId.toString(), "DELETE", null);
}
}- 優(yōu)點(diǎn) :
- 支持分布式系統(tǒng)中的數(shù)據(jù)一致性
- 削峰填谷,減輕系統(tǒng)負(fù)載峰值
- 服務(wù)解耦,提高系統(tǒng)彈性和可擴(kuò)展性
- 缺點(diǎn):
- 一致性延遲,只能保證最終一致性
- 實(shí)現(xiàn)和維護(hù)更復(fù)雜,需要消息隊(duì)列基礎(chǔ)設(shè)施
- 可能需要處理消息重復(fù)和亂序問題
- 適用場景
- 大型分布式系統(tǒng)
- 可以接受短暫不一致的業(yè)務(wù)場景
- 需要解耦數(shù)據(jù)源和緩存更新邏輯的系統(tǒng)
2.3. 過期淘汰(被動(dòng)更新)
- 本質(zhì):依賴 Redis 自身的過期策略(如 TTL 過期、LRU 淘汰)被動(dòng)更新緩存,配合核心策略使用(比如 Cache Aside 中給緩存設(shè) TTL,到期自動(dòng)淘汰舊數(shù)據(jù))。
- 特點(diǎn):不主動(dòng)更新,而是 “被動(dòng)清理舊數(shù)據(jù)”,是所有策略的基礎(chǔ)保障(避免緩存永久有效)。
簡單說:過期淘汰是 “策略目標(biāo)”(讓過期緩存被清理),惰性刪除 + 定期刪除是 “技術(shù)手段”。
2.3.1. 惰性刪除(Lazy Delete)
- 邏輯:當(dāng)用戶訪問某個(gè) key 時(shí),Redis 先檢查該鍵是否過期,若過期則立即刪除,不返回值;
- 定位:過期淘汰的核心實(shí)現(xiàn)手段之一,被動(dòng)觸發(fā),節(jié)省 CPU 資源(不用輪詢所有 key)。
2.3.2. 定期刪除(Periodic Delete)
- 邏輯:Redis 會(huì)啟動(dòng)一個(gè)后臺(tái)線程,每隔一段時(shí)間(默認(rèn) 100ms) 隨機(jī)抽取一部分過期 key 檢查,刪除其中已過期的;為了不阻塞主線程,每次檢查的時(shí)間和數(shù)量都有限制。
- 定位:補(bǔ)充惰性刪除的不足(避免過期 key 長期不被訪問,一直占用內(nèi)存),主動(dòng)但輕量化。
2.3.3. 兩者結(jié)合的原因
- 只靠惰性刪除:過期 key 若長期不被訪問,會(huì)一直占內(nèi)存;
- 只靠定期刪除:輪詢所有 key 會(huì)消耗大量 CPU,影響性能;
- 結(jié)合使用:既保證了過期 key 最終會(huì)被清理(定期刪除兜底),又避免了過度消耗 CPU(惰性刪除減少檢查),是 Redis 平衡性能和內(nèi)存的最優(yōu)方案。
3. 內(nèi)存淘汰
內(nèi)存淘汰屬于「兜底型緩存清理機(jī)制」,是指 Redis 達(dá)到最大內(nèi)存(maxmemory)時(shí),按照預(yù)設(shè)規(guī)則(如 LRU、LFU、隨機(jī)等)自動(dòng)淘汰部分緩存數(shù)據(jù),本質(zhì)是 “內(nèi)存管理手段”,而非 “保證數(shù)據(jù)一致性的更新策略”。
- 典型行為:Redis 內(nèi)存占滿后,淘汰最少使用的 key(LRU 策略);
- 核心目標(biāo):當(dāng) Redis 內(nèi)存達(dá)到
maxmemory上限時(shí),主動(dòng)淘汰部分鍵,釋放內(nèi)存以保證 Redis 能繼續(xù)接收新寫入; - 核心定位:目的是避免 Redis 內(nèi)存溢出,而非保證緩存與數(shù)據(jù)庫的一致性 —— 淘汰的可能是最新的、也可能是舊的緩存數(shù)據(jù),完全不考慮業(yè)務(wù)邏輯;
- 常見策略:
volatile-lru:淘汰設(shè)置了過期時(shí)間的鍵中,最近最少使用的;allkeys-lru:淘汰所有鍵中最近最少使用的;volatile-ttl:淘汰設(shè)置了過期時(shí)間的鍵中,剩余過期時(shí)間最短的;noeviction(默認(rèn)):不淘汰任何鍵,內(nèi)存滿時(shí)拒絕新寫入并返回錯(cuò)誤;
- 是否屬于:? 嚴(yán)格來說,不算 “業(yè)務(wù)層面的緩存更新策略”,而是 Redis 底層的內(nèi)存保護(hù)機(jī)制;但廣義上可視為 “被動(dòng)清理緩存的補(bǔ)充手段”。
簡單記:主動(dòng)更新是 “主動(dòng)做事”,過期淘汰是 “被動(dòng)兜底做事”,內(nèi)存淘汰是 “實(shí)在沒內(nèi)存了才清理”,前兩者屬于緩存更新策略范疇,后者是底層機(jī)制。
到此這篇關(guān)于Redis緩存更新策略的文章就介紹到這了,更多相關(guān)redis緩存更新策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis快照模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了redis快照模式的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Windows環(huán)境下安裝Redis并設(shè)置Redis開機(jī)自啟實(shí)踐
文章介紹了如何在Windows系統(tǒng)上安裝和配置Redis,包括下載Windows版本的Redis、設(shè)置連接密碼、啟動(dòng)Redis、設(shè)置開機(jī)自啟等步驟2025-12-12
Linux、Windows下Redis的安裝即Redis的基本使用詳解
Redis是一個(gè)基于內(nèi)存的key-value結(jié)構(gòu)數(shù)據(jù)庫,Redis 是互聯(lián)網(wǎng)技術(shù)領(lǐng)域使用最為廣泛的存儲(chǔ)中間件,這篇文章主要介紹了Linux、Windows下Redis的安裝即Redis的基本使用詳解,需要的朋友可以參考下2022-09-09
Redis SDS字符串與集合的底層實(shí)現(xiàn)原理解析
這篇文章給大家介紹了Redis SDS字符串與集合的底層實(shí)現(xiàn)原理解析,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2026-04-04

