目录
前言
该项目是黑马得免费项目 请官网领取 点我免费获取
基于Session登录
业务逻辑:
学到的点:
ThreadLocal
在每一个线程里面 进行独立的map存放数据信息 ,对于一些需要存放在本地的变量 ,以防多线程出现的安全隐患顺便方便同一个线程多次访问所需要的数据。
由于是弱引用 所以在使用完以后 我们应该记得进行清理:
下图是在进行拦截的时候的操作
糊涂工具包的使用
很多有用的简便方法值得使用
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
集群Session问题
发生的原因:
多台tomcat之间 不能共享Session数据,当进行切换时则会发生问题。
解决方案:
用redis 因为Session 里面放的也是 key -value Redsi刚好满足
流程:
短信验证码的流程修改:
思路
原本是将验证码存放在 session中存取验证,但是由于共享的问题转为Redis进行存放。
现在:采用String,String的形式存放,并且以手机号作为关键的Key实现唯一性。
User对象存放的流程修改:
思路
原来的思路: 将对象存放到session之中,服务器会给每一个session一个id,存放到用户的浏览器的cookie之中,下次根据这个sessionId,获取到用户的信息,以达到免登录的效果,但是还是由于集群的问题,此方案废弃
现在的思路: 在存放对象时,生产随机的token当作Key,随后传给前端,前端在将token写入,每次请求的时候都带着这个token。存放的方式 采用 hash进行存放方便后续的修改:
总结的点:
1: 选取合适的数据类型 进行存放: 比如对象的存放可以存json也可以用hash,取决与你这么进行判断
2: key的选择 ,要合适确保唯一性
3: 选择合适的粒度,一些没用的信息就不要进行存放了
4: 一些key的时间注意设置,不易过长
添加缓存
流程:
代码:
public Result queryById(Long id) {
//1:从redis 查询缓存
String shopjson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
//2:判断是否存在
if (StrUtil.isNotBlank(shopjson)){
//3:存在 返回
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return Result.ok(shop);
}
//4:不存在去数据库查
Shop shop = query().eq("id", id).one();
if (shop==null){
//5:数据库不存在 报错
return Result.fail("店铺不存在");
}
//6:数据库存在 存到缓存
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop));
//7:返回
return Result.ok(shop);
}
缓存更新:
最佳方案:
实施代码:
首先是 对查询的操纵:
接着是对 更新的操作:
注意要加事务注解
确保 更新数据库与删除缓存的原子性
缓存穿透
产生的原因
用户访问缓存和数据库中都不存在的数据时,缓存永远不会生效,这些请求则会打到数据库上。
解决方案:
方案1:缓存空对象:
实现逻辑:
原理:
就是没有找到,我也进行缓存,只不过存放的是null值,并且设置合适的过期时间,以防止内存占用过大。
方案2:布隆过滤
原理:
核心原理在于 布隆过滤器,其工作原理是,对数据库的key进行hash,然后根据hash来判断有没有,没有则不会进行,有则进行相应的操作。
这样导致,可能存在错误的判断情况。
方案1的实战:
流程的修改;
源码
public Result queryById(Long id) {
//1:从redis 查询缓存
String shopjson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
//2:判断是否存在
if (StrUtil.isNotBlank(shopjson)){
//3:存在
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return Result.ok(shop);
}
//如果是空值(不是null 就是“”) 提前返回结果 步走数据库
if (shopjson!=null){
return Result.fail("店铺id不存在");
}
//4:不存在去数据库查
Shop shop = query().eq("id", id).one();
if (shop==null){
//5:数据库不存在 进行 空值缓存
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//6:数据库存在 存到缓存
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7:返回
return Result.ok(shop);
}
总结:
缓存雪崩
第一个方案 就是 让 ttl 尽量 的分散 避免同时失效。
缓存击穿
方案一:互斥锁
方案二:逻辑过期
主要是 开辟一个新的线程,进行处理 ,先用旧的数据顶替一下
方案1实战
流程的修改
源码
private Result huancunJiChuan(Long id){
//1:从redis 查询缓存
String shopjson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
//2:判断是否存在
if (StrUtil.isNotBlank(shopjson)){
//3:存在
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return Result.ok(shop);
}
//如果是空值(不是null 就是“”) 提前返回结果 步走数据库
if (shopjson!=null){
return Result.fail("店铺id不存在");
}
Shop shop = null;
try {
//4:不存在去数据库查
//获取锁 失败休眠
if (!tryLock(RedisConstants.LOCK_SHOP_KEY+id)){
Thread.sleep(50);
return huancunJiChuan(id);
}
//4.2成功开始操作数据库
//模拟延迟
Thread.sleep(200);
shop = query().eq("id", id).one();
if (shop==null){
//5:数据库不存在 进行 空值缓存
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//6:数据库存在 存到缓存
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException exception) {
throw new RuntimeException();
} finally {
//8:释放锁
unlock(RedisConstants.LOCK_SHOP_KEY+id);
}
//9:返回结果
return Result.ok(shop);
}
方案2实战
流程修改
源码:
主要逻辑代码
private final static ExecutorService EXECUTOR_SERVICE= Executors.newFixedThreadPool(10);
private Result huancunJiChuanByLuoJI(Long id) {
//1:从redis 查询缓存
String redisDataJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
//2:判断是否存在
if (StrUtil.isBlank(redisDataJson)){
//3 不存在 返空
return Result.fail("店铺id不存在");
}
//4 命中 转换成对象 因为不论过不过其最后都要返回这个值
RedisData redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
//由于这个是 shop是个 object类型 所以 给你会转成 jsonObject 需要手动转一下
Shop shop=JSONUtil.toBean((JSONObject) redisData.getData(),Shop.class);
//5 判断是否过期
if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
//5.1 未过期 直接返回值
return Result.ok(shop);
}
//5.2 过期 开始缓存重建
//6缓存重建
//6.1 获取互斥锁
//6.2 判断是否成功获取互斥锁
if (tryLock(RedisConstants.LOGIN_USER_KEY+id)){
//6.3 成功 开启独立线程 构造缓存
EXECUTOR_SERVICE.submit(()->{
try {
setShop2Redis(id,20L);
} catch (InterruptedException exception) {
exception.printStackTrace();
}finally {
//释放锁
unlock(RedisConstants.LOCK_SHOP_KEY+id);
}
});
}
//6.4成功 失败都返回过期数据
return Result.ok(shop);
// return Result.ok(redisData.getData());
}
上锁等小功能代码:
//设置过期时间
public void setShop2Redis(Long id,Long experSecondes) throws InterruptedException {
//封装数据
Shop shop=getById(id);
//模拟延迟
Thread.sleep(200);
//封装过期时间
RedisData redisData=new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(experSecondes));
//写入redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
//上锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
//由于 这个是包装类可能存在 null的情况 所以进行转化一下。
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
测试:
测试之前准备:
首先 :在执行之前 写入数据:时间是 10秒过期 我们等十秒后开始测试
这是执行后缓存与数据库的情况
然后 修改数据库,让其与缓存不一致
预期效果:
因为实现了逻辑过期 并且 在执行数据库的时候会进行 200毫秒的延迟 ,我们使用 jmter进行测试 前面的数据 应该还是 老数据zyc 而过了200毫秒后的数据应该是 wyy了
实际结果;
缓存工具的封装
对于阅读了上述的这么多案列,我们不难发现很多的方法是重复的可以进行封装处理的,比如几个常见的:
按照自己需求进行相关功能的封装。