🔥作者主页 :小林同学的学习笔录
🔥mysql专栏 :小林同学的专栏
目录
[1.1 短信登录](#1.1 短信登录)
[1.1.1 基于Session实现登录](#1.1.1 基于Session实现登录)
[1.1.2 集群的session共享问题](#1.1.2 集群的session共享问题)
[1.1.3 基于Redis实现共享session登录](#1.1.3 基于Redis实现共享session登录)
[1.1.4 登录拦截器的优化](#1.1.4 登录拦截器的优化)
[1.2 商户查询缓存](#1.2 商户查询缓存)
[1.2.1 什么是缓存](#1.2.1 什么是缓存)
[1.2.2 添加Redis缓存](#1.2.2 添加Redis缓存)
[1.2.3 缓存更新策略](#1.2.3 缓存更新策略)
[1.2.3.1 主动更新策略](#1.2.3.1 主动更新策略)
[1.2.4 缓存穿透](#1.2.4 缓存穿透)
[1.2.5 缓存雪崩](#1.2.5 缓存雪崩)
[1.2.6 缓存击穿](#1.2.6 缓存击穿)
[1.2.7 缓存工具封装](#1.2.7 缓存工具封装)
主打逻辑清晰有条理,代码实现可以找我要
小林同学学JAVA,不是干货不制作https://blog.csdn.net/2301_77358195
1.Redis
Redis数据库很多人知道,但是对于它的作用还是有点不清晰,一张图让你了解
1.1 短信登录
资源获取:可以找我要
启动项目后,在浏览器访问:http://localhost:8081/shop-type/list ,如果可以看到数据则证明运行
没有问题
启动nginx,然后访问:http:// http://localhost:8080 ,即可看到页面
1.1.1 基于Session实现登录
threadlocal: Java Web 应用程序中使用 ThreadLocal 来保存用户登录信息。这样做的好处是,每
个线程(即每个请求)都可以访问自己独立的用户登录信息,而不会与其他线程的登录信息混淆。
用户退出时清除 ThreadLocal 数据。
有一个问题就是,当我修改页面的信息并且退出用户,然后继续登录该用户是否会数据同步问题?
这其实要是不关threadlocal的事情,threadlocal只不过是保证你这次登录请求里面的其他请求是否
会被拦截器放行,拦截器校验该threadlocal,如果是同一个用户就会选择放行。
数据同步的话,需要你把用户信息保存到数据库,然后第二次登录查看该用户信息,从而实现数据
同步。
①.接下来完成一下发送验证码的请求以及后端响应验证码
主要逻辑:
- 1.验证手机号
- 2.如果手机号不符合则返回错误信息
- 3.手机号符合,生成验证码
- 4.把验证码保存到session
- 5.发送验证码
②.然后按登录又发送一个请求
主要逻辑:
- 1.检验手机号(因为是另外一个请求,有可能电话号码被改了)
- 2.校验验证码
- 3.验证码不一致或者为null,返回错误信息
- 4.验证码一致根据手机号查询用户
- 5.判断用户是否存在
- 6.不存在,就创建用户并保存到数据库
- 7.将用户信息保存到session,但是只保存DTO(用户的id和名称),隐藏用户的敏感信息
③.点完登录页面
前端再一次发送请求后会携带sessionId访问后端
为了不让请求直接请求到对应的controller,要配置一个拦截器进行请求的转发到不同的controller
拦截器的主要逻辑:
- 1.获取session
- 2.获取session的用户
- 3.判断用户是否存在
- 4.用户不存在,拦截并返回401的代码
- 5.存在,把用户信息保存到threadlocal
- 6.放行
1.1.2 集群的session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数
据丢失的问题。
session的替代方案应该满足:
- 数据共享
- 内存存储
- key、value结构
1.1.3 基于Redis实现共享session登录
改进后的效果
①.接下来完成一下发送验证码的请求以及后端响应验证码
主要逻辑:
- 1.验证手机号
- 2.如果手机号不符合则返回错误信息
- 3.手机号符合,生成验证码
- 4.把验证码保存到redis,并且设置验证码的有效期
- 5.发送验证码
②.然后按登录又发送一个请求
主要逻辑:
- 1.检验手机号(因为是另外一个请求,有可能电话号码被改了)
- 2.从redis获取验证码并校验
- 3.验证码不一致或者为null,返回错误信息
- 4.验证码一致根据手机号查询用户
- 5.判断用户是否存在
- 6.不存在,就创建用户并保存到数据库
- 7.保存用户到redis中
- 7.1 随机生成token,作为登录的令牌
- 7.2 将User对象转为HashMap
- 7.3 存储对象到HashMap
- 7.4 设置toke有效期为30分钟
- 8.返回token给客户端,这里一定要返回给客户端
拦截器的主要逻辑:
1.获取session
2.基于TOKEN获取redis中的用户
3.判断用户是否存在
4.用户不存在,拦截并返回401的代码
5.将查询到的Hash数据转为UserDTO对象
6.存在,把用户信息保存到threadlocal
7.刷新token的有效期
8.放行
拦截器刷新token有效期的原因是为了仿造session,如果在30分钟内没有任何活动,tomcat会删除该sessionId,需要用户重新登录,
如果超出没走拦截器redis对于前端的token信息将被删除,从而需要重新登录
如果走拦截器就会一直刷新token,防止登录的时候设置token时间为30分钟,超过30分钟被强制退出
1.1.4 登录拦截器的优化
为什么需要优化呢?
因为我们只对应一个拦截器,并且该拦截器只拦截登录的路径,这就存在问题,如果用户活动其他
路径,但是token有效期并没有被刷新,因此需要再定义一个拦截器来拦截所有路径,给token更新
有效期
可以看到第一个拦截器负责更新token以及保存用户到ThreadLocal,第二个拦截器则只需要判断
ThreadLocal是否有用户,从而确定是不是要放行
1.2 商户查询缓存
1.2.1 什么是缓存
缓存是一种临时存储数据的技术,旨在提高数据访问速度和性能。当程序需要访问某些数据时,它
们通常会从缓存中获取数据,而不是直接从原始数据源(如数据库或网络)获取。
这样可以减少访问原始数据源的频率,从而降低系统的负载并提高响应速度。
缓存涉及的地方很广,比如:
1.2.2 添加Redis缓存
代码用到了许多hutool工具类方法
①.toBean(String jsonString, Class<T> beanClass)
JSONUtil.toBean(type, ShopType.class)
含义是将type字符串转成ShopType对象
②.toJsonStr(Object obj)
JSONUtil.toJsonStr(shopType)
含义是将shopType实例转成String类型
③.toList(String jsonArray, Class<T> elementType)
JSONUtil.toList(shopType, ShopType.class)
含义是将字符串转换为ShopType对象
主要逻辑:
- 1.从redis查询商铺
- 2.判断一下商铺存在不存在
- 3.redis商铺存在,返回商铺信息
- 4.redis商铺不存在,就去数据库查询
- 5.数据库中商铺不存在,返回错误信息
- 6.数据库商铺存在,将数据缓存到redis
- 7.返回数据
1.2.3 缓存更新策略
业务场景:
低一致性需求:使用内存淘汰机制。例如:修改比较少
高一致性需求:主动更新,并以超时剔除作为兜底方案。例如:修改操作比较多
1.2.3.1 主动更新策略
操作缓存和数据库时有三个问题需要考虑:
1.删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存
答案:可以看出删除缓存比较高效
2.如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等分布式事务方案
3.先操作缓存还是先操作数据库?
先删除缓存,再操作数据库
先操作数据库,再删除缓存
答案:先操作数据库,再删除缓存,这种方案线程安全性高
为什么先操作数据库,再删除缓存,这种方案线程安全性高?
案例:给查询商铺的缓存添加超时剔除和主动更新的策略
修改ShopController中的业务逻辑,满足下面的需求:
根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
根据id修改店铺时,先修改数据库,再删除缓存
1.2.4 缓存穿透
缓存穿透是指恶意访问者通过构造恶意的查询条件,使得每次查询都无法命中缓存,从而大量请求
直接穿透缓存到达数据库,导致数据库负载过大。
常见的解决方案有两种:
①.缓存空对象:
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
②.布隆过滤:
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能
注意:这里的空对象是"",不是null,null不是对象
解决缓存穿透
用缓存空对象解决缓存穿透
主要逻辑:
- 1.从redis查询商铺
- 2.判断一下商铺存在不存在
- 3.redis商铺存在,返回商铺信息
- 4.如果命中的是空值就直接返回错误信息,不用再去查数据库解决缓存穿透
- 5.redis商铺不存在,就去数据库查询
- 6.数据库中商铺不存在,返回错误信息前加上values=""的缓存
- 7.数据库商铺存在,将数据缓存到redis
- 8.返回数据
改变的地方有两点:
java
//4.然后在查数据库之前,判断是否为空值,如果为空值("")就返回错误信息,如果不为空值则去查找数据库
if(shopJson.equals("")){
//返回错误信息
return Result.fail("店铺不存在");
}
//6.如果店铺不存在,缓存空值,缓存时间尽量设计少点,因为有可能出现数据不一致问题
if (shop == null) {
//将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//返回错误信息
return Result.fail("店铺不存在");
}
总结:
缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤
- 增强id的复杂度(如雪花算法),避免被猜测id规律,因为可以通过过滤id,如果id格式不符合可以被过滤掉
- 加强用户权限校验
- 做好热点参数的限流
1.2.5 缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,
带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值,避免同一时间多个key失效问题
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
1.2.6 缓存击穿
缓存击穿是指一个缓存中不存在但数据库中存在的数据,在高并发情况下,当有大量请求同时访问
这个不存在于缓存但存在于数据库的数据时,这些请求会直接穿透缓存,去请求数据库,导致数据
库负载激增,甚至可能引起数据库压力过大而崩溃。
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效
了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
注意:热点数据,缓存预热需要提前加载。
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
java
//尝试获取锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
基于互斥锁方式解决缓存击穿问题
这里的互斥锁用字符串里面的setnx来实现,因为它只能有一个key,其他key是加不进去的
主要逻辑:
- 1.从redis查询商铺
- 2.判断一下商铺存在不存在
- 3.redis商铺存在,返回商铺信息
- 4.如果命中的是空值就直接返回错误信息,不用再去查数据库解决缓存穿透
- 5.实现缓存重建
- 5.1 获取互斥锁
- 5.2 判断是否获取成功
- 5.3 失败,则休眠并重试
- 5.4 成功,根据id查询数据库
- 6.数据库中商铺不存在,返回错误信息前加上values=""的缓存
- 7.数据库商铺存在,将数据缓存到redis
- 8.释放互斥锁
- 9.返回数据
基于逻辑过期方式解决缓存击穿问题
java
//设置逻辑时间
RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
主要逻辑:
- 1.从redis查询商铺
- 2.判断一下商铺存在不存在
- 3.商铺存在,返回商铺信息
- 4.命中,需要先把json反序列化为对象
- 5.判断是否过期
- 5.1 未过期,直接返回数据
- 6.已过期,缓存重建
- 6.1 获取互斥锁
- 6.2 判断是否获取锁
- 6.3 成功,开启独立线程,实现缓存重建
- 6.4 释放锁
- 6.5 返回过期的商铺信息
java
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1 未过期,直接返回数据
return shop;
}
总结:
1.2.7 缓存工具封装
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
java
@Slf4j
@Component
public class CacheClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
//创建一个线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
public void set(String key, Object values, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(values),time,unit);
}
//将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
public void setWithLogicalExpire(String key, Object values, Long time, TimeUnit unit){
//设置逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(values);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
//根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
String key = keyPrefix + id;
//1.从redis查询商铺
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断一下商铺存在不存在
//3.商铺存在,返回商铺信息
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, type);
}
//如果命中的是空值就直接返回,不用再去查数据库解决缓存穿透
if(json != null){
//返回错误信息
return null;
}
//4.查数据库
R r = dbFallback.apply(id);
//5.商铺不存在,返回错误信息
if (r == null) {
//将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//返回错误信息
return null;
}
//6.商铺存在,将数据缓存到redis
//stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),CACHE_SHOP_TTL, TimeUnit.MINUTES);
this.set(key,r,time,unit);
//7.返回数据
return r;
}
//缓存击穿,用逻辑时间解决方案
public <R,ID> R queryWithLoginExpire(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
String key = keyPrefix + id;
//1.从redis查询商铺
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断一下商铺存在不存在
//3.商铺存在,返回商铺信息
if (StrUtil.isBlank(json)) {
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1 未过期,直接返回数据
return r;
}
//6.已过期,缓存重建
//6.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean tryLock = tryLock(lockKey);
//6.2 判断是否获取锁
if (tryLock) {
//6.3 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
//重构缓存
try {
//查询数据库
R r1 = dbFallback.apply(id);
//写入redis
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lockKey);
}
});
}
//6.4 返回过期的商铺信息
return r;
}
//互斥锁解决缓存穿透
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
//尝试获取锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
}