缓存击穿的风险和应对
2022-09-04 本文已影响0人
右耳菌
1. 缓存击穿场景重现
2. 思路
思路: 查询之前先判断目标数据是否存在,不存在的直接忽略。将流量拦截于缓存和数据库之前。
3. 分析
-
**问题的本质: **
判断集合中是否存在某个元素。 -
类似的场景:
数据库设计中,海量数据查询,快速判断数据是否存在?
数十亿网站域名,快速判定网址不合规。
垃圾邮件快速判定;
爬虫应用中的URL地址去重场景; -
不合理的做法:
把数据全部加载到内存
4. 设计
目的: 减少内存占用
方式: 不保存所有ID信息,只在内存中做一个标记
布隆过滤器(Bloom Filter)
布隆过滤器(Bloom Filter)是1970年由布隆提出的。
它实际上是一个很长的二进制数组和一系列hash函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。
它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
5. 自研布隆过滤器
利用Redis特性和命令: bitmaps (SETBIT设置指定位置的值、GETBIT获取值)可以理解为这是Redis自带的二进制数组特性。
- 例子
package cn.lazyfennec.cache.redis.service;
import com.study.cache.redis.annotations.NeteaseCache;
import com.study.cache.redis.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@Service // 默认 单实例
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate; // spring提供jdbc一个工具(mybastis类似)
@Autowired
RedisTemplate redisTemplate;
// 生产环境:第一次初始化,后续,根据数据变化,自动维护
@PostConstruct // 演示需要 --- 启动的时候去初始化布隆过滤器。
public void init(){
// 1. 加载所有数据 TODO 演示
for (int i = 0; i < 1; i++) {
String userId = "10001";
int hashValue = Math.abs(userId.hashCode());
long index = (long) (hashValue % Math.pow(2, 32)); // 元素 和 数组的映射
// 设置Redis里面二进制数据中的值,对应位置 为 1
redisTemplate.opsForValue().setBit("user_bloom_filter", index, true);
}
}
/**
* 根据ID查询用户信息 (redis缓存,用户信息以json字符串格式存在(序列化))
*/
// @Cacheable(value="user", key = "#userId")// 返回值 存到redis: value -- key: user::10001
public User findUserById(String userId) throws Exception {
// 提前查询
int hashValue = Math.abs(userId.hashCode());
long index = (long) (hashValue % Math.pow(2, 32)); // 元素 和 数组的映射
Boolean result = redisTemplate.opsForValue().getBit("user_bloom_filter", index);
if(!result) {
System.out.println("数据不存在" + userId);
return null;
}
// 压根就不存在
// 1. 先读取缓存
Object cacheValue = redisTemplate.opsForValue().get(userId);
if (cacheValue != null) {
System.out.println("###从缓存读取数据");
return (User) cacheValue;
}
// 2. 没有缓存读取数据 --- 连接,并发 支撑非常小 ---
String sql = "select * from tb_user_base where uid=?";
User user = jdbcTemplate.queryForObject(sql, new String[]{userId}, new BeanPropertyRowMapper<>(User.class));
System.out.println("***从数据库读取数据");
// 3. 设置缓存
redisTemplate.opsForValue().set(userId, user);
return user;
}
@CacheEvict(value = "user", key = "#user.uid") // 方法执行结束,清除缓存
public void updateUser(User user) {
String sql = "update tb_user_base set uname = ? where uid=?";
jdbcTemplate.update(sql, new String[]{user.getUname(), user.getUid()});
}
/**
* 根据ID查询用户名称
*/
// 我自己实现一个类似的注解
@NeteaseCache(value = "uname", key = "#userId") // 缓存
public String findUserNameById(String userId) {
// 查询数据库
String sql = "select uname from tb_user_base where uid=?";
String uname = jdbcTemplate.queryForObject(sql, new String[]{userId}, String.class);
return uname;
}
}
6. 布隆过滤器图示
- 优点:
- 内存空间占用少;
- 缺点:
- 布隆过滤器需要不断维护,带来新的工作
- 布隆过滤器并不能精准过滤。
(布隆过滤器判定不存在,100%不存在,判断为存在,则可能不存在的。)
理论上Hash计算值是有碰撞的(不同的内容hash计算出同样的值),导致不存在的元素可能会被判断为存在
布隆过滤器并非拦截所有请求,意在将缓存击穿控制在一定的量!
如果觉得有收获就点个赞吧,更多知识,请点击关注查看我的主页信息哦~