一、缓存穿透:当查询不存在的数据时

缓存穿透是指查询一个数据库中根本不存在的数据,导致请求直接穿透缓存层直达数据库,失去缓存保护作用。

主要原因:

  1. 业务代码或数据问题

  2. 恶意攻击、爬虫大量请求不存在的数据

解决方案:

1. 缓存空对象

将查询为空的Key也缓存起来,并设置较短的过期时间(如300秒),避免频繁查询数据库。

String get(String key) {
    String cacheValue = cache.get(key);
    if (StringUtils.isBlank(cacheValue)) {
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        if (storageValue == null) {
            cache.expire(key, 60 * 5); // 空值缓存300秒
        }
        return storageValue;
    }
    return cacheValue;
}
2. 布隆过滤器(Bloom Filter)

使用布隆过滤器预先过滤掉肯定不存在的数据请求。

特点:

  • 当过滤器说某个值不存在时,肯定不存在

  • 当过滤器说存在时,可能不存在(存在误判率)

Redisson实现示例:

RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
bloomFilter.tryInit(100000000L, 0.03); // 预计元素1亿,误差率3%
bloomFilter.add("zhuge");

// 查询前先判断
if (!bloomFilter.contains(key)) {
    return "";
}

二、缓存击穿:热点Key突然失效

大量缓存同时在某一时刻失效,导致所有请求直接访问数据库。

解决方案:错峰过期

int expireTime = new Random().nextInt(300) + 300; // 300-600秒随机
cache.expire(key, expireTime);

三、缓存雪崩:缓存层整体失效

缓存层无法提供服务(如Redis集群宕机),导致流量直接冲击数据库层。

防御策略:

  1. 保证高可用:使用Redis Sentinel或Cluster

  2. 限流降级:引入Sentinel/Hystrix,对非核心数据直接返回降级信息

  3. 提前演练:模拟缓存宕机场景,制定应急预案

四、热点Key重建优化

热点Key失效时,大量线程同时重建缓存可能导致服务崩溃。

解决方案:互斥锁

String get(String key) {
    String value = redis.get(key);
    if (value == null) {
        String mutexKey = "mutex:key:" + key;
        if (redis.set(mutexKey, "1", "ex 180", "nx")) {
            value = db.get(key);
            redis.setex(key, timeout, value);
            redis.delete(mutexKey);
        } else {
            Thread.sleep(50);
            return get(key);
        }
    }
    return value;
}

五、缓存与数据库双写不一致

常见场景:

  1. 先更新数据库,后删除缓存 → 可能导致读取旧数据

  2. 先删除缓存,后更新数据库 → 并发读可能重建旧缓存

解决方案:

  1. 容忍短期不一致:为缓存设置过期时间

  2. 分布式锁:保证读写顺序性

  3. 监听Binlog:通过Canal等工具异步更新缓存

六、Redis开发规范与性能优化

1. 键值设计规范

  • Key命名业务名:表名:id(如trade:order:1

  • 避免BigKey

    • String类型 ≤ 10KB

    • 集合元素 ≤ 5000个

  • 渐进式删除BigKey:使用hscansscan等分批删除

2. 命令使用建议

  • 警惕O(N)命令:如hgetallsmembers

  • 禁用危险命令:keysflushall

  • 善用批量操作:mgetmsetpipeline

  • 事务较弱,推荐使用Lua脚本

3. 客户端连接池优化

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);      // 最大连接数
config.setMaxIdle(20);       // 最大空闲连接
config.setMinIdle(5);        // 最小空闲连接
config.setTestOnBorrow(true);

连接池大小计算公式
maxTotal ≈ (QPS / 单连接QPS) * 冗余系数

4. 内存淘汰策略(8种)

  • volatile-lru(推荐):从已设置过期时间的Key中使用LRU淘汰

  • allkeys-lru:从所有Key中使用LRU淘汰

  • noeviction:不淘汰,内存满时拒绝写入

5. Linux内核参数优化

# 减少SWAP使用倾向
echo 1 > /proc/sys/vm/swappiness
echo "vm.swappiness=1" >> /etc/sysctl.conf

# 允许内存超分配,避免fork失败
echo "vm.overcommit_memory=1" >> /etc/sysctl.conf

# 增加文件句柄数
ulimit -n 65535

6. 慢查询监控

# 设置慢查询阈值(1毫秒)
config set slowlog-log-slower-than 1000
# 查看慢查询日志
slowlog get 5

七、总结与实践建议

  1. 读多写少才用缓存:写多读多且要求强一致性的场景,直接使用数据库

  2. 缓存不是银弹:合理设置过期时间,容忍短期不一致

  3. 监控告警不可少:关注缓存命中率、慢查询、内存使用率

  4. 设计阶段考虑容灾:缓存宕机后,系统要有降级策略

缓存设计是门艺术,需要在性能、一致性、复杂度之间找到平衡点。避免过度设计,让解决方案保持简洁高效,才是工程实践的真谛。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