redis缓存穿透怎么理解

发布时间:2021-12-08 14:16:25 作者:iii
来源:亿速云 阅读:131
# Redis缓存穿透怎么理解

## 引言

在当今互联网应用中,缓存技术已成为提升系统性能的关键组件。作为高性能键值存储系统的代表,Redis被广泛应用于缓存场景中。然而,在使用Redis缓存时,开发者常会遇到"缓存穿透"这一棘手问题。本文将深入剖析缓存穿透的概念、产生原因、危害以及多种解决方案,帮助开发者构建更健壮的缓存系统。

## 一、缓存穿透的基本概念

### 1.1 什么是缓存穿透

缓存穿透(Cache Penetration)是指**查询一个根本不存在的数据**,导致这个查询请求直接穿过缓存层,每次都要访问持久化存储(如数据库)的现象。与缓存击穿、缓存雪崩不同,穿透问题关注的是"不存在数据"的异常访问场景。

### 1.2 相关术语辨析

- **缓存击穿**:热点key过期瞬间大量请求直达数据库
- **缓存雪崩**:大量key同时过期导致请求暴击存储层
- **缓存穿透**:查询不存在数据的持续高压请求

三者对比表:
| 问题类型 | 触发条件 | 影响范围 | 典型场景 |
|---------|----------|----------|----------|
| 穿透    | 查询不存在数据 | 单个或多个不存在key | 恶意攻击、业务bug |
| 击穿    | 热点key过期 | 单个热点key | 秒杀商品查询 |
| 雪崩    | 大量key同时过期 | 大批量key | 缓存初始化、定时任务刷新 |

## 二、缓存穿透的产生原因

### 2.1 恶意攻击场景

攻击者构造大量数据库不存在的key进行请求,例如:
- 遍历不存在的用户ID:`user:9999999`
- 使用负数值或超长字符串:`product:-10086`
- 随机生成UUID作为查询参数

### 2.2 业务逻辑缺陷

- 未校验的输入参数直接作为缓存key
- 误删除数据后未清理缓存
- 分页查询未处理越界请求

### 2.3 数据同步延迟

新业务上线时:
1. 用户查询刚下架的商品
2. 缓存已删除但搜索引擎仍有索引
3. 持续产生对不存在商品的查询

## 三、缓存穿透的危害分析

### 3.1 对数据库的直接压力

典型案例:
- 某电商平台遭遇CC攻击,攻击者每秒发送2万次不存在的商品ID查询
- Redis未命中导致QPS全部压到MySQL
- 数据库CPU飙升至90%,正常业务查询响应时间从50ms升至2s+

### 3.2 系统资源浪费

资源消耗对比表:
| 资源类型 | 正常查询 | 穿透查询 |
|---------|----------|----------|
| Redis连接 | 1次 | 1次 |
| 网络IO | 缓存返回约1KB | 完整查询流程 |
| CPU周期 | 缓存解码纳秒级 | SQL解析+执行毫秒级 |

### 3.3 连带效应

- 连接池被占满导致正常请求阻塞
- 磁盘IO升高影响其他业务表查询
- 可能触发数据库的慢查询告警机制

## 四、解决方案全景图

### 4.1 防御矩阵

┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ 客户端防护 │───▶│ 缓存层防护 │───▶│ 存储层防护 │ └───────────────┘ └───────────────┘ └───────────────┘


### 4.2 方案选型参考

根据QPS级别选择策略:
- 万级QPS:布隆过滤器+空值缓存
- 十万级QPS:布隆过滤器+限流
- 百万级QPS:多级缓存+弹性扩缩容

## 五、详细解决方案

### 5.1 空对象缓存(Null Caching)

