高并发系统缓存更新策略:四种方案深度剖析与最优选择

在高并发分布式系统中,缓存(如Redis)是提升读取性能的核心组件,但缓存与数据库的一致性问题一直是架构设计的难点。本文将深度分析「先写缓存/先写数据库」「先删缓存/先写数据库」四类经典更新策略,结合高并发场景的痛点,给出工业级最优解决方案。

一、核心问题背景

缓存更新的本质是平衡数据一致性、系统性能和实现复杂度

  • 一致性:缓存数据需尽可能与数据库保持一致,避免「脏数据」;

  • 性能:更新操作不能引入过高开销,需适配高并发场景;

  • 复杂度:方案需工程落地简单,避免过度设计。

下文将逐一分析四种主流更新策略的优劣。

二、四种缓存更新策略全解析

1. 先写缓存,再写数据库

1.1. 核心流程





接收更新请求
写入缓存
缓存写入成功?
写入数据库
返回失败
数据库写入成功?
返回成功
返回失败

1.2. 核心问题
  • 数据不一致风险极高:缓存写入成功但数据库写入失败(如网络抖动、数据库宕机),会导致缓存存在脏数据,数据库无对应数据,且脏数据会长期存在(除非手动清理)。

  • 高并发下覆盖问题:多线程场景下,后写入的缓存可能被先写入的数据库操作覆盖,最终缓存与数据库数据永久不一致。

示例:

  • 线程A:写缓存(值=100)→ 等待写数据库

  • 线程B:写缓存(值=200)→ 写数据库(值=200)

  • 线程A:写数据库(值=100)

最终结果:数据库=100,缓存=200,数据完全不一致。

1.3. 适用场景

几乎无适用场景,仅允许极端数据不一致的非核心场景可临时使用,高并发下完全不可选

2. 先写数据库,再写缓存

2.1. 核心流程





接收更新请求
写入数据库
数据库写入成功?
写入缓存
返回失败
缓存写入成功?
返回成功
返回失败

2.2. 核心问题
  • 高并发更新覆盖:这是该策略最致命的问题,会导致永久脏数据。

示例:

  • 线程1:写数据库(值=100)→ 准备写缓存

  • 线程2:写数据库(值=200)→ 写缓存(值=200)

  • 线程1:写缓存(值=100)

最终结果:数据库=200,缓存=100,数据不一致且无法自动恢复。

  • 无效缓存写入:很多场景下,刚写入的缓存数据短期内不会被读取,额外的写缓存操作浪费系统资源。

  • 缓存写入失败风险:缓存集群宕机或网络问题导致缓存写入失败,同样会引发数据不一致。

2.3. 适用场景

仅适用于低并发、读极少更新的场景(如静态配置更新),高并发下不推荐。

3. 先删缓存,再写数据库

3.1. 核心流程





接收更新请求
删除缓存
缓存删除成功?
写入数据库
返回失败
数据库写入成功?
返回成功
返回失败

3.2. 核心问题
  • 缓存击穿+脏数据:高并发读场景下,会出现「缓存miss→读旧数据→回写缓存」的致命问题。

示例:

  • 线程1:删除缓存 → 准备写数据库(新值=200)

  • 线程2:读取缓存(miss)→ 从数据库读旧值=100 → 写入缓存

  • 线程1:写数据库(新值=200)

最终结果:数据库=200,缓存=100,形成永久脏数据(直到缓存过期或手动删除)。

  • 虽可通过「延迟双删」(删缓存→写数据库→延迟1秒再删缓存)缓解,但无法100%解决(延迟时间难以精准控制),且增加了实现复杂度。
3.3. 适用场景

仅适用于低并发读、更新频率极低的场景,高并发下需谨慎使用。

4. 先写数据库,再删缓存(最优方案)

4.1. 核心流程





接收更新请求
写入数据库
数据库写入成功?
删除缓存
返回失败
缓存删除成功?
返回成功
记录日志+异步重试

4.2. 核心优势
  • 数据不一致风险最低

    • 即使缓存删除失败,后续读取请求会从数据库读取最新数据并回写缓存,脏数据仅临时存在(直到缓存过期);

    • 高并发下,即使删除缓存延迟,读取请求最多读到「旧缓存」,但数据库已存新数据,后续缓存会被自动修正。

  • 实现简单:无需复杂的锁、延迟逻辑,工程落地成本低。

  • 性能损耗小:删除缓存是轻量操作(比写缓存快),避免了「无效写缓存」的开销。

4.3. 潜在问题及解决方案
潜在问题 解决方案
数据库写成功,缓存删失败 → 缓存存旧数据 1. 给缓存设置过期时间 (兜底,过期后自动失效); 2. 增加「删除缓存重试机制」(如消息队列异步重试); 3. 监控缓存删除失败情况,人工兜底
缓存删除后大量请求直接打数据库 → 数据库压力陡增(缓存击穿) 1. 给热点key加「互斥锁」(同一时间仅一个线程读库回写缓存); 2. 热点数据设置「永不过期」+ 主动更新; 3. 布隆过滤器拦截无效请求
4.4. 适用场景

所有高并发场景的首选方案,是工业界主流的缓存更新策略(如电商、金融、社交等核心系统均采用)。

三、最优方案代码实现(Java + Redis)

以「更新用户余额」为例,展示「先写数据库,再删缓存」的核心实现,包含缓存重试、互斥锁防击穿等关键细节:

