前言
在高并发的情况下,用户频繁查询数据库会导致系统性能严重下降,服务端响应时间增长。
我们可以使用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>
redis:
host: 192.168.56.18
port: 6379
database: 0
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; } }
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); } }
redisTemplate
如果Reids中没有,再去数据库查询,并将查询到的菜品数据存入Redis,然后再返回给前端;
redis缓存的value | |
---|---|
dish_分类Id , 比如: dish_1397844263642378242 | List< |
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); } }
127.0.0.1:6379> keys dish* 1) "dish_1531972995910795265" 2) "dish_1397844263642378242" 3) "dish_1397844391040167938" 4) "dish_1397844303408574465"
//删除+批量删除 @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中提供了很多缓存操作的注解,常见的是以下的几个:
名称 | 解释 |
---|---|
@EnableCaching | 开启基于注解的缓存 |
@Cacheable | 主要针对查询方法配置,在方法执行前先查看缓存中是否有数据,如果有则直接返回;若没有,调用方法并将方法返回值放到缓存中 |
@CacheEvict | 清空缓存 |
@CachePut |
1.划分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注解标注在方法上, 用于将当前方法的返回值对象放入缓存中;
该注解用于将方法返回值,放入缓存中,主要有下面两个属性:
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; }
/查询所有 //@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; }
@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个注解的使用方式如下:
- 在SpringBoot项目的启动类上加入@EnableCaching注解,开启缓存注解功能
- 在SetmealController的查询方法上加入@Cacheable注解
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("项目启动成功"); } }
//根据套餐分类查询当前分类下的套餐列表 @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); }
allEntries = true)
@CacheEvict(value = "setmeal",
//新增套餐 @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); }
参考