您好,登录后才能下订单哦!
# 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的""
或特定标记对象)
- 需考虑缓存污染问题
布隆过滤器位数组操作流程:
1. 初始化m位的bit数组,全部置0
2. 添加元素时,用k个hash函数计算得到k个位置并置1
3. 检查元素时,若所有hash位置都为1则可能存在
# 使用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
// 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();
});
-- 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
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) {
// 业务逻辑
}
实时监控方案:
# 使用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")
// Spring EventListener示例
@EventListener
public void handleProductUpdate(ProductUpdateEvent event) {
CompletableFuture.runAsync(() -> {
// 预热布隆过滤器
bloomFilter.add(event.getProductId());
// 加载二级缓存
loadingCache.put(event.getProductId(),
productService.getProduct(event.getProductId()));
}, warmUpExecutor);
}
典型架构示例:
客户端 → CDN边缘缓存 → L1 Redis → L2 Redis → 数据库
↓
布隆过滤器层
问题现象: - 用户搜索不存在的用户名导致MySQL负载飙升 - 每秒约8000次穿透查询
解决方案: 1. 部署Redis布隆过滤器集群 2. 使用用户ID范围分片(0-1亿、1-2亿…) 3. 添加名字格式校验(长度2-20,仅允许特定字符)
效果: - 数据库查询下降99.8% - 布隆过滤器误判率稳定在0.3%
挑战: - 爬虫遍历商品ID - 商品下架后仍有大量查询
实施步骤: 1. 商品下架时同步: - 删除缓存 - 更新布隆过滤器 - 记录到黑名单服务 2. 查询链路:
graph TD
A[请求] --> B{布隆过滤器检查}
B -->|存在可能| C[查询缓存]
B -->|不存在| D[返回404]
C -->|命中| E[返回数据]
C -->|未命中| F[校验黑名单]
F -->|在黑名单| D
F -->|不在| G[查询数据库]
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"
# 基于突增比例的告警
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()
缓存穿透问题犹如缓存系统的”免疫缺陷”,需要开发者构建多层次的防御体系。通过本文介绍的空对象缓存、布隆过滤器、请求校验等组合策略,配合完善的监控机制,可以有效提升系统抗穿透能力。随着技术的发展,新的解决方案将不断涌现,但理解问题本质、根据业务特点设计针对性方案的原则永远不会过时。
# 编译redisbloom模块
git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom
make
# 启动Redis加载模块
redis-server --loadmodule ./redisbloom.so
测试环境:4核8G云主机,Redis 6.2,MySQL 8.0
方案 | 吞吐量(QPS) | 平均延迟 | 数据库负载 |
---|---|---|---|
无防护 | 12,000 | 15ms | 90% |
空缓存 | 45,000 | 5ms | 30% |
布隆过滤器 | 78,000 | 2ms | % |
布隆+空缓存 | 65,000 | 3ms | % |
”`
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。