java 复制代码
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;

@Slf4j
@Service
public class UserBalanceService {

    @Resource
    private UserBalanceMapper userBalanceMapper; // 数据库操作Mapper
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private RetryQueueService retryQueueService; // 异步重试队列服务

    // 缓存key前缀
    private static final String CACHE_KEY_USER_BALANCE = "user:balance:";
    // 锁key前缀
    private static final String LOCK_KEY_USER_BALANCE = "lock:user:balance:";
    // 缓存过期时间(30分钟)
    private static final long CACHE_EXPIRE_TIME = 30L;
    // 锁过期时间(5秒,防止死锁)
    private static final long LOCK_EXPIRE_TIME = 5L;

    /**
     * 更新用户余额:先写数据库,再删缓存
     * @param userId 用户ID
     * @param newBalance 新余额
     */
    public void updateUserBalance(Long userId, Long newBalance) {
        // 1. 先更新数据库(保证数据持久化)
        boolean dbUpdateSuccess = userBalanceMapper.updateBalance(userId, newBalance);
        if (!dbUpdateSuccess) {
            log.error("更新用户{}余额失败,数据库写入失败", userId);
            throw new RuntimeException("余额更新失败");
        }

        // 2. 再删除缓存(核心步骤)
        String cacheKey = CACHE_KEY_USER_BALANCE + userId;
        try {
            redisTemplate.delete(cacheKey);
            log.info("删除用户{}余额缓存成功,key:{}", userId, cacheKey);
        } catch (Exception e) {
            // 缓存删除失败,记录日志并加入异步重试队列
            log.error("删除用户{}余额缓存失败,key:{},原因:{}", userId, cacheKey, e.getMessage());
            retryQueueService.addRetryTask(cacheKey, "DELETE"); // 加入重试队列
        }
    }

    /**
     * 读取用户余额:缓存优先,加互斥锁防击穿
     * @param userId 用户ID
     * @return 用户余额
     */
    public Long getUserBalance(Long userId) {
        String cacheKey = CACHE_KEY_USER_BALANCE + userId;
        // 1. 先查缓存
        Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
        if (cacheValue != null) {
            return (Long) cacheValue;
        }

        // 2. 缓存miss,加互斥锁防止缓存击穿
        String lockKey = LOCK_KEY_USER_BALANCE + userId;
        Long dbValue = null;
        try {
            // 尝试获取锁(SET NX EX)
            boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_EXPIRE_TIME, TimeUnit.SECONDS);
            if (lockSuccess) {
                // 3. 获取锁成功,读取数据库
                dbValue = userBalanceMapper.getBalance(userId);
                if (dbValue != null) {
                    // 4. 回写缓存(设置过期时间)
                    redisTemplate.opsForValue().set(cacheKey, dbValue, CACHE_EXPIRE_TIME, TimeUnit.MINUTES);
                    log.info("回写用户{}余额缓存成功,key:{},值:{}", userId, cacheKey, dbValue);
                }
            } else {
                // 未获取到锁,等待50ms后重试
                log.info("用户{}余额缓存miss,未获取到锁,等待重试", userId);
                TimeUnit.MILLISECONDS.sleep(50);
                return getUserBalance(userId); // 递归重试
            }
        } catch (InterruptedException e) {
            log.error("获取用户{}余额锁失败,原因:{}", userId, e.getMessage());
            Thread.currentThread().interrupt();
        } finally {
            // 释放锁(无论是否成功,都释放)
            redisTemplate.delete(lockKey);
        }
        return dbValue;
    }
}

补充说明

  • RetryQueueService:异步重试队列服务,可基于RabbitMQ/RocketMQ实现,核心逻辑是对删除失败的缓存key进行3次重试(间隔1s、3s、5s);

  • 数据库操作:实际项目中建议使用事务保证数据库写入的原子性;

  • 监控:需监控缓存删除失败率、数据库更新成功率,及时发现异常。

四、总结

  1. 最优策略:高并发系统中优先选择「先写数据库,再删缓存」,这是平衡一致性、性能和复杂度的最优解;

  2. 核心兜底措施:给缓存设置过期时间(解决删除失败的脏数据问题),给热点key加互斥锁(解决缓存击穿问题);

  3. 避坑提醒:「写缓存」类策略(先写缓存/后写缓存)高并发下易导致永久脏数据,「先删缓存再写数据库」易引发缓存击穿,均不推荐作为核心策略。

通过以上方案,可在高并发场景下最大限度保证缓存与数据库的一致性,同时兼顾系统性能和工程实现的简洁性。

相关推荐
sxhcwgcy1 小时前
Spring Boot 整合 log4j2 日志配置教程
spring boot·单元测试·log4j
大阿明1 小时前
Spring BOOT 启动参数
java·spring boot·后端
hutengyi1 小时前
Spring Boot 实战篇(四):实现用户登录与注册功能
java·spring boot·后端
itjinyin1 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端
回到原点的码农2 小时前
Spring Boot 热部署
java·spring boot·后端
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于springBoot的考试成绩管理系统为例,包含答辩的问题和答案
java·spring boot·后端
骇客野人2 小时前
XXL-JOB集成到springBoot手册
java·数据库·spring boot
guestsun4 小时前
SpringBoot七大事务失效场景分析
java·spring boot·mybatis