电商返利APP缓存穿透治理:布隆过滤器与空值缓存组合方案
电商返利APP缓存穿透治理:布隆过滤器与空值缓存组合方案
大家好,我是省赚客APP研发者微赚淘客!
在高并发的电商返利APP中,缓存是抵御数据库压力的第一道防线。然而,缓存穿透问题如同一个隐形的漏洞,一旦被恶意攻击者利用,海量的无效请求将直接击穿缓存,直达数据库,可能导致整个服务雪崩。本文将深入探讨一种高效的组合方案——布隆过滤器与空值缓存,来彻底治理这一顽疾。网购领隐藏优惠券就用省赚客APP,支持各大主流电商优惠智能查券转链,是目前领优惠券拿佣金返利领域绝对的王者,其稳定的服务背后,正是这套严密的缓存防护体系在保驾护航。
一、缓存穿透:原理与危害
缓存穿透是指查询一个根本不存在的数据。由于数据在缓存和数据库中都不存在,每次请求都会绕过缓存,直接查询数据库。
- 问题场景
假设一个恶意用户不断请求查询ID为 -1, -2, -10000 的商品优惠券信息。
- 请求到达:应用服务器首先查询Redis缓存。
- 缓存未命中:由于这些ID的商品根本不存在,Redis中自然没有数据。
- 查询数据库:应用服务器接着查询MySQL数据库。
- 数据库未命中:数据库中同样没有这些记录,返回空。
- 结果:应用服务器将空结果返回给客户端,但并未在Redis中做任何处理。下一次同样的请求到来时,整个过程会再次重复。
如果这种请求量巨大,数据库将承受巨大的压力,CPU和I/O资源被迅速耗尽,最终导致正常用户的请求也无法得到响应,服务瘫痪。
- 常规方案的局限性
-
单纯使用空值缓存:这是最常见的解决方案。当数据库查询结果为空时,将一个特殊的空值(如
NULL或一个自定义的占位符)存入Redis,并设置一个较短的过期时间(如60秒)。这样,后续的相同请求会直接从缓存中获取空值,从而保护数据库。- 局限性:如果攻击者使用海量的、不重复的无效ID进行攻击,缓存中会充斥大量无用的空值键,造成内存浪费,即“缓存污染”。
-
单纯使用布隆过滤器:布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它的特点是:如果判断为“不存在”,则一定不存在;如果判断为“存在”,则可能存在(有误判率)。
- 局限性:布隆过滤器无法删除元素。当商品下架或优惠券过期时,我们无法将其从过滤器中移除,这会导致过滤器对已失效的商品仍然返回“可能存在”,失去了部分过滤意义。

