黑马点评
- 1、短信登录 Session实现
- 1.1 分析
- 1.2、Session实现的缺点
- 1.3、其中的问题
- 1.3.1、session覆盖
- 1.3.2、在拦截之后remove User的作用
- 2、用Redis实现短信登录
- 2.1 分析
- 2.2 代码以及问题
- 2.2.1 String问题
- 2.2.2 刷新问题
- 2.2.3 注入对象问题
- 2.2.4 拦截器order问题
- 3、缓存
- 3.1 商户信息的redis保存
- 3.2 redis 商户列表redis缓存
- 3.3 缓存更新策略
- 实践 商户信息一致性
- 3.4 缓存穿透
- 利用缓存空对象来解决缓存穿透问题
- 3.5 缓存雪崩
- 3.6 缓存击穿
- 需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
- 需求:修改根据id查询商铺的业务,基于逻辑过期时间方式来解决缓存击穿问题
- 3.7 工具类封装
- 4、优惠券秒杀
- 4.1 全局唯一ID
- 4.1.1 id生成器的特性
1、短信登录 Session实现
1.1 分析
部分代码:
1.2、Session实现的缺点
redis可以数据共享,且redis是内存存储,key-value结构。
1.3、其中的问题
1.3.1、session覆盖
看弹幕有人提到说如果多个用户登录,会不会出现session覆盖问题,都保存成属性值一样的情况下
回答 :不会,因为请求到服务器中,是基于session-id保存的,每个session-id的属性值相同不会有影响,是基于用户的。
1.3.2、在拦截之后remove User的作用
防止内存泄漏,如果当前线程仍然使用,但是却不在使用theardLocal,那么如果我们没有remove的话,即使key是弱引用,theadLocal 为null,但是key仍然有值,所以最好的办法就是手动remove。
2、用Redis实现短信登录
2.1 分析
使用redis着重要考虑:
①键要唯一
②如果存储键,方便我们后续的读取。
在这里,保存code时,首先数据类型使用的是String,在 redis中使用phone,刚好登录时,会提交phone和code,根据提交的code可以在redis中查找到code,然后验证。
保存用户信息时,使用的数据类型是Hash,Hash可以存储对象,当然也可以使用String 的json字符串进行保存。key这里使用的是一个随机字符串,那后续如何获取呢?我们可以手动把其放在token中,前端获取后端返回的数据保存token中,接着,每次发送ajax请求时,都会携带到请求头中,key为authorization。
2.2 代码以及问题
2.2.1 String问题
id是Long型,但是BeanToMap的值是String,Object,并且我们使用的是StringRedisTemplate,这要求我们保存到redis中的值为String类型,这时候就需要我们显示的将值转为String。
基本类型与对象类型: Java 中的基本类型(如 int、long、boolean)和它们的包装类型(如 Integer、Long、Boolean)与 String 之间的转换需要明确的语法,因为 Java 是强类型语言,编译器不会自动执行这些转换。
2.2.2 刷新问题
我们保证一个用户的活跃度,这时候应该是只要其访问了网页就要刷新其时常,所以不应该只在login时刷新,也不应该只在登录校验的几个网页做刷新。而应该在在所有的请求时都做刷新,这时候需要再有一个拦截器。对所有网页都刷新,但是这个拦截器不做登录校验功能,而是如果获取到了用户,把用户信息保存在Local中,不管咋样,都放行,交给登录校验的拦截器。
2.2.3 注入对象问题
使用 new 创建的对象不能通过 Spring 的 @Resource、@Autowired 或其他依赖注入注解进行注入。这是因为 Spring 的依赖注入机制依赖于容器管理的 Bean。通过 new 创建的对象不在 Spring 容器的管理范围内,因此无法享受到容器提供的特性(如生命周期管理、AOP、事务管理等)。但我们可以通过构造函数来注入。
2.2.4 拦截器order问题
哪个先注册在前面先执行哪一个,默认order都是0.可以通过order来决定先执行哪一个,值越小优先级越高。
3、缓存
3.1 商户信息的redis保存
使用了String
3.2 redis 商户列表redis缓存
使用了List
3.3 缓存更新策略
主动更新时 我们都选择自主更新就是由代码开发者 自己确定更新缓存
数据库数据变更时,我们选择删除缓存,而不是更新缓存,删除缓存的好处:数据库数据变更时,我们直接删除缓存,比如更新100次,只用删除一次就行了,查询时在更新缓存,但是如果更新缓存的话,就是导致,数据库数据变更100次那么缓存就要更新100次,如果中间没有读操作,就会导致无效的写操作。
我们一般都会选择先操作数据库在删除缓存。
第一种情况:先删除缓存,再操作数据库这种误差的机率比较大,因为更新数据库的操作慢,但是写缓存的速度快,在多线程的情况下,这种出现的概率比较大。
但是第二种情况:先操作数据,在删除缓存,由于更新数据库的速度比较慢,所以一般不会这样子进行,并且,即使写入缓存导致不一致了,也可以加入超时策略,作为兜底方案。
实践 商户信息一致性
查询操作时,添加过期时间
更新操作时
3.4 缓存穿透
查询商铺时的信息修改逻辑
当数据库不存在时,则把其存入到redis当中。存一个空字符串。
查询到时 如果是空值 则直接结束。
记得设置一个过期时间,因为在这中间可能会在数据库中插入信息。
利用缓存空对象来解决缓存穿透问题
3.5 缓存雪崩
3.6 缓存击穿
需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于逻辑过期时间方式来解决缓存击穿问题
3.7 工具类封装
如果每个都需要我们去写缓存击穿缓存穿透的话就很麻烦,我们可以封装一个工具类
工具类
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
/**
* @ClassName ChcheClicnt
* @Description TODO
* @Author lukcy
* @Date 2024/10/10 17:16
* @Version 1.0
*/
@Slf4j
@Component
public class ChcheClicnt {
@Resource
private StringRedisTemplate stringRedisTemplate;
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
private static final ExecutorService CACHAE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
public void setWithLogicExpire(String key, Object value, 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 queryWithPassThough(String prifix, ID id, Class<R> type, Function<ID,R> DBcallback,Long time, TimeUnit unit){
String key=prifix+id;
//查询redis中是否有
String Json = stringRedisTemplate.opsForValue().get(key);
//有直接返回
if (StrUtil.isNotBlank(Json)) {
R r = JSONUtil.toBean(Json, type);
return r;
}
//redis中保存的是空值
if(Json!=null){
return null;
}
//没有 查询数据库
R r = DBcallback.apply(id);
//数据库中没有 保存空值设置短的过期时间 返回错误
if (r == null) {
stringRedisTemplate.opsForValue().set(key,"",2L, TimeUnit.MINUTES);
return null;
}
//有,保存Json形式在redis中 设置过期时间
this.set(key,r,time,unit);
//返回
return r;
}
public <R,ID> R queryWithExpire(String prifix, ID id, Class<R> type, Function<ID,R> DBcallback,Long time, TimeUnit unit){
String key=prifix+id;
//查询redis中是否有
String Json = stringRedisTemplate.opsForValue().get(key);
//没有 直接返回
if (StrUtil.isBlank(Json)) {
return null;
}
//有 判断是否过期 把json反序列化为对象
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
//没有过期
if(expireTime.isAfter(LocalDateTime.now())){
return r;
}
//过期
String lockkey=LOCK_SHOP_KEY+id;
// 尝试获取锁
if (!tryLock(lockkey)) {
//获取锁失败
return r;
}
//获取锁成功
CACHAE_REBUILD_EXECUTOR.submit(()->{
//开启线程 重建缓存
//查数据库
try {
R r1 = DBcallback.apply(id);
//存入缓存
this.setWithLogicExpire(key,r1,time,unit);}
catch (Exception e) {
e.printStackTrace();
} finally {
deleteLock(lockkey);
}
});
//返回
return r;
}
private boolean tryLock(String key){
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
private void deleteLock(String key){
stringRedisTemplate.delete(key);
}
}
运用
public Result queryById(Long id) {
//缓存穿透 利用缓存空值解决
// Shop shop=chcheClicnt.queryWithPassThough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
//缓存击穿 利用互斥锁解决
// Shop shop = queryWithMeutx(id);
Shop shop=chcheClicnt.queryWithExpire(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
if(shop==null){
return Result.fail("该商铺不存在!");
}
return Result.ok(shop);
}
4、优惠券秒杀
4.1 全局唯一ID
用数据库自增长的话,规律太明显,并且一个系统,生成订单的量会日益增多,一个数据库容量可能不够,如果分到多个表中,由于运用的是数据库的自增长,可能会导致id相同。
4.1.1 id生成器的特性
我i们不直接使用redis的自增长,需要拼接一些其他信息,比如时间戳,这需要我们有一个基础的开始时间,还有序列号。
时间戳的话即使是一秒下单的,后续的序列号也有32位,一秒下单量是足够的。
还有一个问题就是reids自增的key,如果我们用的是一个key,即使不同业务用的key不同, 但是相同业务用一个key的话,也会一直自增,redis自增也是有上限的,64位,可能会超。因此我们使用的key精确到天,这样也方便我们统计每天的下单量。