Redis实战笔记
文章目录
前言
该笔记是根据黑马的Redis视频中的实战篇而记录的笔记,不仅有课上的ppt图片,也有我自己对课程内容的理解,并且包括所有的源代码
。
思维导图
短信验证码的登录注册功能
基于Session实现登录
实现发送短信验证码功能
@Override
public Result sendCode(String phone, HttpSession session) {
//1. 校验手机号(正则表达式)
// RegexUtils.isPhoneInvalid(phone)是项目中已提前写好的,校验手机号格式的方法,内部也是用正则表达式,返回的布尔值是判断是否无效
if (RegexUtils.isPhoneInvalid(phone)) {
// 1.1 返回给前端 报错信息
return Result.fail("手机号格式错误");
}
//2. 生成验证码
String code = RandomUtil.randomNumbers(6);
//3. 保存验证码到session
session.setAttribute("code",code);
//4. 发送验证码 (可能会用到第三方短信服务,这里的项目就不演示了)
log.debug("短信验证码是: {}",code);
return Result.ok();
}
实现短信验证码登录功能
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 这个验证是有问题的,因为他是将手机号和验证码分开验证的,并没有关联验证,
// 如果手机号发生改变的话,也会登录成功,在下一个版本会用redis代替
// 1. 从 loginForm里面拿到手机号,验证码进行校验
String phone = loginForm.getPhone();
Object code = session.getAttribute("code");
if(code==null||!code.toString().equals(loginForm.getCode())){
// 1.1 返回给前端 报错信息
return Result.fail("验证码错误");
}
if (RegexUtils.isPhoneInvalid(phone)) {
// 1.1 返回给前端 报错信息
return Result.fail("手机号格式错误");
}
// 2.根据手机号查用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("phone",phone);
User one = this.getOne(wrapper);
// 3.如果用户不存在,创建新用户
if(one==null){
one= createUserWithPhone(phone);
}
// 4.如果用户存在,保存用户脱敏信息到session
session.setAttribute("user",one);
return Result.ok();
}
实现校验登录状态功能(登录拦截器)
idea 查看接口的方法的快捷键 Ctrl+I
不熟悉 ThreadLocal的小伙伴可以看这篇文章 https://zhuanlan.zhihu.com/p/102744180
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从session中获取用户信息
Object user = request.getSession().getAttribute("user");
// 2.判断用户是否存在
if (user == null) {
// user为空,则登录信息不存在,进行拦截
// 这里不需要再对数据库进行查询了,因为如果这个登录信息不在数据库中,他就不会登录成功。
response.setStatus(404);
return false;
}
//3.保存用户的脱敏信息到ThreadLocal中
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties((User)user,userDTO);
UserHolder.saveUser(userDTO);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 操作完成后,将登录的用户信息移除调,避免用户泄露
UserHolder.removeUser();
}
}
public class MVCConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
//对于不需要登录也能访问的路径进行放行
.excludePathPatterns(
"/user/code",
"/user/login",
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/blog/hot",
"upload/**"
);
}
}
基于Session实现登录的问题
存在session共享问题:多台 Tomcat并不共享session,当请求切换到不同 tomcat服务时导致数据 丢失的问题
下面是刚才登录功能的架构图,如果用户在第一个tomcat上登录了,然后请求的服务发到了第二个服务,第二个
tomcat上的session上没有登录信息,会导致请求失败
那如果要实现session共享,要满足的条件如下
- 数据共享
- 内存存储
- key,value结构
那用redis就可以解决这个问题
基于Redis实现共享session登录
我们这里在Redis里存对象,用Hash结构,而不是用json来存储,因为Hash结构可以将对象中每个字段独立存储,可以对单个字段做CRUD,内存占用更少。
其实用JSON字符串保存也可以,比较直观,这里就用Hash结构来存储。
这里用随机token来作为key存储对象数据,也是为了防止有人利用phone来获取用户信息
@Override
public Result sendCode(String phone, HttpSession session) {
//1. 校验手机号(正则表达式)
// RegexUtils.isPhoneInvalid(phone)是项目中已提前写好的,校验手机号格式的方法,内部也是用正则表达式,返回的布尔值是判断是否无效
if (RegexUtils.isPhoneInvalid(phone)) {
// 1.1 返回给前端 报错信息
return Result.fail("手机号格式错误");
}
//2. 生成验证码
String code = RandomUtil.randomNumbers(6);
//3. 保存验证码到redis, 相当于 set key value ex 120
// 为了防止其他业务也是用手机号作为key,我们这里要给他加一个业务名前缀
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//4. 发送验证码 (可能会用到第三方短信服务,这里的项目就不演示了)
log.debug("短信验证码是: {}",code);
return Result.ok();
}
Redis中的键值对
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 这个验证是有问题的,因为他是将手机号和验证码分开验证的,并没有关联验证,
// 如果手机号发生改变的话,也会登录成功,在下一个版本会用redis代替
// 1. 从 loginForm里面拿到手机号,从redis中拿到校验码
String phone = loginForm.getPhone();
String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if(code==null||!code.equals(loginForm.getCode())){
// 1.1 返回给前端 报错信息
return Result.fail("验证码错误");
}
if (RegexUtils.isPhoneInvalid(phone)) {
// 1.1 返回给前端 报错信息
return Result.fail("手机号格式错误");
}
// 2.根据手机号查用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("phone",phone);
User one = this.getOne(wrapper);
// 3.如果用户不存在,创建新用户
if(one==null){
one= createUserWithPhone(phone);
}
// 4.将用户脱敏信息保存到redis中
// 4.1 生成随机token
String token = UUID.randomUUID().toString(true);
// 4.2 将User对象转为Hash存储。
UserDTO userDTO = BeanUtil.copyProperties(one, UserDTO.class);
// 4.3 这里如果只是 Map<String, Object> userMap = BeanUtil.beanToMap(userDTO)
// ,由于userDTO的id是Long类型,用stringRedisTemplate操作时,无法将Long类型转为String类型会报错
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
// 4.3 存到redis中
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
// 4.4 一定要设置过期时间,但是这里不是说30分钟后无效,是用户只要访问,他的有效时间就是30分钟,而当用户30分钟不访问后,才失效
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.SECONDS);
return Result.ok();
}
Redis中的键值对
登录拦截器的优化
虽然用户信息在Redis中保存的时候设置过期时间了,但是这个时间是当用户信息保存到 redis 后,只要过了30分钟后,用户信息就删除了。但是我们希望的情况是当用户超过30分钟没有访问才删除。所以我们要对拦截器进行优化。
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
// 这里为什么要有一个构造器呢,是因为这个类没有交给 Spring 管理,Spring不会帮我们去创建他,我们要自己手动创建
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO 1. 获取请求头的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
response.setStatus(404);
return false;
}
// TODO 2.基于Token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
// 2.判断用户是否存在
if (userMap.isEmpty() ) {
response.setStatus(404);
return false;
}
// 5. TODO 将查到的用户从Hash转为userDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6. 保存
UserHolder.saveUser(userDTO);
// 7. 刷新token有效期,只要用户访问任何一个业务就刷新他的过期时间
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.SECONDS);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 操作完成后,将登录的用户信息移除调,避免用户泄露
UserHolder.removeUser();
}
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (UserHolder.getUser() == null) {
// 无用户,需要拦截
response.setStatus(401);
return false;
}
// 有用户,放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 操作完成后,将登录的用户信息移除调,避免用户泄露
UserHolder.removeUser();
}
}
商户查询缓存
什么是缓存?
缓存就是数据交换的缓存区,是存储数据的临时地方,一般读写性能较高
缓存的作用
- 降低后端负载(减少查询数据库,减少数据库的压力)
- 提高读写效率,降低响应时间
缓存的成本
- 数据一致性成本(当数据库的数据发生变化时,redis中以前缓存的还是旧数据)
- 代码维护成本(为了解决一致性,缓存击穿,缓存雪崩等问题,会提高代码复杂度)
- 运维成本
public Result queryById(Long id) {
//1. 从Redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断缓存是否命中
if (StrUtil.isNotBlank(shopJson)) {
//3.缓存命中直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.缓存未命中,根据商铺id查询数据库
Shop shop = this.getById(id);
if(shop==null){
//5. 数据库中查询失败,直接返回
return Result.fail("店铺不存在");
}
//6. 数据库中查询成功,将数据缓存到Redis中
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
小作业:将商铺类型缓存到Redis中
public Result queryTypeList() {
// 1.这个是首页就要开始查询,应该是从数据库查到数据传到redis
List<ShopType> sort = this.query().orderByAsc("sort").list();
List<String> collect = sort.stream().map((shopType ->
JSONUtil.toJsonStr(shopType))).collect(Collectors.toList());
stringRedisTemplate.opsForList().leftPushAll(CACHE_SHOP_LIST_KEY,collect);
return Result.ok(sort);
}
总结
这篇文章算是简单的说明了一下 Redis 在应用开发中的一些实用场景,但是这其中还有许多的场景bug,也就是一些我们没有想到的 undefined state,会导致我们 redis 缓存的数据不对,这些内容我们就放到下一篇来讲吧
我是Mayphyr,从一点点到亿点点,我们下次再见