一、Bug 场景
在一个电商商品查询系统中,使用 Redis 作为缓存以减轻数据库压力。用户通过商品 ID 查询商品信息时,系统先从 Redis 缓存中查找,若未找到则查询数据库,并将查询结果存入 Redis 缓存。然而,在遭受恶意攻击时,大量不存在的商品 ID 查询涌入系统,导致所有查询都穿透到数据库,数据库压力剧增,甚至出现服务不可用的情况。
二、代码示例
商品服务(有缺陷)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private RedisTemplate redisTemplate;
// 假设这是数据库查询方法
private Object queryProductFromDB(String productId) {
// 模拟数据库查询逻辑,返回商品信息或null
return null;
}
public Object getProduct(String productId) {
Object product = redisTemplate.opsForValue().get(productId);
if (product == null) {
product = queryProductFromDB(productId);
if (product != null) {
redisTemplate.opsForValue().set(productId, product);
}
}
return product;
}
}
三、问题描述
- 预期行为:正常情况下,大部分商品查询能从 Redis 缓存中获取数据,只有少部分缓存未命中的查询才会穿透到数据库,数据库压力处于可控范围。
- 实际行为:当大量不存在的商品 ID 查询进入系统时,由于这些 ID 在 Redis 中没有对应的缓存数据,系统会不断查询数据库,导致数据库压力剧增。这是因为系统没有对不存在的数据进行有效处理,每次查询都认为是正常的缓存未命中,从而持续访问数据库。恶意攻击者可以利用这一点,通过大量查询不存在的商品 ID,耗尽数据库资源,导致系统崩溃。
四、解决方案
- 布隆过滤器(Bloom Filter) :在查询数据库之前,使用布隆过滤器判断商品 ID 是否存在。布隆过滤器可以快速判断一个元素一定不存在或者可能存在。如果布隆过滤器判断商品 ID 一定不存在,直接返回,不再查询数据库;如果判断可能存在,再查询数据库。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private RedisTemplate redisTemplate;
private static final BloomFilter bloomFilter = BloomFilter.create(
Funnels.stringFunnel(java.nio.charset.StandardCharsets.UTF_8), 1000000, 0.01);
// 假设这是数据库查询方法
private Object queryProductFromDB(String productId) {
// 模拟数据库查询逻辑,返回商品信息或null
return null;
}
public Object getProduct(String productId) {
if (!bloomFilter.mightContain(productId)) {
return null;
}
Object product = redisTemplate.opsForValue().get(productId);
if (product == null) {
product = queryProductFromDB(productId);
if (product != null) {
redisTemplate.opsForValue().set(productId, product);
bloomFilter.put(productId);
}
}
return product;
}
}
- 缓存空值:当查询数据库发现商品不存在时,将空值存入 Redis 缓存,并设置较短的过期时间。这样,后续相同的查询直接从 Redis 获取空值,避免再次查询数据库。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private RedisTemplate redisTemplate;
// 假设这是数据库查询方法
private Object queryProductFromDB(String productId) {
// 模拟数据库查询逻辑,返回商品信息或null
return null;
}
public Object getProduct(String productId) {
Object product = redisTemplate.opsForValue().get(productId);
if (product == null) {
product = queryProductFromDB(productId);
if (product != null) {
redisTemplate.opsForValue().set(productId, product);
} else {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set(productId, null, 10, TimeUnit.MINUTES);
}
}
return product;
}
}