短信登录
基于Session实现登录
流程:
发送短信验证码-->短信验证码注册登录-->校验登录状态(保存用户到ThreadLocal,方便后续使用)
不能每次请求服务都要进行登录状态校验,解决办法:拦截器
在Spring框架中,拦截器(Interceptor)可以通过实现HandlerInterceptor
接口或继承HandlerInterceptorAdapter
类来实现。拦截器通常用于在请求到达控制器之前进行预处理,例如身份验证、权限检查等。
1.创建拦截器(拦截器需要实现HandlerInterceptor
接口,并重写方法)
2.注册拦截器(通过WebMvcConfigurer
接口来注册拦截器。)
3.配置拦截器的排除路径(在实际应用中,某些接口(如登录、注册接口)不需要进行身份验证)
区分一下session,cookie和token
Cookie:Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来,服务不保存。每次请求时客户端带上cookie。服务器检查该Cookie,以此来辨认用户状态。
Session: 服务器在处理客户端请求过程中会创建session,并且为该session生存唯一的session ID。
服务器将session ID发送到客户端.当客户端再次请求时,就会带上这个session ID.服务器接收到请求之后就会一句Session ID 找到相应的Session ,完成请求.session是服务本地保存,发给客户端,客户端每次访问都带着,直接和服务的session比对
Token:Token是服务端生成的一串字符串,当作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token并将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码.token是 服务经过计算发给客户端的,服务不保存,每次客户端来请求,经过解密等计算来验证是否是自己下发的.
JWT:JWT不仅仅是一个生成Token的过程,它还提供了一种结构化、自包含、无状态的Token设计。验证Token时,服务器不需要查询数据库,直接解析和验证签名即可。
机制 | 存储位置 | 通信方式 | 生命周期 |
Cookie | 浏览器 | 自动Header携带 | 可设置过期时间 |
️ Session | 服务端 | SessionID传递 | 服务端控制 |
Token | 客户端 | 手动Header添加 | 令牌有效期决定 |
️ JWT | 客户端 | Bearer Token | 包含过期时间声明 |
集群Session共享问题
session的痛点(负载均衡导致的)
多态Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题
基于Redis实现共享session登录
发送验证码逻辑实现:
1.校验手机号
2.不符合返回错误信息
3.符合生成验证码
4.保存验证码到redis
5.发送验证码
登录验证逻辑:
1.校验手机号
2.不符合返回错误信息
3.从redis中获取验证码并校验
4.不一致报错,一致,根据手机号查询用户
5.判断用户是否存在
6.保存用户信息到redis
6.1随机生成token,作为登录令牌
6.2将User对象转为Hash存储
6.3存储
7.返回token
登录拦截器的优化
拦截器:
1.获取token
2.查询Redis的用户
3.保存到ThreadLocal
4.刷新token有效器
5.放行
问题:
如果用户登录以后一直请求的是不需要拦截的请求,那么token有效期没有刷新,即使用户一直在使用,也没有做到token更新
解决办法:双拦截器
第一个拦截器拦截所有请求-->第二个拦截器拦截需要登录的请求
商户查询缓存
什么是缓存
缓存:数据交换的缓冲区,是存储数据的临时地方,一般读写性能毕竟高
浏览器(浏览器缓存)--->Redis(应用层缓存)--->数据库(数据库缓存)
缓存的作用:
1.降低后端负载 2.提高读写效率,降低相应时间
缓存的成本
1.数据一致性成本 2.代码维护成本 3.运维成本
添加Redis缓存
客户端请求优先到达缓存,如果缓存命中直接返回数据,如果未命中,请求数据库,并且写入缓存
缓存更新:
内存淘汰:不用自己维护,内存不足时自动淘汰部分数据
超时剔除:给缓存数据添加TTL时间,到期后自动删除缓存
主动更新:当涉及到数据库的增删改时,主动更新Redis缓存
主动更新策略:
1.在更新数据库的同时更新缓存------常用
2.数据库和缓存整合为一个服务,由服务来维护一致性
3.调用者只操作缓存,由其它线程异步的缓存数据持久到数据库,保证最终一致
操作缓存和数据库时有三个问题需要考虑:
1.删除缓存还是更新缓存?
2.如何保证缓存与数据库的操作同时成功或失败?
3.先操作缓存还是先操作数据库?
都有可能出现线程不安全的问题。 方案二的不安全几率更低---常用
缓存穿透
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案:
缓存空对象:缓存null
优点:实现简单,维护方便
缺点:额外的内存消耗,可能造成短期的不一致
布隆过滤:
客户端-->布隆过滤器-->Redis
布隆过滤器:利用hash存储数据库里的字段
优点:内存占用少,没有多余key
缺点:实现复杂,可能存在误判可能
缓存雪崩
在同一时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大的压力
解决方案:
给不同的key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿
也叫热点Key问题,就是一个高并发访问并且缓存重建业务较复杂的key突然失效了,无效的请求访问会在瞬间给数据库带来巨大的冲击
常见的解决方案:
互斥锁
逻辑过期
缓存工具封装
方法1:将任意java对象序列化为json并存储在String类型的key中,并且可以设置TTL过期时间
方法2:将任意java对象序列化为json并存储在String类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期的方式解决缓存击穿问题
Public class CacheClient{
private StringRedisTemplate stringRedisTemplate;
public void set(String key, Object object,Long time,TimeUnit unit){
StringRedisTemplate.opsForValue().set(key,JSONUTIL.toJsonStr(value),time,unit)
}
public void setWithLogicalExpire(String key, Object object,Long time,TimeUnit unit){
//设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
StringRedisTemplate.opsForValue().set(key,JSONUTIL.toJsonStr(redisData))
}
public <R, ID> R queryWithPassThrough(Long time,TimeUnit unit,
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback){
String key = keyPrefix + id;
//1.从Redis中查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(json)){
return JSONUtil.toBean(json,type);
}
//3.判断命中的是否是空值
is(json != null){
return null;
}
//4.从数据库进行查询
R r = dbFallback.apply(id);
//5.不存在,返回错误
if(r == null){
//将空值写入redis
stringRedisRemplate.opsForValue().set(key,"",null,TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
this.set(key,r,time,unit);
}
public <R, ID> R queryWithLogicalExpire(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.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.反序列化为RedisData对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
// 4.判断是否逻辑过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.未过期,直接返回缓存数据
return r;
} else {
// 6.已过期,更新缓存
// 6.1 从数据库查询最新数据
R latestData = dbFallback.apply(id);
// 6.2 如果数据库中没有数据,返回null
if (latestData == null) {
return null;
}
// 6.3 更新缓存
RedisData newRedisData = new RedisData<>();
newRedisData.setData(latestData);
newRedisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(newRedisData));
// 6.4 返回最新数据
return latestData;
}
} else {
// 7.缓存中没有数据,从数据库查询
R r = dbFallback.apply(id);
// 8.如果数据库中没有数据,返回null
if (r == null) {
return null;
}
// 9.将数据写入缓存
RedisData redisData = new RedisData<>();
redisData.setData(r);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
// 10.返回查询结果
return r;
}
}
}
优惠劵秒杀
全局唯一ID
全局ID生成器
是一种在分布式系统统一下来生成全局唯一ID的工具,一般要满足以下特性:
唯一性 高可用 高性能 递增性 安全性
为了增加ID的安全性,我们可以不适用Redis自增的数值,而是拼接一些其他信息
符号位+时间戳+序列号
实现优惠卷秒杀下单
下单时需要判断两点:
1.秒杀是否开始或结束
2.库存是否充足
超卖问题
在高并发的场景下,多个线程同时发送请求,此时库存还够,每个线程都认为库存还够,每个线程都执行了优惠卷下单,此时库存出现超卖问题。-->并发安全问题
解决方案:
悲观锁vs乐观锁
乐观锁的关键时判断之前查询得到的数据是否有被修改过,常见的方式有两种
版本号法(对数据修改以后版本号加1,修改数据前先判断是否和查询到的版本号对应)
CAS法(在修改前判断查询到的数据是否和之前查询到的数据保持一致-->改进:只要库存大于0就行)
乐观锁的弊端:成功率太低
一人一单
要求同一个优惠卷,一个用户只能下一单
根据优惠卷id和订单id进行查询-->判断订单是否存在-->如果订单存在,不允许下单
同样存在多线程并发的问题,解决方案:悲观锁
通过加锁可以解决在单机情况下的一人一单安全问题,但在集群模型下就不行了
在JVM内部只有一个锁监视器
分布式锁
分布式锁概述
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
满足的基本特性:
多进程可见 互斥 高可用 安全性 高性能
基于Redis的分布式锁
实现分布式锁需要实现的两个基本方法:
获取锁:
互斥:确保只有一个线程获取锁
1.添加锁:SETNX lock thread1
2.添加锁过期时间,避免服务宕机引起的死锁:EXPIRE lock 10
3.合并:SET lock thread1 NX EX 10
释放锁:
1.手动释放
2.超时释放:DEK key
基于Redis实现分布式锁1
public interface ILock {
boolean tryLock(long timeoutSec);
void unlock();
}
特殊情况依然存在安全问题
基于Redis实现分布式锁2
改进Redis的分布式锁:
1.在获取锁时存入线程标示(可以用UUID表示)
2.在释放锁的时先获取锁中的线程标示,判断与当前线程标示是否一致一致则释放锁,不一致则不释放锁
特殊情况依然存在安全问题
基于Redis实现分布式锁3
Redis的Lua脚本,Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行的原子性
Lua脚本:
if(redis.call('get',KEYS[1] == ARGV[1])) then
return redis.call('del',KEY[1])
end
return 0
总结
分布式锁Redisson
基于setnx实现的分布式锁存在下面的问题:
1.不可重入:同一个线程无法多次获取同一把锁
2.不可重试:获取锁只尝试一次就返回false,没有重试机制
3.超时释放:业务执行耗时较长,也会导致锁释放,存在安全隐患
4.主从一致性:如果Redis提供了主从集群,主从同步存在延迟,主宕机时,如果从并同步主中的锁数据,则会出现锁实现
Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网地址: https://redisson.org
Redisson入门:
1.引入依赖
2.配置Redisson客户端
Redisson可重入锁原理
在获取锁的时候,判断一下当前拥有锁的是否是当前线程,如果是在同一线程,只需要把重入的次数+1
释放锁的时候,重入次数-1,重入次数减为0时,释放锁。
Redisson分布式锁主从一致性问题
解决:简单粗暴
总结:
Redis秒杀优化
逻辑实现
逻辑业务串行执行,所用时间为所需业务时间之和
优化思路:
主线程负责进行资格判断,开启新的线程处理耗时比较长的对订单的后续操作
主线程进行资格判断
把下单信息封装成对象-->将下单信息放入阻塞队列-->开启独立线程实现异步下单
Redis消息队列实现异步秒杀
消息队列:字面意思就是存放消息的队列。
生产者:判断秒杀时间和库存 校验一人一单 发送优惠卷id和用户id到消息队列
消费者:接收消息,完成下单
Redis提供了三种不同的方式来实现消息队列
List结构:基于List结构模拟消息队列
PubSub:基于点对点消息模型
Stream:比较完善的消息队列模型
基于List结构模拟消息队列
基于PubSub的消息队列
基于Stream的消息队列
基于Stream的消息队列——消费者组
总结
达人探店
点赞
需求:
一个人只能对同一篇笔记进行一次点赞,如果点赞完以后则取消点赞
如果当前用户已经点赞,则点赞按钮高亮显示
点赞排行榜
将时间戳作为score传入,实现按照点赞时间排序
好友关注
需求:
1.关注和取关接口
2.判断是否关注的接口
3.共同关注:利用Redis的set的求交集功能
Feed流实现方案分析
Feed流的实现方案1:
拉模式:也叫做读扩散
推模式
推拉结合模式