前言
在高并发的情况下,用户频繁查询数据库会导致系统性能严重下降,服务端响应时间增长。
我们可以使用Redis做Web项目的缓存,尽量使用户去缓存中获取数据;
这样做不仅提升了用户获取数据的速度 ,也缓解了MySQL数据库的读写压力;
那我们如何把MySQL数据库中数据放到Redis缓存服务器中呢?
我们可以通过SpringDataRedis提供的redisTemplate对象直接操作Redis数据库;
但是这种方式过于繁琐,我们可以通过SpringCache的注解配置,实现MySQL数据缓存到Redis;
一、redisTemplate缓存手机验证码功能
我们可以把每1个用户的验证码信息保存在Session中;
由于浏览器每次请求服务器都会在cookie中携带sessionID,我们根据sessionID这个key从服务器内存中获取到当前用户的验证码信息;
我们也可以直接把用户的验证码信息保存在Redis中,只要每1次客户端请求服务器时可以提供1个唯一的Key,我们照样也可以从Redis中把当前用户的验证码信息获取出来;
1.引入redis依赖
<!--引入redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
pom.xml
2.添加redis的配置
redis:
host: 192.168.56.18
port: 6379
database: 0
application.yaml
3.添加reids的序列化器
package com.itheima.reggie.config;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
//Redis配置类
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//设置Key的序列化器 默认是JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
RedisConfig.java
4.修改代码逻辑
package com.itheima.reggie.controller;
import cn.hutool.core.util.RandomUtil;
import com.itheima.reggie.common.ResultInfo;
import com.itheima.reggie.common.SmsTemplate;
import com.itheima.reggie.domain.User;
import com.itheima.reggie.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.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.*;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
//进点客户端
@RestController
@Slf4j
public class UserController {
@Autowired
private HttpSession session;
@Autowired
private SmsTemplate smsTemplate;
@Autowired
private UserService userService;
@Autowired
private RedisTemplate redisTemplate;
//进店用户输入自己的手机号码,点击获取验证码
@PostMapping("/user/sendMsg")
public ResultInfo sendMsg(@RequestBody Map<String, String> map) {
//1.接收参数
String phone = map.get("phone").trim();
//2.生成验证码
String code = RandomUtil.randomNumbers(6).trim();
log.info("生成的验证码为:{}", code);
//3.将当前用户的手机号码做key,验证码做value,保存到session中
//session.setAttribute("SMS_" + phone, code);
redisTemplate.opsForValue().set("SMS_" + phone, code,5, TimeUnit.MINUTES);
//4.发送验证码到进店用户的手机
//smsTemplate.sendSms(phone, code);
return ResultInfo.success(null);
}
/*
用户新用户注册+老用户登录:
* 这个接口兼具用户注册和登录的功能,实现思路是这样的:
* 接收用户输入的手机号和验证码,首先判断验证码是否正确,如果不正确直接给出错误提示
* 如果验证码没问题,接下来,验证用户的手机号在数据库是否存在
* 如果存在,表示老用户在进行登录操作,并且登录成功了
* 如果不存在,表示新用户在进行注册操作,需要将用户信息保存到user表中
* 将此人的信息保存到session
* */
@PostMapping("/user/login")
public ResultInfo login(@RequestBody Map<String, String> map) {
String phone = map.get("phone");
String code = map.get("code");
//通过用户发送的phone,获取到session中的code
// String codeFromSession = (String) session.getAttribute("SMS_" + phone);
String codeFromRedis = (String) redisTemplate.opsForValue().get("SMS_" + phone);
log.info("登录时用户输入的验证码为:{}---手机号{}", codeFromRedis, phone);
//校验code 前端传入code和session中保存code是否一致?
if (!StringUtils.equals(code, codeFromRedis) || StringUtils.isEmpty(code) || StringUtils.isEmpty(codeFromRedis)) {
return ResultInfo.error("验证码不对");
}
//
User currentUser = userService.findByPhone(phone);
if (currentUser == null) {
//3-2 没查到,代表新用户在注册
currentUser = new User();
currentUser.setPhone(phone);
currentUser.setStatus(1);//激活
userService.save(currentUser);
}
//4. 将user信息保存到session中(注意: 这个键必须跟拦截器中使用的键一致)
// session.setAttribute("SESSION_USER", currentUser);
redisTemplate.opsForValue().set("SMS_" + phone, code,5, TimeUnit.MINUTES);
return ResultInfo.success(currentUser);
}
//用户退出
@PostMapping("/user/logout")
public ResultInfo loggOut() {
session.setAttribute("SESSION_USER", null);
return ResultInfo.success(null);
}
}
UserController.java
二、redisTemplate缓存菜品功能
改造菜品信息的查询方法,先从Redis中获取分类对应的菜品数据,如果缓存中有则直接返回,不再查询数据库;
如果Reids中没有,再去数据库查询,并将查询到的菜品数据存入Redis,然后再返回给前端;
改造菜品信息增删改方法,当执方法完毕之后,要删除菜品对应的缓存数据;
1.Redis数据存储设计
如果我们要把数据库中数据缓存到Redis,应该采用Redis的哪1种数据类型,进行存储呢?
应该遵循以下原则:
- 如果我们缓存的数据是整存整取可以使用字符串;
- 如果要操作数据中的某1元素,可以考虑hash、set等其他数据结构;
redis缓存的key | redis缓存的value |
dish_分类Id , 比如: dish_1397844263642378242 | List<Dish> 二进制字符串 |
2.缓存菜品数据
修改在查询菜品逻辑如下:
- 先去Redis缓存中查询,Redis缓存中有该数据,直接将数据响应给客户端;
- 如果Redis缓存中没有该数据,就去MySQL数据库中查询;
- 从MySQL数据库中查询到数据之后,把数据缓存到Redis缓存之后,再将数据响应给客户端;
package com.itheima.reggie.controller;
import cn.hutool.core.collection.CollectionUtil;
import com.itheima.reggie.common.ResultInfo;
import com.itheima.reggie.domain.Dish;
import com.itheima.reggie.service.DishService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/dish/list")
public ResultInfo findDishListBycategoryId(Long categoryId, Integer status) {
//1.先从Redis中查询:设置1个key=dish_categoryId
String redisKey = "dish_" + categoryId;
List<Dish> dishList = null;
//2.如果从Redis缓存中查询到了
dishList = (List<Dish>) redisTemplate.opsForValue().get(redisKey);
if (CollectionUtil.isEmpty(dishList)) {
System.out.println("没有从Redis缓存中查询到数据!");
//2.没有从Redis缓存中查询到,从MySQL数据库查询,保存到Redis再返回数据
dishList = dishService.findBycategory(categoryId, "");
redisTemplate.opsForValue().set(redisKey, dishList);
}
return ResultInfo.success(dishList);
}
}
DishController.java
3.删除菜品缓存
当我们通过后台管理系统更新了MySQL数据库中的数据,如何保证Redis缓存也随之更新呢?
在对数据库数据进行增、删、改操作之后,直接清空该数据在Redis中的缓存;
127.0.0.1:6379> keys dish*
1) "dish_1531972995910795265"
2) "dish_1397844263642378242"
3) "dish_1397844391040167938"
4) "dish_1397844303408574465"
客户端请求Redis缓存时发现缓存中没有该数据,自然会去MySQL数据库中查询,然后再把更新之后的数据,缓存到Redis数据库;
//删除+批量删除
@DeleteMapping("/dish")
public ResultInfo bathDelete(Long[] ids) {
//菜品删除之后,需要删除Redis中所有菜品的缓存--
Set dishKeyList = redisTemplate.keys("dish_*");
redisTemplate.delete(dishKeyList);
if (ids != null && ids.length > 0) {
dishService.bathDelete(ids);
}
return ResultInfo.success(null);
}
三、SpringCache引入
Spring3.1版本之后引入了 通过注解配置实现缓存的技术;
通过在既有代码中加入少量它定义的各种注解,即能够达到缓存方法的返回对象的效果。
Spring的缓存技术还具备相当的灵活性,不仅能够使用SpEL来定义缓存的键和各种条件,也支持和主流的专业缓存比如Redis集成。
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
名称 | 解释 |
@EnableCaching | 开启基于注解的缓存 |
@Cacheable | 主要针对查询方法配置,在方法执行前先查看缓存中是否有数据,如果有则直接返回;若没有,调用方法并将方法返回值放到缓存中 |
@CacheEvict | 清空缓存 |
@CachePut | 将方法的返回值放到缓存中 |
1.划分Key
在学习SpringCache的注解之前,先学习1个给Redis中key划分模块的小技巧;
当Redis数据库中有很多key的时候,这些key密密麻麻地堆放在一起,没有任何层次结构,混沌一片,看起来特别混乱;
我们可以人为的使用冒号 “ :” 做key的层次划分;
把多个Key划分到1个逻辑单元中,方便我们对每1个key进行区分和管理;
下文我称为这个人为划分出来的逻辑单元为模块;
2.@EnableCaching
该注解标注在SpringBoot项目的启动类上,表示开启基于注解的缓存;
@EnableCaching //开启基于注解的缓存
@SpringBootApplication
@MapperScan("com.itheima.mapper")
public class SpringCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCacheApplication.class);
System.out.println("--------------启动成功--------------------");
}
}
3.@CachePut
@CachePut注解标注在方法上, 用于将当前方法的返回值对象放入缓存中;
该注解用于将方法返回值,放入缓存中,主要有下面两个属性:
3.1.value属性
该属性用于指定缓存key所在的模块(上文提到的模块);
3.2.key属性
以上value属性确定了缓存key的所在的模块;
key属性结合value属性可以用于指定缓存key的具体名称;
key属性支持Spring的表达式语言SPEL语法,key的写法如下:
- 处理器方法参数名:获取方法参数的变量 #user.id
- 处理器方法返回值:获取方法返回值中的变量 #result.id
3.3.代码示例
//新增
@CachePut(value = "user", key = "#user.id")
@RequestMapping("/user/save")
public User save(User user) {
userMapper.insert(user);
return user;
}
4.@Cacheable
@Cacheable标注在查询方法上,表示在方法执行前,先查看缓存中是否有数据,如果有直接返回,若没有,调用方法查询并将方法返回值放到缓存中;
/查询所有
//@Cacheable标注在查询方法上,表示在方法执行前,先查看缓存中是否有数据,
// 如果有直接返回,
// 若没有,调用方法查询并将方法返回值放到缓存中
@Cacheable(value = "user", key = "'all'") //user::all
@RequestMapping("/user/findAll")
public List<User> findAll() {
System.out.println("进入到了findAll方法");
List<User> userList = userMapper.selectList(new QueryWrapper<>());
return userList;
}
//根据name和age查询
@Cacheable(value = "user", key = "#name+'_'+#age") //user::name_age
@RequestMapping("/user/findByNameAndAge")
public List<User> findByNameAndAge(String name, Integer age) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", name);
wrapper.eq("age", age);
List<User> userList = userMapper.selectList(wrapper);
return userList;
}
5.@CacheEvict
@CacheEvict一般标注在增、删、修改方法上,用于删除指定的缓存数据;
@CacheEvict(value = "user", key = "'A'") : 删除user模块下名称为A的key也就是user::a
@CacheEvict(value = "user", allEntries = true) :删除user模块下所有key;
//修改
//@CacheEvict 可以根据value和key进行删除
@CacheEvict(value = "user", key = "'all'") //删除user模块下名称为all的key也就是user::all
@RequestMapping("/user/update")
public void update(User user) {
userMapper.updateById(user);
}
//删除
//@CacheEvict 可以根据value进行删除
@CacheEvict(value = "user", allEntries = true) //删除整个user模块下所有key
@RequestMapping("/user/delete")
public void delete(Long id) {
userMapper.deleteById(id);
}
6.@Cacheable和@CachePut的区别
@Cacheable和@CachePut都具有缓存数据的功能,不同点是:
@Cacheable:只会执行1次缓存任务,当标记在一个方法上时表示该方法是支持缓存的,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果。
@CachePut:该注解标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
四、SpringCache缓存套餐功能
在日常开发中SpringCache有3个常用的注解,分别是@EnableCaching注解、@Cacheable注解和@CacheEvict注解;
这3个注解的使用方式如下:
- 导入Spring Cache和Redis相关maven坐标(Spring核心包有)
- 在SpringBoot项目的启动类上加入@EnableCaching注解,开启缓存注解功能
- 在SetmealController的查询方法上加入@Cacheable注解
- 在SetmealController的新增、删除、修改方法上加入@CacheEvict注解
1.启动类开启缓存(@EnableCaching)
要想使用SpringCahche必须在SpringBoot的启动类增加@EnableCaching注解
package com.itheima.reggie;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication//主类
@MapperScan("com.itheima.reggie.mapper")//指定mybatis类所在的包
@EnableTransactionManagement //开启对事物管理的支持
@Slf4j
@EnableCaching
public class WebManageApplication {
public static void main(String[] args) {
SpringApplication.run(WebManageApplication.class, args);
log.info("项目启动成功");
}
}
WebManageApplication.java
2.设置套餐缓存(@Cacheable)
查询套餐时在Redis中设置套餐的缓存
//根据套餐分类查询当前分类下的套餐列表
@Cacheable(value = "setmea", key = "#{categoryId}")
@GetMapping("/setmeal/list")
public ResultInfo findByCategoryId(Long categoryId, Integer status) {
List<Setmeal> setmealList = setmealService.findByCategory(categoryId, status);
return ResultInfo.success(setmealList);
}
2.清理套餐缓存@CacheEvict(value = "setmeal", allEntries = true)
增、删、修改套餐数据时, 删除Redis中设置的所有套餐相关缓存;
//新增套餐
@CacheEvict(value = "setmeal", allEntries = true)
@PostMapping("setmeal")
//@RequestBody注解后面 不跟对象参数就跟 map参数
public ResultInfo save(@RequestBody() Setmeal setmeal) {
setmealService.save(setmeal);
return ResultInfo.success(null);
}
//删除套餐
@CacheEvict(value = "setmeal", allEntries = true)
@DeleteMapping("/setmeal")
public ResultInfo deleteByIds(Long[] ids) {
if (ids != null && ids.length > 0) {
setmealService.deleteByIds(ids);
}
return ResultInfo.success(null);
}
//更新当前套餐信息
@CacheEvict(value = "setmeal", allEntries = true)
@PutMapping("/setmeal")
public ResultInfo update(@RequestBody() Setmeal setmeal) {
//更新基本信息
setmealService.update(setmeal);
return ResultInfo.success(null);
}
//套餐批量启售、停售套餐
@CacheEvict(value = "setmeal", allEntries = true)
@PostMapping("/setmeal/status/{status}")
public ResultInfo bathSale(@PathVariable("status") Integer status,
@RequestParam("ids") Long[] idList) {
if (idList != null && idList.length > 0) {
setmealService.bathUpdate(status, idList);
}
return ResultInfo.success(null);
}
参考