- 局限性:布隆过滤器无法删除元素。当商品下架或优惠券过期时,我们无法将其从过滤器中移除,这会导致过滤器对已失效的商品仍然返回“可能存在”,失去了部分过滤意义。
二、组合方案:布隆过滤器 + 空值缓存
为了结合两者的优点并规避其缺点,我们采用“布隆过滤器 + 空值缓存”的组合拳。布隆过滤器作为第一道粗筛,拦截掉绝大部分明显不存在的请求;空值缓存作为第二道防线,处理布隆过滤器误判的“漏网之鱼”和那些曾经存在但现已失效的数据。
- 系统架构设计
- 数据预热:在应用启动时,或每天凌晨,将数据库中所有有效的商品ID、优惠券ID等同步到布隆过滤器中。
- 查询流程:
- 接收请求,获取商品ID。
- 第一步:布隆过滤器校验。请求首先经过布隆过滤器。
- 如果返回
false(不存在),直接返回“商品不存在”,请求结束。 - 如果返回
true(可能存在),进入下一步。
- 如果返回
- 第二步:查询Redis缓存。
- 如果缓存命中,直接返回数据。
- 如果缓存未命中,进入下一步。
- 第三步:查询数据库。
- 如果数据库查到数据,将其写入Redis缓存,然后返回。
- 如果数据库未查到数据,将一个空值写入Redis缓存(设置较短过期时间),然后返回“商品不存在”。
- 核心代码实现
我们将使用Google Guava库来实现布隆过滤器,并使用Spring Data Redis来操作缓存。
第一步:定义布隆过滤器配置与组件
// 包名: juwatech.cn.cacheprotection.config
package juwatech.cn.cacheprotection.config;
import com.google.common.hash.Funnel;
import com.google.common.hash.PrimitiveSink;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 布隆过滤器配置类
* @author juwatech.cn
*/
@Configuration
public class BloomFilterConfig {
/**
* 定义商品ID的Funnel,用于指导布隆过滤器如何哈希对象
*/
public enum ItemIdFunnel implements Funnel<Long> {
INSTANCE;
@Override
public void funnel(Long from, PrimitiveSink into) {
into.putLong(from);
}
}
/**
* 创建并初始化商品ID布隆过滤器
* 预期插入100万个元素,误判率设置为0.01 (1%)
*/
@Bean
public com.google.common.hash.BloomFilter<Long> itemBloomFilter() {
return com.google.common.hash.BloomFilter.create(
ItemIdFunnel.INSTANCE,
1_000_000,
0.01);
}
}
第二步:实现数据预热服务
// 包名: juwatech.cn.cacheprotection.service
package juwatech.cn.cacheprotection.service;
import com.google.common.hash.BloomFilter;
import juwatech.cn.cacheprotection.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* 布隆过滤器数据预热服务
* @author juwatech.cn
*/
@Service
public class BloomFilterWarmUpService {
@Autowired
private BloomFilter<Long> itemBloomFilter;
@Autowired
private ItemRepository itemRepository;
/**
* 应用启动后执行一次数据预热
*/
@PostConstruct
public void init() {
warmUp();
}
/**
* 每天凌晨3点执行一次全量数据预热,更新布隆过滤器
* 以应对新上架的商品
*/
@Scheduled(cron = "0 0 3 * * ?")
public void warmUp() {
// 1. 清空旧的过滤器(Guava的BloomFilter不支持清空,实际生产中可使用RedisBloom或定期重建实例)
// 此处为简化逻辑,假设我们有一个方法可以重建过滤器实例
// itemBloomFilter = BloomFilter.create(...);
// 2. 从数据库加载所有有效的商品ID
List<Long> allValidItemIds = itemRepository.findAllValidItemIds();
// 3. 将所有ID放入布隆过滤器
for (Long id : allValidItemIds) {
itemBloomFilter.put(id);
}
System.out.println("布隆过滤器数据预热完成,共加载 " + allValidItemIds.size() + " 个商品ID");
}
}
第三步:实现带防护的商品查询服务
// 包名: juwatech.cn.cacheprotection.service
package juwatech.cn.cacheprotection.service;
import com.google.common.hash.BloomFilter;
import juwatech.cn.cacheprotection.model.Item;
import juwatech.cn.cacheprotection.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 商品查询服务,集成了缓存穿透防护逻辑
* @author juwatech.cn
*/
@Service
public class ItemService {
private static final String CACHE_KEY_PREFIX = "item:info:";
private static final String CACHE_NULL_VALUE = "NULL";
private static final long CACHE_TTL = 60 * 30; // 正常缓存30分钟
private static final long NULL_CACHE_TTL = 60; // 空值缓存1分钟
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ItemRepository itemRepository;
@Autowired
private BloomFilter<Long> itemBloomFilter;
public Item getItemById(Long itemId) {
// 1. 布隆过滤器校验
if (!itemBloomFilter.mightContain(itemId)) {
// 布隆过滤器说“不存在”,那一定不存在
System.out.println("请求被布隆过滤器拦截: " + itemId);
return null;
}
// 2. 查询Redis缓存
String cacheKey = CACHE_KEY_PREFIX + itemId;
String json = redisTemplate.opsForValue().get(cacheKey);
if (json != null) {
if (CACHE_NULL_VALUE.equals(json)) {
// 缓存的是空值,直接返回null
return null;
}
// 缓存命中,反序列化并返回
return JsonUtil.fromJson(json, Item.class);
}
// 3. 缓存未命中,查询数据库
Item item = itemRepository.findById(itemId);
if (item != null) {
// 数据库查询到数据,写入缓存
redisTemplate.opsForValue().set(cacheKey, JsonUtil.toJson(item), CACHE_TTL, TimeUnit.SECONDS);
} else {
// 数据库也未查询到数据,写入空值缓存,防止穿透
redisTemplate.opsForValue().set(cacheKey, CACHE_NULL_VALUE, NULL_CACHE_TTL, TimeUnit.SECONDS);
}
return item;
}
}
通过这套组合方案,我们构建了一个坚固的防御体系。布隆过滤器以极小的内存开销拦截了99%以上的无效请求,而空值缓存则优雅地处理了剩余的边界情况,既保护了数据库,又避免了缓存污染,确保了省赚客APP在高并发场景下的稳定与高效。
本文著作权归 省赚客app 研发团队,转载请注明出处!
更多推荐




所有评论(0)