最近在排查一个线上事故时,发现数据库服务器CPU使用率突然飙升到98%,连接池全部占满。查看慢查询日志,发现大量类似SELECT * FROM users WHERE id = 987654321的查询——这些ID明显不存在于系统中。这就是典型的缓存穿透攻击场景。
缓存穿透是指查询一个数据库中根本不存在的记录,由于缓存系统通常采用"查询-命中-返回/未命中-查库-回填"的工作机制,当遇到大量不存在的key查询时,每次请求都会穿透缓存层直接访问数据库。这种攻击的成本极低——攻击者只需要构造大量随机或无效的key即可,但对系统的破坏力却十分惊人。
在实际运维中,我总结出缓存穿透的几个明显特征:
很多开发者容易混淆缓存穿透与缓存击穿、雪崩的概念,这里我用实际案例说明它们的区别:
三者的根本区别在于:穿透是查询不存在的数据,而击穿和雪崩都是针对本应存在但暂时不可用的数据。
空值缓存(Null Caching)是我在生产环境验证过的最简单有效的解决方案。其核心思想是:即使数据库查询返回空结果,也将其缓存起来,避免重复查询数据库。
在Java项目中,我通常这样实现空值缓存:
java复制public User getUserById(Long id) {
// 参数校验前置
if (id == null || id <= 0) {
throw new IllegalArgumentException("Invalid user ID");
}
String cacheKey = "user:" + id;
// 一级缓存检查
User user = localCache.get(cacheKey);
if (user != null) {
return user == NULL_OBJECT ? null : user;
}
// 二级Redis缓存检查
String redisValue = redisTemplate.opsForValue().get(cacheKey);
if (redisValue != null) {
if (redisValue.isEmpty()) {
localCache.put(cacheKey, NULL_OBJECT);
return null;
}
User parsedUser = JSON.parseObject(redisValue, User.class);
localCache.put(cacheKey, parsedUser);
return parsedUser;
}
// 数据库查询
user = userRepository.findById(id).orElse(null);
// 缓存回填策略
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user),
USER_CACHE_TTL, TimeUnit.SECONDS);
localCache.put(cacheKey, user);
} else {
// 空值缓存设置较短TTL
redisTemplate.opsForValue().set(cacheKey, "",
NULL_CACHE_TTL, TimeUnit.SECONDS);
localCache.put(cacheKey, NULL_OBJECT);
}
return user;
}
实际经验:在电商系统中,对商品详情页采用空值缓存后,数据库QPS从峰值8000+降至正常水平300左右,效果显著。
布隆过滤器是解决缓存穿透的利器,特别适合用户ID、商品ID等离散值场景。
在分布式系统中,我推荐使用Redis的Bloom模块:
bash复制# RedisBloom模块加载
redis-cli --eval setup_bloom.lua
# 添加元素
BF.ADD user_ids 10001
BF.ADD user_ids 10002
# 检查存在性
BF.EXISTS user_ids 10001
对应的Java实现:
java复制public class BloomFilterService {
private final RedisTemplate<String, Object> redisTemplate;
public void initUserFilter(Collection<Long> userIds) {
String script = "for _, id in ipairs(ARGV) do\n" +
" redis.call('BF.ADD', KEYS[1], id)\n" +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(script),
Collections.singletonList("user_filter"),
userIds.toArray()
);
}
public boolean mightContain(Long userId) {
return redisTemplate.execute(
(RedisCallback<Boolean>) conn ->
conn.execute("BF.EXISTS", "user_filter".getBytes(),
String.valueOf(userId).getBytes()) == 1L
);
}
}
除了上述方案,还需要构建多层次的防御体系:
java复制// 基础校验
public void validateUserId(Long id) {
if (id == null) {
throw new ValidationException("ID不能为空");
}
if (id <= 0) {
throw new ValidationException("ID必须为正数");
}
if (id > MAX_USER_ID) {
throw new ValidationException("ID超出范围");
}
}
// 正则校验(适用于字符串ID)
public void validateProductCode(String code) {
if (!Pattern.matches("^[A-Z]{2}\\d{6}$", code)) {
throw new ValidationException("产品编码格式错误");
}
}
使用Sentinel实现多维度的限流策略:
java复制// 注解方式配置
@SentinelResource(
value = "userQuery",
blockHandler = "handleBlock",
fallback = "handleFallback"
)
public User getUser(Long id) {
// ...
}
// 控制台规则配置
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("userQuery");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1000); // 阈值
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
java复制public class BlacklistService {
private static final String NOT_FOUND_PREFIX = "nf:";
private final RedisTemplate<String, Object> redisTemplate;
public boolean checkAndBlock(String ip, String key) {
String counterKey = NOT_FOUND_PREFIX + ip;
Long count = redisTemplate.opsForValue().increment(counterKey);
redisTemplate.expire(counterKey, 1, TimeUnit.HOURS);
if (count != null && count > 100) {
redisTemplate.opsForValue().set("blacklist:" + ip, "1", 24, TimeUnit.HOURS);
return true;
}
return false;
}
}
根据我在多个大型项目的实施经验,推荐以下防御组合:
接入层:
应用层:
数据层:
建立完善的监控体系:
prometheus复制# Prometheus监控指标
- name: cache_penetration_attempts
type: counter
help: "Total cache penetration attempts"
- name: bloom_filter_rejections
type: counter
help: "Requests rejected by bloom filter"
# Grafana告警规则
groups:
- name: cache.rules
rules:
- alert: HighCachePenetration
expr: rate(cache_penetration_attempts[5m]) > 50
for: 10m
labels:
severity: warning
annotations:
summary: "High cache penetration attempts detected"
在百万级QPS的电商系统中实测结果:
| 方案 | 数据库QPS | 平均响应时间 | 错误率 |
|---|---|---|---|
| 无防护 | 8500+ | 1200ms | 15% |
| 空值缓存 | 300 | 50ms | 0.1% |
| 空值+布隆 | 50 | 20ms | 0.01% |
| 全量防护 | 10 | 5ms | 0.001% |
案例:某社交平台将不存在的用户资料缓存为null,设置24小时过期。当新用户注册后,仍然返回"用户不存在"。
根本原因:空值缓存TTL过长,且未建立缓存更新机制。
解决方案:
java复制@TransactionalEventListener
public void handleUserCreateEvent(UserCreatedEvent event) {
String key = "user:" + event.getUserId();
redisTemplate.delete(key);
bloomFilter.add(event.getUserId());
}
案例:某电商在商品搜索功能中使用布隆过滤器,导致大量正常商品被误判为不存在。
问题分析:布隆过滤器仅适用于精确匹配场景,不适用于:
正确做法:仅对主键查询使用布隆过滤器,其他查询类型采用:
反模式:全局统一限流阈值,导致高峰期正常用户被误杀。
优化方案:实施差异化限流:
java复制// 基于用户等级的差异化限流
public boolean shouldRateLimit(User user) {
int limit = 100; // 默认
if (user.isVip()) {
limit = 1000;
} else if (user.isNormal()) {
limit = 500;
}
return rateLimiter.tryAcquire(limit);
}
对于特别频繁的查询参数(如id=-1),可以采用特殊处理:
java复制public User getUser(Long id) {
// 热点参数特殊处理
if (KNOWN_BAD_IDS.contains(id)) {
cacheNullValue(id);
return null;
}
// 正常流程...
}
定期预热可能被查询的key:
java复制@Scheduled(fixedRate = 3600000)
public void warmUpCache() {
List<Long> activeUserIds = userRepository.findActiveUserIds();
activeUserIds.forEach(id -> {
if (!bloomFilter.mightContain(id)) {
bloomFilter.add(id);
}
});
}
使用算法识别异常访问模式:
python复制# Python示例(实际生产可用Java ML库)
from sklearn.ensemble import IsolationForest
clf = IsolationForest(contamination=0.01)
clf.fit(training_data)
def is_abnormal(request):
features = extract_features(request)
return clf.predict([features])[0] == -1
在Java项目中,我通常采用以下架构实现智能防护:
sql复制-- 创建防穿透的特殊索引
CREATE INDEX idx_user_negative ON users(id)
WHERE id < 0;
-- 查询优化
SELECT /*+ INDEX(users idx_user_negative) */ *
FROM users WHERE id = -1;
sql复制-- 使用覆盖索引避免回表
ALTER TABLE users ADD INDEX idx_id_cover (id) INCLUDE (name, age);
-- 查询重写
EXPLAIN SELECT EXISTS(
SELECT 1 FROM users WHERE id = -1
) AS exists_flag;
javascript复制// 使用特殊索引
db.users.createIndex({_id: 1}, {partialFilterExpression: {_id: {$lt: 0}}})
// 查询优化
db.users.find({_id: -1}).explain("executionStats")
现象:秒杀活动期间,数据库CPU飙升至100%,大量正常请求超时。
根因分析:
解决方案:
现象:新用户注册后,部分查询仍返回"用户不存在"。
问题定位:
优化措施:
随着攻击手段的不断进化,缓存穿透防护也在持续发展:
在实际项目中,我建议每隔半年重新评估防护策略的有效性,及时跟进最新的防护技术。最近在实施的一个金融项目中,我们采用了智能流量分析+动态规则引擎的方案,成功将缓存穿透导致的数据库访问量降低了99.8%。