在设计高并发系统中,详细分析下数据缓存怎么做

3 阅读6分钟

在高并发系统中,数据缓存的设计是提升性能和保障稳定性的核心环节。以下是缓存设计的系统性方案,涵盖架构、策略、一致性和容错等多个维度:


一、缓存架构设计

1. 多级缓存体系

  • 本地缓存(L1)

    • 技术选型:Caffeine(Java)、Guava Cache、Ehcache
    • 场景:高频访问的只读数据(如配置项、静态资源)
    • 特点:纳秒级访问速度,但容量有限,需设置合理淘汰策略(如LRU)
    • 缓存一致性问题解决方案:MQ广播消息、dubbo广播调用方式、Zookeeper Watcher机制、分布式任务调度的广播执行任务、广播功能特点的中间件或服务,都可以用来操作本地缓存或者本地方法
  • 分布式缓存(L2)

    • 技术选型:Redis Cluster、Memcached
    • 场景:共享数据(如用户会话、库存信息)
    • 特点:支持高可用、数据分片,但存在网络延迟
  • 客户端缓存(L0)

    • 技术选型:HTTP缓存头(ETag/Last-Modified)、LocalStorage
    • 场景:静态资源(JS/CSS/图片)
    • 特点:减少服务端请求,需设置Cache-Control策略
// Caffeine本地缓存示例
Cache<String, Object> localCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .build();

2. 缓存拓扑模式

  • 旁路缓存(Cache-Aside)

    • 读流程:先查缓存,未命中则查数据库并回填
    • 写流程:先更新数据库,再删除缓存(防脏读)
    • 适用场景:通用场景,需处理缓存与数据库一致性
  • 读写穿透(Read/Write-Through)

    • 读流程:缓存作为代理,未命中时由缓存加载数据
    • 写流程:缓存同步写入数据库(需缓存支持)
    • 适用场景:对一致性要求极高的场景(如金融系统)
  • 写回(Write-Behind)

    • 写流程:先更新缓存,异步批量写入数据库
    • 适用场景:写密集型且允许短暂不一致(如日志处理)

二、缓存策略优化

1. 缓存预热

  • 启动时加载:系统启动时预加载热点数据(如电商首页商品)
  • 定时任务:通过CronJob定期刷新缓存(如每日排行榜)
# 缓存预热伪代码
def preheat_cache():
    hot_items = db.query("SELECT * FROM items ORDER BY views DESC LIMIT 1000")
    redis.set("hot_items", serialize(hot_items), ex=3600)

2. 缓存更新

  • 主动更新
    • 监听数据库变更(如MySQL Binlog、Debezium)同步至缓存
    • 使用消息队列(Kafka)解耦更新逻辑
// Binlog监听更新缓存示例
binlogEventListener.onUpdate(data -> {
    redis.del("cache_key:" + data.getId());
});
  • 被动更新
    • 设置合理的TTL(如30分钟 + 随机偏移)
    • 采用延迟双删策略(先删缓存→更新DB→延迟再删缓存)

3. 缓存淘汰

  • 策略选择
    • volatile-lru:淘汰最近最少使用的过期键
    • allkeys-lfu(Redis 4.0+):淘汰访问频率最低的键
  • 内存控制
    • 监控Redis的used_memory,设置maxmemory阈值(如80%)
    • 使用MEMORY PURGE(Memcached)或Redis-Cli --bigkeys分析大Key

三、高并发场景下的缓存问题与解决方案

1. 缓存击穿(Hotspot Invalid)

  • 定义:redis中存在某些热点数据时,即有大量请求并发访问的key-value数据。当极热点key-value数据突然失效时,缓存未命中引起对后台数据库的频繁访问,这种现象叫缓存击穿
  • 问题:热点Key过期后,大量请求穿透到数据库
  • 解决
    • 互斥锁:使用Redis的SET key uuid NX PX 3000实现分布式锁,仅允许一个线程重建缓存
    • 逻辑过期:缓存不设TTL,由后台线程异步更新,业务层检查逻辑过期时间

