缓存穿透是指查询一个不存在的数据,由于缓存和数据库都没有命中,导致每次请求都需要从数据库中读取数据,增加了数据库的负担。解决缓存穿透的方法有以下几种:
布隆过滤器(Bloom Filter):使用位数组来表示一个集合,并通过哈希函数将元素映射到数组上。在查询数据时,先判断该数据是否存在于布隆过滤器中,如果存在则直接返回结果,否则再从数据库中查询数据。
缓存空对象:在缓存中存储空对象,当查询一个不存在的数据时,直接返回空对象,而不是默认值或者错误信息。
设置热点数据永不过期:对于一些热点数据,可以设置永不过期,这样即使缓存未命中,也不会影响数据的一致性。
使用分布式锁:在查询数据前先使用分布式锁进行加锁,保证只有一个线程能够访问数据库,其他线程需要等待锁释放后才能进行查询。
使用数据库的缓存机制:一些数据库提供了自己的缓存机制,可以将查询结果缓存到内存中,减少对数据库的访问次数。
布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,
假设布隆过滤器判断这个数据不存在,则直接返回
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突

当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了

对于缓存穿透问题,设置热点数据永不过期是一种解决方法。热点数据指的是被频繁访问的数据,如果将这些数据的过期时间设置为永久或者相对较长的时间,即使缓存未命中,也不会影响数据的一致性。
private static Map<String, Object> hotDataMap = new ConcurrentHashMap<>();
hotDataMap.put("key", value);
hotDataMap.put("key", value); // 可以重复添加
Object value = hotDataMap.get("key");
if (value == null) {
// 从数据库中查询数据
value = getValueFromDatabase();
hotDataMap.put("key", value);
}
cache.put(key, value);
需要注意的是:
如果设置了热点数据的永不过期,需要定期清理缓存中的无用数据,以避免占用过多内存。
使用分布式锁可以解决缓存穿透问题。缓存穿透是指查询一个不存在的数据,由于缓存和数据库都没有命中,导致每次请求都需要从数据库中读取数据,增加了数据库的负担。而分布式锁可以在多台服务器之间协调对某个资源的操作,保证同一时间只有一个线程可以对该资源进行操作。
引入分布式锁框架,如 Redis 的分布式锁、Zookeeper 等。
在查询数据前先使用分布式锁进行加锁,保证只有一个线程能够访问数据库,其他线程需要等待锁释放后才能进行查询。
如果查询结果为空,则释放锁并返回空对象;如果查询结果不为空,则将结果存入缓存中。
在更新数据时,也需要使用分布式锁进行加锁,保证只有一个线程能够对缓存进行更新。
在释放锁时,需要确保所有线程都已经完成了对数据的处理。
客户端尝试获取锁,向Redis服务器发送SETNX命令(SET if Not eXists)。
Redis服务器收到SETNX命令,尝试为客户端设置锁,如果该锁不存在,Redis会将锁设置为1,表示客户端获取了锁;否则,Redis返回0,表示客户端未能获取锁。
客户端收到Redis服务器返回的结果,如果结果为1,则表示客户端已经成功获取了锁,可以执行后续操作;如果结果为0,则表示客户端未能获取锁,需要再次尝试获取或者等待其他客户端释放锁。
客户端在执行完任务后,需要释放锁,向Redis服务器发送DEL命令,告诉Redis服务器该客户端已经完成任务,锁不再需要。
Redis服务器收到DEL命令,将该客户端的锁删除,其他客户端可以继续尝试获取锁。
需要注意的是,在分布式环境中,需要使用带有超时时间的锁,以防止锁死。在获取锁时,需要设置一个超时时间,如果在指定时间内未能成功获取锁,则需要放弃获取锁。同时,在释放锁时,需要检查该锁是否属于当前客户端,避免误删其他客户端的锁。


@Override
public Result queryShopById(Long id) {
//1 从redis获取
String shopStr = redisTemplate.opsForValue().get(RedisKey.CACHE_SHOP_PRE + id);
if (MyStrUtil.isNotEmpty(shopStr)) {
return Result.ok(JSONUtil.toBean(shopStr, Shop.class));
}
Shop shop = this.getById(id);
if (shop == null) {
return Result.fail("店铺不存在");
}
redisTemplate.opsForValue().set(RedisKey.CACHE_SHOP_PRE + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
return Result.ok(shop);
}
@Override
public Result queryShopById(Long id) {
//1 从redis获取
String shopStr = redisTemplate.opsForValue().get(RedisKey.CACHE_SHOP_PRE + id);
if (StringUtils.isNotBlank(shopStr)) {
return Result.ok(JSONUtil.toBean(shopStr, Shop.class));
}
//空字符串
if(shopStr != null){
return Result.fail("店铺不存在");
}
Shop shop = this.getById(id);
if (shop == null) {
redisTemplate.opsForValue().set(RedisKey.CACHE_SHOP_PRE + id, "", 1, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
redisTemplate.opsForValue().set(RedisKey.CACHE_SHOP_PRE + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
return Result.ok(shop);
}
gitee.com/charlinchenlin/koo-erp