目录
一、使用git管理项目代码
二、环境搭建
2.1、maven坐标
2.2、配置文件
2.3、配置类
三、redis缓存短信验证码
3.1、实现思路
四、redis缓存菜品数据
五、Spring Cache技术
5.1、spring cache介绍
5.2、spring cache 常用注解
1、spring cache以CacheManager对象中的Map集合作为缓存区时:
2、spring cache以redis作为缓存区时:
六、使用spring cache缓存套餐数据
首先我们第一步先把瑞吉项目推送到码云git远程仓库中去,推送到master主分支上,并且再创建一个v1.0分支然后把瑞吉项目再推送一次到v1.0分支上,等会我们做优化就用v1.0分支上的瑞吉项目代码进行优化开发。 (注意:新建远程仓库的时候不要初始化readme文件,容易报错)
补充能量:
如果我们项目中没有VCS只有git,但是我们又想手动创建本地仓库,那么就可以在下面的位置,把红色部分清空掉即可、
最终结果如下:
注:这里最终为什么要创建一个v1.0分支,目的就是我们做修改代码逻辑,优化代码的时候基本上都不在master主分支下修改,我们都是在分支下做一些逻辑,然后测试没毛病的话再合并到主分支master当中。
注:这三个操作我们在redis常用命令中在java中使用redis已经做好笔记了,忘记的话翻笔记
,这三个步骤环境搭建好之后,我们就可以在项目中使用redis了~
注:我们知道我们现在想用redis做缓存,那么我们就首先要先在项目中导入redis坐标(这里在讲redis常用命令在java中使用redis的笔记中也整理过,忘记的话可以看一下)
具体操作步骤演示如下所示:
注:这里我们不用设置密码,因为我们linux系统中的redis服务端没有设定密码。
这些环境搭建好之后,我们就可以把这次修改的代码再次推送到v1.0远程仓库分支当中去(再次推送的话,推送的是我们修改添加的那些代码):
(注:需要先提交到本地仓库,然后推送到v1.0远程仓库分支即可,【直接commit提交到本地仓库即可,不用再add添加到暂存区、工作区】)
首先我们知道,没有优化前我们手机短信验证码是存放在session域当中的,session域默认是三十分钟后数据消失,而实际中我们的验证码通常让他5分支或者1分支就消失,因此我们就可以用redis做缓存,代替session域存放手机短信验证码问题。
我们把验证码存放到redis当中后,那么我们用户在用手机号验证码登录的时候,我们也不能再在session域当中取出验证码进行判断了(此时session域中验证码已经被redis代替了)因此需要从redis当中取出验证码进行判断,然后决定让不让用户移动端登录。 并且如果移动端用户通过手机验证码的方式验证登录成功了,那么我们就没必要再留着这个验证码了,因此就可以在redis里面把该验证码删除掉即可。
细节:要注意我们用redis做缓存的时候,一定要保证部署在linux系统中的redis服务端是开启状态的,如果不是开启的状态,那么我们是没办法往redis中储存数据的,门都没开你还想进呢、如何在linux系统中开启redis 看redis(linux版)如何启动的笔记即可、(注意防火墙要把端口号打开)
代码演示如下所示:(绿色注解部分,就是redis代替session域的代码)
package com.Bivin.reggie.controller;
import com.Bivin.reggie.pojo.User;
import com.Bivin.reggie.service.impl.UserServiceImpl;
import com.Bivin.reggie.utils.ValidateCodeUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 手机验证码登录表现层
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
// 注入业务层对象
@Autowired
private UserServiceImpl userService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 接收客户端发送的获取验证码的请求功能
*
* 因此我们可以用User实体类接收客户端发送的请求携带的json格式的phone数据
*/
@PostMapping("/sendMsg")
public R sendMsg(HttpServletRequest request, @RequestBody User user) { // @RequestBody: json格式注解
/**
* // User实体类当中接收封装到客户端想要发送验证码的手机号后,我们就可以做为该手机号生成验证码了
*/
// 1、获取封装在User实体类当中的客户端手机号
String phone = user.getPhone();
if (StringUtils.isNotEmpty(phone)) { // 如果手机号不为null的话
// 2、为该客户端的手机号随机生成验证码 (调用随机生成验证码工具类即可生成,该工具类当中可以设定生成几位验证码)
String s = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("随机生成的验证码:" + s);
/**
* 注: 我们又不是真正开发项目的,因此我们这里的用阿里云短信服务给客户端手机号发送验证码功能这步就不演示了,
* 因为第二步生成的验证码,我们后台通过输出或者日志的形式就能知道随机生成的验证码是多少(只不过没有给这个手机发送短信告知而已,我们自己玩还告知个啥)
*/
// 3、调用阿里云的短信服务,并且让这个生成的验证码传递给短信服务类就可以让阿里云为该客户端手机发送第二步中生成的验证码了。
// SMSUtils.sendShortMessage(phone,s); // 把客户端的手机号和第二步中随机生成的验证码传递给阿里云的短信服务类,就可以为该手机号发送该生成的验证码了
// 4、将生成的验证码储存到Session域当中
// HttpSession session = request.getSession();
// session.setAttribute(phone, s); // key为客户端手机号 value为该客户端手机号生成的验证码
/**
* 代替第四步: 将生成的验证码储存到redis当中
*
* 注: value为String类型
*
*/
ValueOperations ops = redisTemplate.opsForValue();
ops.set(phone,s,5, TimeUnit.MINUTES); // 将手机号当作key,生成的验证码当作value, 5分钟后消失
return new R(1, "手机验证码短信发送成功");
}
// 这里的话就说明客户端传递的手机号是空的,因此直接返回信息即可
return new R(0, "手机验证码短信发送失败");
}
/**
* 手机验证码登录功能
* 客户端的POST请求: http://localhost:8080/user/login
*
* 携带的请求数据格式:
* code "7288"
* phone "13512451245"
*/
/**
* 我们后台这里可以做一些逻辑:就是判断一下该手机号用户是不是第一次登录该功能,如果是的话就把他的手机号保存在我们的数据库当中记录吧。
*
* 就保存到我们的user用户数据库当中进行保存记录即可。
*/
@PostMapping("/login")
public R login(@RequestBody Map map, HttpSession session) {
//log.info(map.toString());
//获取手机号
String phone = map.get("phone").toString();
//获取验证码
String code = map.get("code").toString();
//从Session中获取保存的验证码
// Object codeInSession = session.getAttribute(phone);
/**
* 用redis代替上面的从session域中获取保存的验证码
*/
ValueOperations ops = redisTemplate.opsForValue();
Object codeInSession = ops.get(phone); // 通过手机号key,取刚才储存在redis中的验证码数据
//进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
if (codeInSession != null && codeInSession.equals(code)) {
//如果能够比对成功,说明登录成功
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone, phone);
//根据用户的手机号去用户表获取用户
User user = userService.getOne(queryWrapper);
if (user == null) {
//判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册(通过客户端输入的手机号查不出来数据库中的数据,那么就说明为新用户)
user = new User();
user.setPhone(phone);
user.setStatus(1); //可设置也可不设置,因为数据库我们设置了默认值
//注册新用户
userService.save(user);
}
//这一行容易漏。。保存用户登录状态
session.setAttribute("user", user.getId()); //在session中保存用户的登录状态,这样才过滤器的时候就不会被拦截了
/**
* 到这里说明用户通过手机验证码的方式登录成功了,那么既然登录成功了就没必要还将那个用过的验证码
* 储存在redis中了,我们直接通过key手机号删除掉即可、
*/
redisTemplate.delete(phone);
return new R(1, user);
}
return new R(0, "ERROR");
}
}
重点看绿色注释部分代码:
@GetMapping(value = "/list")
public R list(DishDto dishDto){ // 客户端传递的川菜分类id可以封装在这个DTO实体类属性当中
List dtoList =null;
/**
* * 分析:首先我们要知道没有使用redis缓存的时候,客户端想要的数据就是我们封装到dtoList集合中响应给他的那些数据。
* 因此我们redis缓存中返回给客户端数据的时候,也用dtoList集合的形式返回响应给用户
*/
/**
* 首先第一步:可以先动态设定一个redis的key
* 我们就根据客户端发送的请求格式设定即可,如果不动态设定一个key的话,那么redis中的key那就都一样了,到底哪个是哪个啊
*
*/
String key = "dish_"+dishDto.getCategoryId() +"_"+ "status_" +dishDto.getStatus();
dtoList = (List) redisTemplate.opsForValue().get(key); // 通过key获取value值,也就是获取客户端想要的那些dtoList集合中的数据
// 通过key判断一下redis缓存中是否有客户端想要的数据库中的那些数据
if (dtoList !=null){
/**
* // 不为null,就说明redis缓存中有客户端想要的那些数据,因此我们直接返回响应给前端即可
*/
return new R(1,dtoList);
}
else {
/**
* // 到这里说明为null,那也就是说redis缓存中没有key所对应的value数据(也就是客户端想要的菜品数据),那么我们就让其调用数据库进行
* // 查询即可(就还是以前我们的那些代码),查询出来后响应给前端页面,并且把查询出来的数据通过上面的key的形式存入到redis缓存当中,
* 当下一次客户端再访问该数据的时候,redis缓存当中就肯定有该数据了。
*/
// 1、获取到该川菜分类id
Long id = dishDto.getCategoryId();
// 2、通过该川菜分类id,查询出来对应的菜品数据库中关联的所有菜品数据
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dish::getCategoryId,id); // 拼接sql: select * from dish = category_id = id;
// 调用查询功能即可把川菜分类id关联的所有菜品数据查询出来
List list = dishService.list(queryWrapper);
// 3、遍历list集合,把每一个关联的菜品数据都遍历出来。 (因为菜品数据中id和菜品口味的dish_id相关联,我们需要拿出来,然后调用口味数据库把口味拿出来)
dtoList =list.stream().map((item) -> { // 遍历list集合,遍历出来的菜品数据对象就一个一个赋予给了item
Long id1 = item.getId(); // 拿到菜品数据对象的id
// log.info("Dwaddwa:"+id1);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DishFlavor::getDishId,id1); // 拼接sql: select * from dish_flavor = dish_id = id1;
// 调用查询功能就能把每个菜品数据所对应的N种口味数据查询出来了
List list1 = dishFlavorService.list(wrapper);
DishDto dto = new DishDto();
// 4、把查询出来的这些口味数据封装到DTO实体类的属性当中去 (DTO实体类封装口味数据的属性也是集合的形式)
dto.setFlavors(list1);
// 5、并且把遍历出来的菜品数据对象也一个一个拷贝封装到DTO实体类属性当中
BeanUtils.copyProperties(item,dto);
return dto;
}).collect(Collectors.toList());
/**
* 把在数据库中查询出来的那些数据通过上面的动态key的形式存入到redis缓存当中去,并且设置存活时间60分钟。
* 等下一次再访问该功能的时候,就能直接在redis缓存当中获取数据了,不用再访问数据库了。
*/
redisTemplate.opsForValue().set(key,dtoList,60, TimeUnit.MINUTES);
return new R(1,dtoList); // 把该dtoList集合种的所有数据响应给前端即可
}
}
我们知道我们已经把上面菜品套餐所对应的所有菜品数据都加入到redis缓存当中去了,但是值得我们思考的是,如果我们此时在这些菜品对应的数据库中增加或者删除了一些菜品数据,那么此时
数据库中的数据肯定就和redis缓存当中储存的数据不一致了吧,毕竟redis缓存中的那些数据是我们没有修改数据之前的哪些数据,但是当客户端还访问这些菜品数据的时候,肯定还是把redis缓存中的数据响应给前端了,这不扯犊子了吗我们都已经修改菜品的这些数据了还响应给前端redis缓存的那些没修改之前的数据,那肯定不行啊。
因此我们需要解决一下问题,就是当修改或者增加这些菜品数据的时候,我们就把以前缓存在redis中的那些老数据清空掉,清空掉之后客户端再访问数据的时候就让他先调一次数据库进行查询,那么调用数据库肯定就拿到的是我们修改之后的数据了,毕竟调的是数据库。
因此我们需要在对这些商品数据库进行添加或者修改功能中设定一下清空上面移动端查询功能中redis缓存中的老数据(这里添加不再展示):
@PutMapping
public R update(@RequestBody DishDto dishDto){ // RequestBody注解: 接收前端传递的json数据注解
// 调用业务层,在业务层做一些逻辑处理。
dishService.UpdateDish(dishDto);
/**
* 也就是说在更新菜品数据库的时候,把移动端查询功能那里的redis缓存的没有更新之前的那些老数据清空掉,
* 因为我们都要更新菜品了,客户端查询的时候肯定不能还给人家展示没有修改之前的那么缓存的老数据啊,肯定要清空掉
* 清空之后用户要是再想访问那个功能要数据的时候,就让他调用数据库查询一下(因为redis缓存中的数据已经被我们清空了),
* 把修改后的数据查询出来响应展示给前端用户并且再把修改后的这些数据储存到redis缓存当中去。
*
* ** 这里有两种清空方式
*/
// 第一种: 我们知道查询时加入到redis缓存中的key都是 dish_ 开头的,因此我们可以直接把所有dish_开头的key全部删除掉,
// 那么也可以说以前储存在redis缓存中的老数据也全部删除掉了
// Set set = redisTemplate.keys("dish_*");
// redisTemplate.delete(set);
// 第二种: 通过动态获取key进行删除。我们知道存入到redis缓存中的时候就是通过动态设定key的,我们现在拿到动态设定的key,然后进行删除清空即可。
String key = "dish_"+dishDto.getCategoryId() +"_"+ "status_" +dishDto.getStatus();
redisTemplate.delete(key);
return new R(1);
}
注:我们现在还没有使用redis作为缓存,而是使用的cacheManager对象中的map集合作为缓存,
缺点就是当我们服务器关闭之后,那些缓存的数据就丢失了。
spring cache中常用的各注解使用的代码格式如下所示:
package com.itheima.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itheima.entity.User;
import com.itheima.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
/**
* 第一步:上面我们说过了通过针对不同的缓存技术,需要实现不同的CacheManager对象,因此第一步需要先自动装配该对象
*
* 注: 其实我们这里缓存的数据就放入到了该CacheManager对象底层的Map集合当中进行存放缓存数据的。
*
*/
@Autowired
private CacheManager cacheManager;
@Autowired
private UserService userService;
/**
* ***** @CachePut注解: 将方法的返回值放到缓存中注解
*
* 分析: value的作用: 缓存的名称,每个缓存名称下面可以有多个key
* 也就是说我们知道我们缓存的数据其实是放入到CacheManager对象的Map集合当中的,我们这里value的目的就是
* 知道把缓存的数据放入到map集合中的userCache位置,也就是说我们可以把map集合当成一个停车场,然后这个value
* 我们就可以理解为是停车场中的B区停车位置(这里也就是说是userCache区),然后后面的key的作用就是我们缓存的
* 返回值数据的key就是这个key的值,然后我们把缓存的key,value数据放入到userCache区进行保存。
*
* key的写法可以多变: 我们 "#result.id" 就表示以返回值(return user;)的结果id作为缓存数据的key,value就是返回值也就是想缓存的返回值的数据,
* 然后就可以把缓存的key,value存放到map集合中的userCache区进行保存了,以后我们就可以
* 通过key获取value也就是返回值的数据了。
*
* "#user.name" : 就是以参数User实体类(User user)传递过来的name数据作用缓存数据的key
*
* 还有多种key的写法,这里不再展示。
*
*
*/
@CachePut(value = "userCache",key = "#result.id")
@PostMapping
public User save(User user){
userService.save(user);
return user;
}
/**
* 我们通常要在删除和修改功能当中清除缓存的老数据,具体为什么就是上面移动端那样的原因。
*
* ****** @CacheEvict注解: 清理指定缓存
*
* 解释:(value = "userCache",key = "#id")
*
* 也就是说我们清理的是map缓存集合中的userCache区域的key对应的缓存数据,
* 该key为客户端传递的想要清理的那个id,然后通过这个key就可以把
* 对应的value缓存数据给清理掉了、
*/
@CacheEvict(value = "userCache",key = "#id")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
userService.removeById(id);
}
/**
* 同理: 也是说我们清理的是map缓存集合中的userCache区域的key,
* 该key为客户端传递的想要清理的那个user实体类中的id,然后通过这个key就可以把
* 对应的value缓存数据给清理掉了、
*/
@CacheEvict(value = "userCache",key = "#user.id")
@PutMapping
public User update(User user){
userService.updateById(user);
return user;
}
/**
* ***** @Cacheable注解: 在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存的数据;
* 若没有数据,调用查询方法并将方法返回值放到缓存中。假设我们这里还是以userCache区
* 作为存放缓存key,value的数据。如果通过客户端传递的id在缓存中找到了对应的数据,
* 那么就直接返回给用户缓存中的数据即可,如果没有找到key对应的缓存数据,那么就查询数据库
* 然后将查询的数据加入到缓存当中去(就以下面的key对应的值作为key,然后将返回值user结果作为value缓存到userCache区域中保存)。
*
* 注: 这里还可以加上一个条件:unless = "#result ==null" 也就是说当调用数据库查询的结果为null的时候
* 查询的数据就不添加到缓存当中去进行缓存保存了,如果不为null那么就让缓存(为null了你还缓存干啥啊,一个null值有啥缓存的)
*
*/
@Cacheable(value = "userCache",key = "#id",unless = "#result ==null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}
@Cacheable(value = "userCache",key = "#user.id + '_' + user.name ",condition = "#result !=null")
@GetMapping("/list")
public List list(User user){
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(user.getId() != null,User::getId,user.getId());
queryWrapper.eq(user.getName() != null,User::getName,user.getName());
List list = userService.list(queryWrapper);
return list;
}
}
注1:我们上面知道,以CacheManager对象中的Map集合作为缓存区时缓存的数据是很容易丢失的,当关闭服务器的时候缓存在map集合中的数据就会丢失了,因此我们真正项目开发中都用spring cache以redis作为缓存区的方法,因为可以保证缓存的数据不丢失。
注2: 因为我们现在已经以redis作为缓存了,也就是说把一些数据缓存到redis数据库当中去,那么我们一定要保证redis服务端是开启状态的,并且linux系统的防火墙要放开redis的端口号(毕竟redis部署在了linux系统上的),如果连redis服务端就没开启的话,还怎么把一些数据缓存保存到redis数据库中去?
具体步骤如下所示:
演示如下所示:
这些全部配置好之后,我们下面的和上面同样的代码再缓存的话,就不是缓存到CacheManager对象的Map集合当中了,而是直接缓存到redis数据库当中去了:
package com.itheima.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itheima.entity.User;
import com.itheima.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private CacheManager cacheManager;
@Autowired
private UserService userService;
@CachePut(value = "userCache",key = "#result.id")
@PostMapping
public User save(User user){
userService.save(user);
return user;
}
@CacheEvict(value = "userCache",key = "#id")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
userService.removeById(id);
}
@CacheEvict(value = "userCache",key = "#user.id")
@PutMapping
public User update(User user){
userService.updateById(user);
return user;
}
@Cacheable(value = "userCache",key = "#id",condition = "#result !=null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}
@Cacheable(value = "userCache",key = "#user.id + '_' + user.name ",condition = "#result !=null")
@GetMapping("/list")
public List list(User user){
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(user.getId() != null,User::getId,user.getId());
queryWrapper.eq(user.getName() != null,User::getName,user.getName());
List list = userService.list(queryWrapper);
return list;
}
}
注: 使用上面的spring cache方式进行redis缓存。
注: 同理和五中一样,要先导坐标,写配置,千万别忘记了(这里不再演示)。
同理:
我们知道我们已经把上面菜品套餐所对应的所有菜品数据都加入到redis缓存当中去了,但是值得我们思考的是,如果我们此时在这些菜品对应的数据库中增加或者删除了一些菜品数据,那么此时
数据库中的数据肯定就和redis缓存当中储存的数据不一致了吧,毕竟redis缓存中的那些数据是我们没有修改数据之前的哪些数据,但是当客户端还访问这些菜品数据的时候,肯定还是把redis缓存中的数据响应给前端了,这不扯犊子了吗我们都已经修改菜品的这些数据了还响应给前端redis缓存的那些没修改之前的数据,那肯定不行啊。
因此我们需要解决一下问题,就是当修改或者增加这些菜品数据的时候,我们就把以前缓存在redis中的那些老数据清空掉,清空掉之后客户端再访问数据的时候就让他先调一次数据库进行查询,那么调用数据库肯定就拿到的是我们修改之后的数据了,毕竟调的是数据库。