1. 缓存一致性问题解析
在分布式系统中,缓存与数据库的数据同步一直是个棘手的问题。我经历过一个电商项目,促销活动期间由于缓存不一致导致商品库存显示异常,直接损失了数十万销售额。这个问题本质上源于数据库和缓存无法保证原子性操作——更新数据库和更新缓存要么同时成功,要么同时失败。
1.1 Cache Aside Pattern实现细节
旁路缓存模式是经过验证的最佳实践,其核心操作流程如下:
读流程:
- 先查询缓存,命中则直接返回
- 未命中时查询数据库
- 将查询结果写入缓存(注意设置合理TTL)
- 返回数据
写流程:
- 先更新数据库
- 再删除缓存(不是更新)
关键提示:删除操作必须设置重试机制,建议采用异步消息队列保证最终一致性。我们项目中使用RabbitMQ实现了删除失败的补偿流程。
1.2 为什么删除优于更新
在早期版本中,我们尝试过直接更新缓存,结果遇到了几个严重问题:
- 写多读少场景浪费资源:对于用户画像这类高频更新低频读取的数据,95%的缓存更新都是无效的
- 并发更新导致脏数据:两个并发的写操作可能以错误顺序更新缓存
- 复杂计算开销大:有些缓存值是多个数据源的聚合结果,每次更新都需要完整计算
实测数据显示,采用删除策略后,缓存集群的CPU负载降低了37%,网络带宽消耗减少了28%。
2. 缓存穿透攻防实战
去年双十一大促前,我们的系统遭遇了恶意攻击——攻击者用脚本随机生成不存在的商品ID发起海量请求。当时没有防护措施,数据库连接池瞬间被打满,整个网站瘫痪了23分钟。
2.1 空对象缓存方案
这是我们最终采用的方案,具体实现要点:
java复制public Product getProduct(String id) {
// 1. 查缓存
Product product = cache.get(id);
if (product != null) {
// 特殊空值标记
if (product == EMPTY_OBJECT) {
return null;
}
return product;
}
// 2. 查数据库
product = db.query(id);
if (product == null) {
// 3. 缓存空值,设置短TTL
cache.set(id, EMPTY_OBJECT, 5*60);
return null;
}
// 4. 缓存真实数据
cache.set(id, product, 30*60);
return product;
}
参数调优经验:
- 空值TTL建议5-30分钟,需要根据业务特点平衡内存消耗和数据新鲜度
- 内存不足时可考虑压缩空值存储,比如用单字符代替完整对象
2.2 布隆过滤器进阶应用
我们在大促期间采用了双层防护:
- 第一层:本地布隆过滤器(Guava实现)
- 第二层:Redis布隆过滤器(Redisson实现)
性能对比数据:
| 方案 | 内存占用 | QPS | 误判率 |
|---|---|---|---|
| 纯空缓存 | 12GB | 8万 | 0% |
| 本地BF | 500MB | 15万 | 1% |
| Redis BF | 800MB | 12万 | 0.1% |
特别提醒:布隆过滤器需要预热,我们开发了全量/增量同步工具,通过Kafka消息保证数据一致性。
3. 缓存雪崩防御体系
某次凌晨系统升级后,由于配置错误导致所有缓存同时过期,引发了连锁反应。这个教训让我们建立了完整的雪崩防护机制。
3.1 多级缓存架构
我们现在采用五层缓存体系:
- 浏览器本地缓存(max-age=60)
- Nginx代理缓存(shared_dict)
- 应用本地缓存(Caffeine)
- Redis集群
- 数据库查询缓存
关键配置参数:
nginx复制# Nginx配置示例
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m
inactive=60m use_temp_path=off;
location /api {
proxy_cache my_cache;
proxy_cache_valid 200 30s;
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
3.2 熔断降级策略
我们基于Sentinel实现了动态规则配置:
- 当DB负载超过80%时,触发二级降级
- 当Redis不可用时,启用本地缓存模式
- 极端情况下返回静态兜底数据
熔断规则示例:
java复制// 商品查询熔断规则
DegradeRule rule = new DegradeRule("productQuery")
.setGrade(RuleConstant.DEGRADE_GRADE_RT)
.setCount(100)
.setTimeWindow(10);
4. 缓存击穿解决方案对比
热点商品秒杀场景下,我们对比了两种方案的实际表现:
4.1 互斥锁实现细节
采用Redisson分布式锁的优化版本:
java复制public Product getProductWithLock(String id) {
Product product = cache.get(id);
if (product == null) {
RLock lock = redisson.getLock("lock:" + id);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
product = cache.get(id);
if (product == null) {
product = db.query(id);
cache.set(id, product, 30*60);
}
} else {
// 降级策略
return getLocalCache(id);
}
} finally {
lock.unlock();
}
}
return product;
}
4.2 逻辑过期方案实践
我们扩展了缓存值结构:
json复制{
"data": {...},
"expire": 1672531200,
"version": "v2.3"
}
异步更新线程池配置要点:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 5, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("cache-rebuild-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy());
性能对比数据:
| 指标 | 互斥锁方案 | 逻辑过期方案 |
|---|---|---|
| 平均响应时间 | 78ms | 12ms |
| 99线 | 210ms | 25ms |
| 数据库QPS | 50 | 120 |
| 数据延迟 | 0ms | 最多500ms |
在支付业务等强一致性场景用互斥锁,在商品展示等场景用逻辑过期,这是我们在多次压测后得出的最佳实践。