文章目录
- 背景
- 代码实现
- 前置
- 实体类
- 常量类
- 工具类
- 结果返回类
- 控制层
- 缓存空对象
- 布隆过滤器
- 结合两种方法
背景
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库
常见的解决方案有两种,分别是缓存空对象和布隆过滤器
1.缓存空对象
优点:实现简单,维护方便
缺点:额外的内存消耗、可能造成短期的不一致
2.布隆过滤器
优点:内存占用较少,没有多余key
缺点:实现复杂、存在误判可能
代码实现
前置
这里以根据 id 查询商品店铺为案例
实体类
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class Shop implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 商铺名称
*/
private String name;
/**
* 商铺类型的id
*/
private Long typeId;
/**
* 商铺图片,多个图片以','隔开
*/
private String images;
/**
* 商圈,例如陆家嘴
*/
private String area;
/**
* 地址
*/
private String address;
/**
* 经度
*/
private Double x;
/**
* 维度
*/
private Double y;
/**
* 均价,取整数
*/
private Long avgPrice;
/**
* 销量
*/
private Integer sold;
/**
* 评论数量
*/
private Integer comments;
/**
* 评分,1~5分,乘10保存,避免小数
*/
private Integer score;
/**
* 营业时间,例如 10:00-22:00
*/
private String openHours;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
@TableField(exist = false)
private Double distance;
}
常量类
public class RedisConstants {
public static final Long CACHE_NULL_TTL = 2L;
public static final Long CACHE_SHOP_TTL = 30L;
public static final String CACHE_SHOP_KEY = "cache:shop:";
}
工具类
public class ObjectMapUtils {
// 将对象转为 Map
public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
Map<String, String> result = new HashMap<>();
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 如果为 static 且 final 则跳过
if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
continue;
}
field.setAccessible(true); // 设置为可访问私有字段
Object fieldValue = field.get(obj);
if (fieldValue != null) {
result.put(field.getName(), field.get(obj).toString());
}
}
return result;
}
// 将 Map 转为对象
public static Object map2Obj(Map<Object, Object> map, Class<?> clazz) throws Exception {
Object obj = clazz.getDeclaredConstructor().newInstance();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object fieldName = entry.getKey();
Object fieldValue = entry.getValue();
Field field = clazz.getDeclaredField(fieldName.toString());
field.setAccessible(true); // 设置为可访问私有字段
String fieldValueStr = fieldValue.toString();
// 根据字段类型进行转换
if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
field.set(obj, Integer.parseInt(fieldValueStr));
} else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
field.set(obj, Boolean.parseBoolean(fieldValueStr));
} else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
field.set(obj, Double.parseDouble(fieldValueStr));
} else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
field.set(obj, Long.parseLong(fieldValueStr));
} else if (field.getType().equals(String.class)) {
field.set(obj, fieldValueStr);
} else if(field.getType().equals(LocalDateTime.class)) {
field.set(obj, LocalDateTime.parse(fieldValueStr));
}
}
return obj;
}
}
结果返回类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;
public static Result ok(){
return new Result(true, null, null, null);
}
public static Result ok(Object data){
return new Result(true, null, data, null);
}
public static Result ok(List<?> data, Long total){
return new Result(true, null, data, total);
}
public static Result fail(String errorMsg){
return new Result(false, errorMsg, null, null);
}
}
控制层
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryShopById(id);
}
/**
* 新增商铺信息
* @param shop 商铺数据
* @return 商铺id
*/
@PostMapping
public Result saveShop(@RequestBody Shop shop) {
return shopService.saveShop(shop);
}
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
return shopService.updateShop(shop);
}
}
缓存空对象
流程图为:
服务层代码:
public Result queryShopById(Long id) {
// 从 redis 查询
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);
// 缓存命中
if(!entries.isEmpty()) {
try {
// 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)
if(entries.containsKey("")) {
return Result.fail("店铺不存在");
}
// 刷新有效期
redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);
return Result.ok(shop);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 查询数据库
Shop shop = this.getById(id);
if(shop == null) {
// 存入空值
redisTemplate.opsForHash().put(shopKey, "", "");
redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
// 不存在,直接返回
return Result.fail("店铺不存在");
}
// 存在,写入 redis
try {
redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));
redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return Result.ok(shop);
}
布隆过滤器
这里选择使用布隆过滤器存储存在于数据库中的 id,原因在于,如果存储了不存在于数据库中的 id,首先由于 id 的取值范围很大,那么不存在的 id 有很多,因此更占用空间;其次,由于布隆过滤器有一定的误判率,那么可能导致少数原本存在于数据库中的 id 被判为了不存在,然后直接返回了,此时就会出现根本性的正确性错误。相反,如果存储的是数据库中存在的 id,那么即使少数不存在的 id 被判为了存在,由于数据库中确实没有对应的 id,那么也会返回空,最终结果还是正确的
这里使用 guava 依赖的布隆过滤器
依赖为:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
封装了布隆过滤器的类(注意初始化时要把数据库中已有的 id 加入布隆过滤器):
public class ShopBloomFilter {
private BloomFilter<Long> bloomFilter;
public ShopBloomFilter(ShopMapper shopMapper) {
// 初始化布隆过滤器,设计预计元素数量为100_0000L,误差率为1%
bloomFilter = BloomFilter.create(Funnels.longFunnel(), 100_0000, 0.01);
// 将数据库中已有的店铺 id 加入布隆过滤器
List<Shop> shops = shopMapper.selectList(null);
for (Shop shop : shops) {
bloomFilter.put(shop.getId());
}
}
public void add(long id) {
bloomFilter.put(id);
}
public boolean mightContain(long id){
return bloomFilter.mightContain(id);
}
}
对应的配置类(将其设置为 bean)
@Configuration
public class BloomConfig {
@Bean
public ShopBloomFilter shopBloomFilter(ShopMapper shopMapper) {
return new ShopBloomFilter(shopMapper);
}
}
首先要修改查询方法,在根据 id 查询时,如果对应 id 不在布隆过滤器中,则直接返回。然后还要修改保存方法,在保存的时候还需要将对应的 id 加入布隆过滤器中
@Override
public Result queryShopById(Long id) {
// 如果不在布隆过滤器中,直接返回
if(!shopBloomFilter.mightContain(id)) {
return Result.fail("店铺不存在");
}
// 从 redis 查询
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);
// 缓存命中
if(!entries.isEmpty()) {
try {
// 刷新有效期
redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);
return Result.ok(shop);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 查询数据库
Shop shop = this.getById(id);
if(shop == null) {
// 不存在,直接返回
return Result.fail("店铺不存在");
}
// 存在,写入 redis
try {
redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));
redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return Result.ok(shop);
}
@Override
public Result saveShop(Shop shop) {
// 写入数据库
this.save(shop);
// 将 id 写入布隆过滤器
shopBloomFilter.add(shop.getId());
// 返回店铺 id
return Result.ok(shop.getId());
}
结合两种方法
由于布隆过滤器有一定的误判率,所以这里可以进一步优化,如果出现误判情况,即原本不存在于数据库中的 id 被判为了存在,就用缓存空对象的方式将其缓存到 redis 中
@Override
public Result queryShopById(Long id) {
// 如果不在布隆过滤器中,直接返回
if(!shopBloomFilter.mightContain(id)) {
return Result.fail("店铺不存在");
}
// 从 redis 查询
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);
// 缓存命中
if(!entries.isEmpty()) {
try {
// 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)
if(entries.containsKey("")) {
return Result.fail("店铺不存在");
}
// 刷新有效期
redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);
return Result.ok(shop);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 查询数据库
Shop shop = this.getById(id);
if(shop == null) {
// 存入空值
redisTemplate.opsForHash().put(shopKey, "", "");
redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
// 不存在,直接返回
return Result.fail("店铺不存在");
}
// 存在,写入 redis
try {
redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));
redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return Result.ok(shop);
}