实现示例:
```java
public Product getProduct(String id) {
    // 尝试从缓存获取
    Product product = redis.get("product:" + id);
    if (product != null) {
        return product instanceof NullProduct ? null : product;
    }
    
    // 查询数据库
    product = db.query("SELECT * FROM products WHERE id = ?", id);
    
    // 数据库不存在则缓存空对象
    if (product == null) {
        redis.setex("product:" + id, 300, new NullProduct());
        return null;
    }
    
    // 正常缓存数据
    redis.setex("product:" + id, 3600, product);
    return product;
}

注意事项: - 设置较短的TTL(如5-10分钟) - 空对象应尽量小(Redis的""或特定标记对象) - 需考虑缓存污染问题

5.2 布隆过滤器(Bloom Filter)

5.2.1 实现原理

布隆过滤器位数组操作流程:

1. 初始化m位的bit数组,全部置0
2. 添加元素时,用k个hash函数计算得到k个位置并置1
3. 检查元素时,若所有hash位置都为1则可能存在

5.2.2 Redis实现方案

# 使用Redis的位图实现
import redis
from hashlib import md5

class RedisBloomFilter:
    def __init__(self, key, expected_insertions=1000000, fpp=0.01):
        self.key = key
        self.redis = redis.StrictRedis()
        # 计算最优参数
        self.size = self._optimal_size(expected_insertions, fpp)
        self.hash_count = self._optimal_hash_count(expected_insertions, self.size)
    
    def add(self, item):
        for seed in range(self.hash_count):
            index = self._hash(item, seed) % self.size
            self.redis.setbit(self.key, index, 1)
    
    def exists(self, item):
        for seed in range(self.hash_count):
            index = self._hash(item, seed) % self.size
            if not self.redis.getbit(self.key, index):
                return False
        return True

5.2.3 参数设计建议

5.3 多级校验策略

5.3.1 前置校验层

// Express中间件示例
app.use('/api/products/:id', (req, res, next) => {
    const id = req.params.id;
    
    // 格式校验
    if (!/^\d{1,8}$/.test(id)) {
        return res.status(400).json({error: 'Invalid ID format'});
    }
    
    // 范围校验
    const numId = parseInt(id);
    if (numId < 1 || numId > MAX_PRODUCT_ID) {
        return res.status(404).json({error: 'Product not found'});
    }
    
    next();
});

5.3.2 业务规则校验

5.4 限流与熔断

5.4.1 Redis+Lua限流脚本

-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local last_tokens = tonumber(redis.call("hget", key, "tokens")) or capacity
local last_refreshed = tonumber(redis.call("hget", key, "last_refreshed")) or now

local delta = math.max(0, now - last_refreshed)
local new_tokens = math.min(capacity, last_tokens + delta * rate)

local allowed = new_tokens >= requested
local result = 0

if allowed then
    result = 1
    new_tokens = new_tokens - requested
end

redis.call("hset", key, "tokens", new_tokens)
redis.call("hset", key, "last_refreshed", now)
redis.call("expire", key, math.ceil(capacity / rate) * 2)

return result

5.4.2 熔断策略配置

Hystrix配置示例:

@HystrixCommand(
    fallbackMethod = "getProductFallback",
    commandProperties = {
        @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="20"),
        @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="50"),
        @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="5000")
    }
)
public Product getProduct(String id) {
    // 业务逻辑
}

六、进阶优化方案

6.1 热点key发现

实时监控方案:

# 使用Redis的HyperLogLog统计key访问
def track_key_access(key):
    redis.pfadd("access_log", key)
    # 每分钟分析热点
    if time.time() % 60 == 0:
        hot_keys = analyze_hot_keys()
        update_bloom_filter(hot_keys)

def analyze_hot_keys():
    all_keys = redis.pfcount("access_log")
    return redis.execute_command("TOPK.LIST", "hot_keys")

6.2 异步预热机制

// Spring EventListener示例
@EventListener
public void handleProductUpdate(ProductUpdateEvent event) {
    CompletableFuture.runAsync(() -> {
        // 预热布隆过滤器
        bloomFilter.add(event.getProductId());
        // 加载二级缓存
        loadingCache.put(event.getProductId(), 
            productService.getProduct(event.getProductId()));
    }, warmUpExecutor);
}

6.3 多级缓存架构

典型架构示例:

客户端 → CDN边缘缓存 → L1 Redis → L2 Redis → 数据库
                ↓
            布隆过滤器层

七、真实案例解析

7.1 社交平台用户查询优化

问题现象: - 用户搜索不存在的用户名导致MySQL负载飙升 - 每秒约8000次穿透查询

解决方案: 1. 部署Redis布隆过滤器集群 2. 使用用户ID范围分片(0-1亿、1-2亿…) 3. 添加名字格式校验(长度2-20,仅允许特定字符)

效果: - 数据库查询下降99.8% - 布隆过滤器误判率稳定在0.3%

7.2 电商商品详情页防护

挑战: - 爬虫遍历商品ID - 商品下架后仍有大量查询

实施步骤: 1. 商品下架时同步: - 删除缓存 - 更新布隆过滤器 - 记录到黑名单服务 2. 查询链路:

   graph TD
   A[请求] --> B{布隆过滤器检查}
   B -->|存在可能| C[查询缓存]
   B -->|不存在| D[返回404]
   C -->|命中| E[返回数据]
   C -->|未命中| F[校验黑名单]
   F -->|在黑名单| D
   F -->|不在| G[查询数据库]

八、监控与度量

8.1 关键监控指标

Prometheus配置示例:

metrics:
  cache_penetration:
    type: counter
    help: "Total cache penetration requests"
    labels: [service]
  bloom_filter_false_positives:
    type: counter
    help: "Bloom filter false positives"
  db_fallback_queries:
    type: gauge
    help: "Current DB queries caused by cache miss"

8.2 告警规则

# 基于突增比例的告警
def check_penetration_alert():
    normal_rate = get_historical_penetration_rate()
    current_rate = get_current_penetration_rate()
    
    if current_rate > normal_rate * 5:  # 5倍突增
        send_alert("Cache penetration spike detected!")
    
    if get_db_load() > 80:  # 数据库负载>80%
        trigger_circuit_breaker()

九、未来发展趋势

  1. 硬件加速布隆过滤器:Intel的SPP(Set Privacy Protection)指令集
  2. 机器学习预测:基于历史访问模式训练穿透预测模型
  3. 服务网格集成:在Istio等Service Mesh层实现统一防护
  4. 量子哈希函数:抗碰撞能力更强的量子哈希算法

结语

缓存穿透问题犹如缓存系统的”免疫缺陷”,需要开发者构建多层次的防御体系。通过本文介绍的空对象缓存、布隆过滤器、请求校验等组合策略,配合完善的监控机制,可以有效提升系统抗穿透能力。随着技术的发展,新的解决方案将不断涌现,但理解问题本质、根据业务特点设计针对性方案的原则永远不会过时。

附录

A. Redis布隆过滤器模块安装

# 编译redisbloom模块
git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom
make

# 启动Redis加载模块
redis-server --loadmodule ./redisbloom.so

B. 性能测试数据对比

测试环境:4核8G云主机,Redis 6.2,MySQL 8.0

方案 吞吐量(QPS) 平均延迟 数据库负载
无防护 12,000 15ms 90%
空缓存 45,000 5ms 30%
布隆过滤器 78,000 2ms %
布隆+空缓存 65,000 3ms %

”`

推荐阅读:
  1. redis缓存穿透,缓存击穿,缓存雪崩原因+解决方案
  2. 内网穿透原理解析

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

redis

上一篇:HBase体系结构是怎么样的

下一篇:HBase如何进行编程

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》