2. 缓存穿透(Cache Penetration)

  • 定义:缓存穿透指查询一条缓存和数据库都不存在的数据,导致这条请求直接查询数据库,如果用户发起大量请求去查询不存在的数据会对数据库造成压力,甚至会导致数据库宕机。
  • 问题:大量请求不存在的Key(如恶意攻击)
  • 解决
    • 布隆过滤器(Bloom Filter):拦截无效请求(需预加载合法Key) ,由于哈希碰撞,有一定的误判率,且不可消除
    • 空值缓存:将不存在的结果缓存为NULL(设置短TTL如30秒)
    • 布谷鸟过滤器:高效存在性判断、支持删除、低误判率

3. 缓存雪崩(Cache Avalanche)

  • 定义:缓存雪崩是针对大量key集中过期,或热点key过期而制定的一种应对措施
  • 问题:大量缓存同时失效,请求压垮数据库
  • 解决
    • 二级缓存:本地缓存作为兜底,降低对分布式缓存的依赖
    • 过期时间优化策略
      • 为缓存有效期增加随机值:基础TTL + 随机时间(如300 + rand(0, 60)秒)
      • 热点数据永远不过期处理,注意,缓存空间达到限定值会通过LRU、LFU、FIFO等淘汰算法将部分key移除,腾出空间,可定时更新,手动刷新
      • 逻辑过期与异步更新

4. 数据一致性

  • 最终一致性方案
    • 消息队列异步同步:通过Canal监听DB变更,发送MQ事件更新缓存
    • 版本号控制:缓存值携带版本号,更新时校验版本防止旧数据覆盖
// 版本号校验示例
String cacheVersion = redis.get("user:1:version");
if (currentVersion > cacheVersion) {
    redis.set("user:1", newData);
}

四、缓存监控与治理

1. 核心监控指标

  • 命中率hit_rate = hits / (hits + misses)(目标≥95%)
  • 延迟分布:P99缓存访问延迟(Redis的latency monitor
  • 内存使用:Redis的used_memoryevicted_keys(淘汰键数)

2. 运维工具

  • Redis Insight:图形化监控缓存集群状态
  • Prometheus + Grafana:自定义仪表盘监控缓存性能
  • Chaos Engineering:模拟缓存节点故障,验证降级策略

3. 容灾设计

  • 多副本架构:Redis Cluster主从切换(故障自动转移)
  • 降级策略
    • 缓存故障时,直接访问数据库并限流(Sentinel熔断)
    • 本地缓存兜底,返回静态默认值

五、实战案例:电商库存缓存

1. 读流程

  • 用户查询商品库存时:
    1. 先查本地缓存(Caffeine),命中则返回。
    2. 未命中则查Redis,若Redis命中则回填本地缓存。
    3. Redis未命中则查数据库,回填Redis和本地缓存。

2. 写流程

  • 管理员修改库存时:
    1. 更新数据库。
    2. 删除Redis中的库存缓存。
    3. 通过MQ广播消息,其他节点的本地缓存失效。

3. 优化技巧

  • 库存预扣减
    • 下单时先扣减Redis库存(DECR原子操作),异步同步至数据库。
    • 使用Lua脚本保证原子性:
    local stock = redis.call('GET', KEYS[1])
    if tonumber(stock) >= tonumber(ARGV[1]) then
        redis.call('DECRBY', KEYS[1], ARGV[1])
        return 1  -- 扣减成功
    else
        return 0  -- 库存不足
    end
    

六、总结

高并发系统的缓存设计需遵循以下原则:

  1. 分层缓存:本地缓存 + 分布式缓存 + 客户端缓存,形成多级防御。
  2. 策略适配:根据数据特性选择Cache-Aside、Write-Through等模式。
  3. 问题防御:通过互斥锁、布隆过滤器、随机TTL应对击穿、穿透、雪崩。
  4. 监控兜底:实时监控命中率与延迟,故障时快速降级。

通过系统化的缓存设计,可提升吞吐量10倍以上,同时保障服务的高可用性。