本文为瑞吉外卖项目的优化部分,在此特别感谢黑马程序员的课程
缓存优化
问题说明:
当用户数量足够多的时候,系统访问量大
频繁的访问数据库,系统性能下降,用户体验差
所以一些通用、常用的数据,我们可以使用Redis来缓存,避免用户频繁访问数据库
环境搭建
导入SpringDataRedis的maven坐标
这里我们就还是用SpringDataRedis来开发了
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
配置文件
配置连接redis的数据,我这里配置的是我的云服务器上装的Redis
1 2 3 4 5 redis: host: 101. XXX.XXX.160 password: root port: 6379 database: 0
配置类
配置一下序列化器,方便我们在图形化界面中查看我们存入的数据,在config包下新建RedisConfig类
但是也可以不配置RedisConfig,而是直接用SpringRedisConfig
,它的默认序列化器就是StringRedisSerializer
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate <>(); redisTemplate.setKeySerializer(new StringRedisSerializer ()); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } }
缓存短信验证码
实现思路
先来回顾一下我们之前的邮件验证码是储存在哪儿的
那现在我们学了Redis的基础应用,我们现在就可以把它缓存在Redis里
具体实现思路如下
在服务端UserController中注入RedisTemplate对象,用于操作Redis;
在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟;
在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码;
代码改造
在UserController中注入RedisTemplate或StringRedisTemplate对象,用于操作Redis
1 2 @Autowired private RedisTemplate redisTemplate;
修改UserController中的sendMsg方法,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @PostMapping("/sendMsg") public Result<String> sendMsg(@RequestBody User user, HttpSession session) throws MessagingException { String phone = user.getPhone(); if (!phone.isEmpty()) { //随机生成一个验证码 String code = MailUtils.achieveCode(); log.info(code); //这里的phone其实就是邮箱,code是我们生成的验证码 MailUtils.sendTestMail(phone, code); - //验证码存session,方便后面拿出来比对 - session.setAttribute(phone, code); + //验证码缓存到Redis,设置存活时间5分钟 + redisTemplate.opsForValue().set(phone, code,5, TimeUnit.MINUTES); return Result.success("验证码发送成功"); } return Result.error("验证码发送失败"); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @PostMapping("/sendMsg") public Result<String> sendMsg (@RequestBody User user, HttpSession session) throws MessagingException { String phone = user.getPhone(); if (!phone.isEmpty()) { String code = MailUtils.achieveCode(); log.info(code); MailUtils.sendTestMail(phone, code); redisTemplate.opsForValue().set("code" , code,5 , TimeUnit.MINUTES); return Result.success("验证码发送成功" ); } return Result.error("验证码发送失败" ); }
在服务端的UserController的login方法中,从Redis获取验证码,如果登录成功则删除Redis中的验证码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @PostMapping("/login") public Result<User> login(@RequestBody Map map, HttpSession session) { log.info(map.toString()); String phone = map.get("phone").toString(); String code = map.get("code").toString(); - //把刚刚存进去的验证码拿出来 - String codeInSession = session.getAttribute(phone).toString(); + //把Redis中缓存的code拿出来 + Object codeInRedis = redisTemplate.opsForValue().get(phone); - //看看接收到用户输入的验证码是否和session中的验证码相同 - log.info("你输入的code{},session中的code{},计算结果为{}", code, codeInSession, (code != null && code.equals(codeInSession))); + //看看接收到用户输入的验证码是否和redis中的验证码相同 + log.info("你输入的code{},session中的code{},计算结果为{}", code, codeInRedis, (code != null && code.equals(codeInRedis))); - if (code != null && code.equals(codeInSession)) { + if (code != null && code.equals(codeInRedis)) { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getPhone, phone); User user = userService.getOne(queryWrapper); if (user == null) { user = new User(); user.setPhone(phone); - user.setName("用户" + codeInSession); + user.setName("用户" + codeInRedis); userService.save(user); } session.setAttribute("user", user.getId()); + //如果登录成功,则删除Redis中的验证码 + redisTemplate.delete(phone); return Result.success(user); } return Result.error("登录失败"); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @PostMapping("/login") public Result<User> login (@RequestBody Map map, HttpSession session) { log.info(map.toString()); String phone = map.get("phone" ).toString(); String code = map.get("code" ).toString(); Object codeInRedis = redisTemplate.opsForValue().get(phone); log.info("你输入的code{},redis中的code{},计算结果为{}" , code, codeInRedis, (code != null && code.equals(codeInRedis))); if (code != null && code.equals(codeInRedis)) { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(User::getPhone, phone); User user = userService.getOne(queryWrapper); if (user == null ) { user = new User (); user.setPhone(phone); user.setName("用户" + codeInRedis); userService.save(user); } session.setAttribute("user" , user.getId()); redisTemplate.delete(phone); return Result.success(user); } return Result.error("登录失败" ); }
缓存菜品数据
菜品数据是我们登录移动端之后的展示页面
所以每当我们访问首页的时候,都会调用数据库查询一遍菜品数据
对于这种需要频繁访问的数据,我们可以将其缓存到Redis中以减轻服务器的压力
实现思路
移动端对应的菜品查看功能,是DishController中的list方法,此方法会根据前端提交的查询条件进行数据库查询操作(用户选择不同的菜品分类)。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。所以现在我们需要对此方法进行缓存优化,提高系统性能
但是还有存在一个问题,我们是将所有的菜品缓存一份,还是按照菜品/套餐分类,来进行缓存数据呢?
答案是后者,当我们点击某一个分类时,只需展示当前分类下的菜品,而其他分类的菜品数据并不需要展示,所以我们在缓存的时候,根据菜品的分类,缓存多分数据,页面在查询时,点击某个分类,则查询对应分类下的菜品的缓存数据
具体实现思路如下
修改DishController中的list方法,先从Redis中获取分类对应的菜品数据,如果有,则直接返回;如果无,则查询数据库,并将查询到的菜品数据存入Redis
修改DishController的save、update和delete方法,加入清理缓存的逻辑,避免产生脏数据(我们实际已经在后台修改/更新/删除了某些菜品,但由于缓存数据未被清理,未重新查询数据库,用户看到的还是我们修改之前的数据)
代码改造
先在DishController中注入RedisTemplate
1 2 @Autowired private RedisTemplate redisTemplate;
修改DishController的list方法,先从Redis中获取菜品数据
如果有,则直接返回
如果无,则查询数据库,并将查询到的菜品数据让Redis缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @GetMapping("/list") public Result<List<DishDto>> get(Dish dish) { + List<DishDto> dishDtoList; + String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus(); + dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key); + //如果有,则直接返回 + if (dishDtoList != null){ + return Result.success(dishDtoList); + } + //如果无,则查询 //条件查询器 LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>(); //根据传进来的categoryId查询 queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId()); //只查询状态为1的菜品(在售菜品) queryWrapper.eq(Dish::getStatus, 1); //简单排下序,其实也没啥太大作用 queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime); //获取查询到的结果作为返回值 List<Dish> list = dishService.list(queryWrapper); log.info("查询到的菜品信息list:{}",list); //item就是list中的每一条数据,相当于遍历了 - List<DishDto> dishDtoList = list.stream().map((item) -> { + dishDtoList = list.stream().map((item) -> { //创建一个dishDto对象 DishDto dishDto = new DishDto(); //将item的属性全都copy到dishDto里 BeanUtils.copyProperties(item, dishDto); //由于dish表中没有categoryName属性,只存了categoryId Long categoryId = item.getCategoryId(); //所以我们要根据categoryId查询对应的category Category category = categoryService.getById(categoryId); if (category != null) { //然后取出categoryName,赋值给dishDto dishDto.setCategoryName(category.getName()); } //然后获取一下菜品id,根据菜品id去dishFlavor表中查询对应的口味,并赋值给dishDto Long itemId = item.getId(); //条件构造器 LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>(); //条件就是菜品id lambdaQueryWrapper.eq(itemId != null, DishFlavor::getDishId, itemId); //根据菜品id,查询到菜品口味 List<DishFlavor> flavors = dishFlavorService.list(lambdaQueryWrapper); //赋给dishDto的对应属性 dishDto.setFlavors(flavors); //并将dishDto作为结果返回 return dishDto; //将所有返回结果收集起来,封装成List }).collect(Collectors.toList()); + //将查询的结果让Redis缓存,设置存活时间为60分钟 + redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES); return Result.success(dishDtoList); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @GetMapping("/list") public Result<List<DishDto>> get (Dish dish) { List<DishDto> dishDtoList; String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus(); dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key); if (dishDtoList != null ){ return Result.success(dishDtoList); } LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(dish.getCategoryId() != null , Dish::getCategoryId, dish.getCategoryId()); queryWrapper.eq(Dish::getStatus, 1 ); queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime); List<Dish> list = dishService.list(queryWrapper); log.info("查询到的菜品信息list:{}" , list); dishDtoList = list.stream().map((item) -> { DishDto dishDto = new DishDto (); BeanUtils.copyProperties(item, dishDto); Long categoryId = item.getCategoryId(); Category category = categoryService.getById(categoryId); if (category != null ) { dishDto.setCategoryName(category.getName()); } Long itemId = item.getId(); LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.eq(itemId != null , DishFlavor::getDishId, itemId); List<DishFlavor> flavors = dishFlavorService.list(lambdaQueryWrapper); dishDto.setFlavors(flavors); return dishDto; }).collect(Collectors.toList()); redisTemplate.opsForValue().set(key,dishDtoList,60 , TimeUnit.MINUTES); return Result.success(dishDtoList); }
修改DishController里的save、update和批量修改方法(status),加入清理缓存的逻辑
save DIFF 修改后的save update DIFF 修改后的update status DIFF 修改后的status 1 2 3 4 5 6 7 8 @PostMapping public Result<String> save(@RequestBody DishDto dishDto) { log.info("接收到的数据为:{}", dishDto); dishService.saveWithFlavor(dishDto); + String key = "dish_" + dishDto.getCategoryId() + "_1"; + redisTemplate.delete(key); return Result.success("添加菜品成功"); }
1 2 3 4 5 6 7 8 @PostMapping public Result<String> save (@RequestBody DishDto dishDto) { log.info("接收到的数据为:{}" , dishDto); dishService.saveWithFlavor(dishDto); String key = "dish_" + dishDto.getCategoryId() + "_1" ; redisTemplate.delete(key); return Result.success("添加菜品成功" ); }
1 2 3 4 5 6 7 8 @PutMapping public Result<String> update(@RequestBody DishDto dishDto) { log.info("接收到的数据为:{}", dishDto); dishService.updateWithFlavor(dishDto); + String key = "dish_" + dishDto.getCategoryId() + "_1"; + redisTemplate.delete(key); return Result.success("修改菜品成功"); }
1 2 3 4 5 6 7 8 @PutMapping public Result<String> update (@RequestBody DishDto dishDto) { log.info("接收到的数据为:{}" , dishDto); dishService.updateWithFlavor(dishDto); String key = "dish_" + dishDto.getCategoryId() + "_1" ; redisTemplate.delete(key); return Result.success("修改菜品成功" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @PostMapping("/status/{status}") public Result<String> status(@PathVariable Integer status, @RequestParam List<Long> ids) { log.info("status:{},ids:{}", status, ids); LambdaUpdateWrapper<Dish> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.in(ids != null, Dish::getId, ids); updateWrapper.set(Dish::getStatus, status); + LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.in(Dish::getId, ids); + List<Dish> dishes = dishService.list(lambdaQueryWrapper); + for (Dish dish : dishes) { + String key = "dish_" + dish.getCategoryId() + "_1"; + redisTemplate.delete(key); + } dishService.update(updateWrapper); return Result.success("批量操作成功"); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @PostMapping("/status/{status}") public Result<String> status (@PathVariable Integer status, @RequestParam List<Long> ids) { log.info("status:{},ids:{}" , status, ids); LambdaUpdateWrapper<Dish> updateWrapper = new LambdaUpdateWrapper <>(); updateWrapper.in(ids != null , Dish::getId, ids); updateWrapper.set(Dish::getStatus, status); LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.in(Dish::getId, ids); List<Dish> dishes = dishService.list(lambdaQueryWrapper); for (Dish dish : dishes) { String key = "dish_" + dish.getCategoryId() + "_1" ; redisTemplate.delete(key); } dishService.update(updateWrapper); return Result.success("批量操作成功" ); }
注意:这里并不需要我们对删除操作也进行缓存清理,因为删除操作执行之前,必须先将菜品状态修改为停售
,而停售状态也会帮我们清理缓存,同时也看不到菜品,随后将菜品删除,仍然看不到菜品,故删除操作不需要进行缓存清理
修改完了之后,我们来测试一下
由于我们现在还没有编写套餐数据的缓存,所以我们现在可以用菜品数据和套餐数据做对比
先手动点击一遍所有的分类,让Redis缓存(包括菜品分类和套餐分类)
之后去控制台清空输出,方便我们后续对比
随后再次点击菜品分类,控制台日志不会输出SQL语句的日志
但是点击套餐分类时,控制台会输出SQL语句的日志
当我们对菜品数据进行任意形式的修改(修改/添加/删除/改状态)时,缓存数据将被清理,同时重新查询,避免出现脏数据
SpringCache
SpringCache介绍
SpringCache是一个框架,实现了基本注解的缓存功能,只需要简单的添加一个注解,就能实现缓存功能
SpringCache提供了一层抽象,底层可以切换不同的cache实现,具体就是通过CacheManager接口来统一不同的缓存技术
针对不同的缓存技术,需要实现不同的CacheManager
CacheManger
描述
EhCacheCacheManager
使用EhCache作为缓存技术
GuavaCacheManager
使用Googke的GuavaCache作为缓存技术
RedisCacheManager
使用Rdis作为缓存技术
SpringCache常用注解
注解
说明
@EnableCaching
开启缓存注解功能
@Cacheable
在方法执行前spring先查看缓存中是否有数据。如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
@CachePut
将方法的返回值放到缓存中
@CacheEvict
将一条或者多条数据从缓存中删除
######## @Cacheable
@Cacheable
的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,其主要参数说明如下
注解
说明
举例
value
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
例如:@Cacheable(value=“mycache”)或者@Cacheable(value=(“cache7”, “cache2”]
key
缓存的key,可以为空,如果指定要按照 SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
例如:@Cacheable(value=“testcache”,key=“#userName”)
condition
缓存的条件,可以为空,使用SpEL编写,返回true或者false,只有为true 才进行缓存
例如:@Cacheable(value=“testcache”,condition=“#userName.length()>2”)
######## @CachePut
@CachePut
的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和@Cacheable不同的是,它每次都会触发真实方法的调用,其主要参数说明如下(其实跟@Cacheable一样)
注解
说明
举例
value
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
例如:@Cacheable(value=“mycache”)或者@Cacheable(value=(“cache7”, “cache2”]
key
缓存的key,可以为空,如果指定要按照 SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
例如:@Cacheable(value=“testcache”,key=“#userName”)
condition
缓存的条件,可以为空,使用SpEL编写,返回true或者false,只有为true 才进行缓存
例如:@Cacheable(value=“testcache”,condition=“#userName.length()>2”)
######## @CachEvict
@CachEvict
的作用主要针对方法配置,能够根据一定的条件对缓存进行清空
注解
说明
举例
value
缓存的名称,在 spring配置文件中定义,必须指定至少一个
例如:@Cacheable(value=“mycache”)或者@Cacheable(value={“cache1”, “cache2”]
key
缓存的key,可以为空,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
例如:@Cacheable(value=“testcache”,key=“#userName”)
condition
缓存的条件,可以为空,使用SpEL编写,返回true或者false,只有为true 才进行缓存
例如:@Cacheable(value=“testcache”,condition=“#userName.length()>2”)
allEntries
是否清空所有缓存内容,缺省为false,如果指定为true,则方法调用后将立即清空所有缓存
例如:@CachEvict(value=“testcache”,allEntries=true)
beforelnvocation
是否在方法执行前就清空,缺省为false,如果指定为true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存
例如:@CachEvict(value=“testcache”, beforelnvocation=true)
SpringCache使用方式
在SpringBoot项目中,使用缓存技术只需要在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存技术支持即可。
这里我们使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 spring: redis: host: 101. XXX.XXX.160 password: root port: 6379 database: 0 cache: redis: time-to-live: 3600000
缓存套餐数据
实现思路
前面我们已经实现了移动端查看套餐的功能,对应SetmealController中的list方法
此方法会根据前端提交的查询条件进行数据库查询操作
在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增强
现在需要对此方法进行缓存优化,提高系统性能
具体实现思路如下
修改SetmealController中的list方法,先从Redis缓存中获取套餐数据
如果有,则直接返回
如果无,则查询数据库,并将查询到的套餐数据存入Redis
修改SetmealController的save、update方法,加入清理缓存的逻辑,避免产生脏数据(我们实际已经在后台修改/更新/删除了某些套餐,但由于缓存数据未被清理,未重新查询数据库,用户看到的还是我们修改之前的数据)
代码修改
导入SpringCache和Redis相关的maven坐标
1 2 3 4 5 6 7 8 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency >
在appilcation.yml中配置缓存数据的过期时间
1 2 3 4 5 6 7 8 9 spring: redis: host: 101. XXX.XXX.160 password: root port: 6379 database: 0 cache: redis: time-to-live: 3600000
在启动类上加上@EnableCaching
注解,开启缓存注解功能
1 2 3 4 5 6 7 8 9 10 11 @Slf4j @SpringBootApplication @ServletComponentScan @EnableTransactionManagement @EnableCaching public class ReggieApplication { public static void main (String[] args) throws Exception { SpringApplication.run(ReggieApplication.class,args); log.info("项目启动成功..." ); } }
再SetmealController的list方法上加上@Cacheale
注解
该注解的功能是:在方法执行前,Spring先查看缓存中是否有数据;如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("/list") @Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status") public Result<List<Setmeal>> list (Setmeal setmeal) { LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(setmeal.getCategoryId() != null , Setmeal::getCategoryId, setmeal.getCategoryId()); queryWrapper.eq(setmeal.getStatus() != null , Setmeal::getStatus, 1 ); queryWrapper.orderByDesc(Setmeal::getUpdateTime); List<Setmeal> setmealList = setmealService.list(queryWrapper); return Result.success(setmealList); }
修改SetmealController的save、update和status方法,加入清理缓存的逻辑
至于为什么不用修改delete方法,在前面我们已经说明过了
实现手段也只需要加上@CacheEvict
注解,该注解的功能是:将一条或者多条数据从缓存中删除
当我们对套餐进行修改操作时,清空名为setmealCache的所有缓存
1 2 3 4 5 6 7 8 @PostMapping @CacheEvict(value = "setmealCache", allEntries = true) public Result<String> save (@RequestBody SetmealDto setmealDto) { log.info("套餐信息:{}" , setmealDto); setmealService.saveWithDish(setmealDto); return Result.success("套餐添加成功" ); }
当我们对套餐进行更新操作时,清空名为setmealCache的所有缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @PutMapping @CacheEvict(value = "setmealCache", allEntries = true) public Result<Setmeal> updateWithDish (@RequestBody SetmealDto setmealDto) { List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes(); Long setmealId = setmealDto.getId(); LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(SetmealDish::getSetmealId, setmealId); setmealDishService.remove(queryWrapper); setmealDishes = setmealDishes.stream().map((item) -> { item.setSetmealId(setmealId); return item; }).collect(Collectors.toList()); setmealService.updateById(setmealDto); setmealDishService.saveBatch(setmealDishes); return Result.success(setmealDto); }
当我们对套餐的状态修改时,清空名为setmealCache的所有缓存
1 2 3 4 5 6 7 8 9 10 @PostMapping("/status/{status}") @CacheEvict(value = "setmealCache", allEntries = true) public Result<String> status (@PathVariable String status, @RequestParam List<Long> ids) { LambdaUpdateWrapper<Setmeal> updateWrapper = new LambdaUpdateWrapper <>(); updateWrapper.in(Setmeal::getId, ids); updateWrapper.set(Setmeal::getStatus, status); setmealService.update(updateWrapper); return Result.success("批量操作成功" ); }
在做完这一步之后,会发现报错:DefaultSerializer requires a Serializable payload but received an object of type
这是因为要缓存的JAVA对象必须实现Serializable
接口,因为Spring会先将对象序列化再存入Redis,将缓存实体类继承Serializable
1 public class Result <T> implements Serializable
修改完毕之后,我们重启服务器测试看看有没有效果,如果有效果的话,我们push一下代码,继续做别的优化
读写分离
问题分析
目前我们所有的读和写的压力都是由一台数据库来承担,
如果数据库服务器磁盘损坏,则数据会丢失(没有备份)
解决这个问题,就可以用MySQL的主从复制,写操作交给主库,读操作交给从库
同时将主库写入的内容,同步到从库中
MySQL主从复制
介绍
配置
前置条件
准备好两台服务器,分别安装MySQL并启动服务成功,我这里用的两台虚拟机(另一台是克隆的,记得修改克隆虚拟机的MySQL的UUID)
修改克隆机的MySQL的uuid
登录克隆机的MySQL
执行SQL语句,记住生成的uuid,待会需要用
1 2 3 4 5 6 mysql> select uuid(); + | uuid() | + | 26532364 -4 f8d-11 ed- a300-005056307198 | +
查看配置文件目录
1 2 3 4 5 6 mysql> show variables like 'datadir' ; + | Variable_name | Value | + | datadir | / var/ lib/ mysql/ | +
编辑配置文件目录,修改uuid为刚刚我们生成的uuid
1 vi /var/lib/mysql/auto.cnf
重启服务
配置主库,我这里就用虚拟机上的mysql当主库了
修改MySQL数据库的配置文件,虚拟机是/etc/my.cnf
log_bin=mysql-bin #[必须]启用二进制日志
server-id=128 #[必须]服务器唯一ID,只需要确保其id是唯一的就好
重启mysql服务
1 systemctl restart mysqld
登录
Mysql数据库,执行下面的SQL
1 grant replication slave on * .* to 'Kyle' @'%' identified by 'root' ;
上面的SQL的作用是创建一个用户Kyle
,密码为root
,并且给Kyle
用户授予replication slave
权限,常用语建立复制时所需要用到的用户权限,也就是slave
必须被master
授权具有该权限的用户,才能通过该用户复制,这是因为主库和从库之间需要互相通信,处于安全考虑,只有通过验证的从库才能从主库中读取二进制数据
4. 登录Mysql数据库,执行下面的SQL
记录下结果中File和Position的值
1 2 3 4 5 +------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------+ | mysql-bin.000005 | 154 | | | | +------------------+----------+--------------+------------------+-------------------+
配置从库,我这里就用我的另一台克隆的虚拟机了
修改MySQL数据库的配置文件/etc/my.cnf
server-id=127 #[必须]服务器唯一ID,只需要确保其id是唯一的就好
重启mysql服务
1 systemctl restart mysqld
登录Mysql数据库,执行下面的SQL,将参数修改为你自己的
1 2 change master to master_host= '192.168.238.131' ,master_user= 'Kyle' ,master_password= 'root' ,master_log_file= 'mysql-bin.000005' ,master_log_pos= 154 ; start slave;
上面的SQL的作用是创建一个用户Kyle
,密码为root
,并且给Kyle
用户授予replication slave
权限,常用语建立复制时所需要用到的用户权限,也就是slave
必须被master
授权具有该权限的用户,才能通过该用户复制,这是因为主库和从库之间需要互相通信,处于安全考虑,只有通过验证的从库才能从主库中读取二进制数据
4. 登录Mysql数据库,执行SQL,查看从库的状态
看到如下如下三行配置相同,则主从连接成功
Slave_IO_State: Waiting for master to send event
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
读写分离案例
背景
面对日益增加的系统访问量,数据库的吞吐量面临着巨大的瓶颈。
对于同一时刻有大量并发读操作
和较少的写操作
类型的应用系统来说,将数据库拆分为主库
和从库
主库
主要负责处理事务性的增删改操作
从库
主要负责查询操作
这样就能有效避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善
Sharding-JDBC介绍
Sharding-JDBC定位为轻量级的JAVA框架,在JAVA的JDBC层提供额外的服务,它使得客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架
使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离
适用于任何基于JDBC的ORM框架
支持任何第三方的数据库连接池
支持任意实现JDBC规范的数据库
使用Sharding-JDBC框架的步骤
导入对应的maven坐标
在配置文件中配置读写分离规则
在配置文件中配置允许bean定义覆盖配置项
项目实现读写分离
前面我们已经配置好了主从数据库,那么我们现在就用瑞吉外卖试试读写分离
导入瑞吉外卖的SQL数据
Git创建一个新分支v1.1
,便于我们提交维护
导入Sharding-JDBC
的maven坐标
1 2 3 4 5 <dependency > <groupId > org.apache.shardingsphere</groupId > <artifactId > sharding-jdbc-spring-boot-starter</artifactId > <version > 4.0.0-RC1</version > </dependency >
在配置文件中配置读写分离规则,配置允许bean定义覆盖配置项
配置项可能会爆红,但是不影响影响项目启动,是IDEA的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 spring: shardingsphere: datasource: names: master,slave master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.238.131:3306/reggie?serverTimezone=UTC&useSSL=false username: root password: root slave: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.238.132:3306/reggie?serverTimezone=UTC&useSSL=false username: root password: root masterslave: load-balance-algorithm-type: round_robin name: dataSource master-data-source-name: master slave-data-source-names: slave props: sql: show: true main: allow-bean-definition-overriding: true
可能遇到的问题
启动时不报错,但是登陆功能报500
异常
查看控制台出现SQLFeatureNotSupportedException
异常
解决方案
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.20</version > </dependency >
Nginx
简介
Nginx是一款轻量级的Web
/反向代理
服务器以及电子邮件(IMAP/POP3)代理服务器,其特点是占有内存少,并发能力强。
事实上Nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用Nginx的网站有:百度、京东、新浪、网易、腾讯、淘宝等。
Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Pam6nep)开发的,第一个公开版本0.1.0发布于2004年10月4日。
官网:https://nginx.org/
Nginx的下载和安装
官网下载链接:https://nginx.org/en/download.html
安装过程:
Nginx是C语言开发的,所以需要先安装依赖
1 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
下载Nginx安装包
1 wget https://nginx.org/download/nginx-1.22.1.tar.gz
解压,我习惯放在/usr/local
目录下
1 tar -zxvf nginx-1.22.1.tar.gz -C /usr/local/
进入到我们解压完毕后的文件夹内
1 cd /usr/local/nginx-1.22.1/
建安装路径文件夹
安装前检查工作
1 ./configure --prefix=/usr/local/nginx
编译并安装
Nginx目录结构
安装完Nginx后,我们先来熟悉一下Nginx的目录结构
重点目录/文件:
conf/nginx.conf
html
logs
sbin/nginx
文件目录树状图如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 . ├── conf <-- Nginx配置文件 │ ├── fastcgi.conf │ ├── fastcgi.conf.default │ ├── fastcgi_params │ ├── fastcgi_params.default │ ├── koi-utf │ ├── koi-win │ ├── mime.types │ ├── mime.types.default │ ├── nginx.conf <-- 这个文件我们经常操作 │ ├── nginx.conf.default │ ├── scgi_params │ ├── scgi_params.default │ ├── uwsgi_params │ ├── uwsgi_params.default │ └── win-utf ├── html <-- 存放静态文件,我们后期部署项目,就要将静态文件放在这 │ ├── 50x.html │ └── index.html <-- 提供的默认的页面 ├── logs <-- 日志目录,由于我们新装的Nginx,所以现在还没有日志文件 └── sbin └── nginx <-- 这个文件我们也经常操作
Nginx配置文件结构
Nginx配置文件(conf/nginx.conf)整体分为三部分
全局块 和Nginx运行相关的全局配置
events块 和网络连接相关的配置
http块 代理、缓存、日志记录、虚拟主机配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 worker_processes 1; <-- 全局块 events { <-- events块 worker_connections 1024; } http { <-- http块 include mime.types; <-- http全局块 default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { <-- Server块 listen 80; <-- Server全局块 server_name localhost; location / { <-- location块 root html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
注意:http块中可以配置多个Server块,每个Server块中可以配置多个location块
Nginx命令
查看版本
1 2 [root@localhost sbin] nginx version: nginx/1.22.1
检查配置文件正确性
进入sbin目录,输入./nginx -t
,如果有错误会报错,而且也会记日志
1 2 3 [root@localhost sbin] nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
启动与停止
进入sbin目录,输入./nginx
,启动完成后查看进程
1 2 3 4 5 [root@localhost sbin] [root@localhost sbin] root 89623 1 0 22:08 ? 00:00:00 nginx: master process ./nginx nobody 89624 89623 0 22:08 ? 00:00:00 nginx: worker process root 89921 1696 0 22:08 pts/0 00:00:00 grep --color=auto nginx
如果想停止Nginx服务,输入./nginx -s stop
,停止服务后再次查看进程
1 2 3 [root@localhost sbin] [root@localhost sbin] root 93772 1696 0 22:11 pts/0 00:00:00 grep --color=auto nginx
重新加载配置文件
当修改Nginx配置文件后,需要重新加载才能生效,可以使用下面命令重新加载配置文件:./nginx -s reload
。
上面的所有命令,都需要我们在sbin目录下才能运行,比较麻烦,所以我们可以将Nginx的二进制文件配置到环境变量中,这样无论我们在哪个目录下,都能使用上面的命令
使用vim /etc/profile
命令打开配置文件,并配置环境变量,保存并退出
1 2 - PATH=$JAVA_HOME/bin:$PATH + PATH=/usr/local/nginx/sbin:$JAVA_HOME/bin:$PATH
之后重新加载配置文件,使用source /etc/profile
命令,然后我们在任意位置输入nginx
即可启动服务,nginx -s stop
即可停止服务
查看自己IP,启动服务后,浏览器输入ip地址就可以访问Nginx的默认页面
Nginx具体应用
部署静态资源
Nginx可以作为静态web服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。
相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到Nginx中。
将静态资源部署到Nginx非常简单,只需要将文件复制到Nginx安装目录下的html目录中即可。
反向代理
1 2 3 4 5 6 7 8 server { listen 82; server_name localhost; location / { proxy_pass http://http://192.168.238.132/50x.html; } }
负载均衡
早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。
应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据。
负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理。
配置负载均衡
默认是轮询算法,第一次访问是192.168.238.132
,第二次访问是101.XXX.XXX.160
也可以改用权重方式,权重越大,几率越大,现在的访问三分之二是第一台服务器接收,三分之一是第二台服务器接收
server 192.168.238.132 weight=10
server 101.XXX.XXX.160 weight=5
1 2 3 4 5 6 7 8 9 10 11 12 upstream targetServer{ server 192.168.238.132; server 101.XXX.XXX.160; } server { listen 82; server_name localhost; location / { proxy_pass http://targetServer; } }
名称
说明
轮询
默认方式
weight
权重方式
ip_hash
依据ip分配方式
least_conn
依据最少连接方式
url_hash
依据url分配方式
fair
依据响应时间方式
Nginx的特点
跨平台:Nginx可以在大多数操作系统中运行,而且也有Windows的移植版本
配置异常简单:非常容易上手。配置风格跟程序开发一样,神一般的配置
非阻塞、高并发:数据复制时,磁盘I/O的第一阶段是非阻塞的。官方测试能够支撑5万并发连接,在实际生产环境中跑到2-3万并发连接数(这得益于Nginx使用了最新的epoll模型)
事件驱动:通信机制采用epoll模式,支持更大的并发连接数
内存消耗小:处理大并发的请求内存消耗非常小。在3万并发连接下,开启的10个Nginx进程才消耗150M内存(15M*10=150M)
成本低廉:Nginx作为开源软件,可以免费试用。而购买F5 BIG-IP、NetScaler等硬件负载均衡交换机则需要十多万至几十万人民币
内置健康检查功能:如果Nginx Proxy后端的某台Web服务器宕机了,不会影响前端访问。
节省带宽:支持GZIP压缩,可以添加浏览器本地缓存的Header头。
稳定性高:用于反向代理,宕机的概率微乎其微。
前后端分离开发
开发人员同时负责前端和后端代码开发,分工不明确,开发效率低
前后端代码混合在一个工程中,不便于管理
对开发人员要求高,人员招聘困难
所以衍生出了一种前后端分离开发
前后端分离开发
介绍
前后端分离开发
,就是在项目开发过程中,对前端代码的开发,专门由前端开发人员
负责,后端代码由后端开发人员
负责,这样可以做到分工明确,各司其职,提高开发效率,前后端代码并行开发,可以加快项目的开发速度。目前,前后端分离开发方式已经被越来越多的公司采用了,成为现在项目开发的主流开发方式。
前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为前端工程和后端工程
开发流程
前后端开发人员都参照接口API文档进行开发
接口(API接口) 就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应参数等内容。
YApi
介绍
YApi是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护API,YApi还为用户提供了优秀的交互体验,开发人员只需要利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
YApi让接口开发更简单高效,让接口的管理更具有可读性、可维护性,让团队协作更合理。
Git仓库:https://github.com/YMFE/yapi
使用
Swagger
介绍
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做成各种格式的接口文档,以及在线接口调试页面等。
官网:https://swagger.io/
使用方式
导入对应的maven坐标
1 2 3 4 5 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-spring-boot-starter</artifactId > <version > 3.0.3</version > </dependency >
导入knife4j相关配置,并配置静态资源映射,否则接口文档页面无法访问,注意将controller的包路径修改为你自己的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Configuration @Slf4j + @EnableSwagger2 + @EnableKnife4j public class WebMvcConfig extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始进行静态资源映射..."); registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/"); registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/"); + registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/"); + registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); //设置对象转化器,底层使用jackson将java对象转为json messageConverter.setObjectMapper(new JacksonObjectMapper()); //将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0,表示设置在第一个位置,避免被其它转换器接收,从而达不到想要的功能) converters.add(0, messageConverter); } + @Bean + public Docket createRestApi() { + //文档类型 + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.blog.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("瑞吉外卖") + .version("1.0") + .description("瑞吉外卖接口文档") + .build(); + } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Configuration @Slf4j @EnableSwagger2 @EnableKnife4j public class WebMvcConfig extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { log.info("开始进行静态资源映射..." ); registry.addResourceHandler("/backend/**" ).addResourceLocations("classpath:/backend/" ); registry.addResourceHandler("/front/**" ).addResourceLocations("classpath:/front/" ); registry.addResourceHandler("doc.html" ).addResourceLocations("classpath:/META-INF/resources/" ); registry.addResourceHandler("/webjars/**" ).addResourceLocations("classpath:/META-INF/resources/webjars/" ); } @Override protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter (); messageConverter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 , messageConverter); } @Bean public Docket createRestApi () { return new Docket (DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.blog.controller" )) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo () { return new ApiInfoBuilder () .title("瑞吉外卖" ) .version("1.0" ) .description("瑞吉外卖接口文档" ) .build(); } }
在拦截器在中设置不需要处理的请求路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //定义不需要处理的请求 String[] urls = new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**", "/common/**", //对用户登陆操作放行 "/user/login", "/user/sendMsg", + + "/doc.html", + "/webjars/**", + "/swagger-resources", + "/v2/api-docs" };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String[] urls = new String []{ "/employee/login" , "/employee/logout" , "/backend/**" , "/front/**" , "/common/**" , "/user/login" , "/user/sendMsg" , "/doc.html" , "/webjars/**" , "/swagger-resources" , "/v2/api-docs" };
启动服务,访问 http://localhost/doc.html 即可看到生成的接口文档,我这里的端口号用的80,根据自己的需求改
常用注解
注解
说明
@Api
用在请求的类上,例如Controller,表示对类的说明
@ApiModel
用在类上,通常是个实体类,表示一个返回响应数据的信息
@ApiModelProperty
用在属性上,描述响应类的属性
@ApiOperation
用在请求的方法上,说明方法的用途、作用
@ApilmplicitParams
用在请求的方法上,表示一组参数说明
@ApilmplicitParam
用在@ApilmplicitParams注解中,指定一个请求参数的各个方面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Data @ApiModel("用户") public class User implements Serializable { private static final long serialVersionUID = 1L ; @ApiModelProperty("主键") private Long id; @ApiModelProperty("姓名") private String name; @ApiModelProperty("手机号") private String phone; @ApiModelProperty("性别 0 女 1 男") private String sex; @ApiModelProperty("身份证号") private String idNumber; @ApiModelProperty("头像") private String avatar; @ApiModelProperty("状态 0:禁用,1:正常") private Integer status; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 @RestController @Slf4j @RequestMapping("/user") @Api(tags = "用户相关接口") public class UserController { @Autowired private UserService userService; @Autowired private RedisTemplate redisTemplate; @PostMapping("/sendMsg") @ApiOperation("发送验证邮件接口") public Result<String> sendMsg (@RequestBody User user) throws MessagingException { String phone = user.getPhone(); if (!phone.isEmpty()) { String code = MailUtils.achieveCode(); log.info(code); MailUtils.sendTestMail(phone, code); redisTemplate.opsForValue().set(phone, code,5 , TimeUnit.MINUTES); return Result.success("验证码发送成功" ); } return Result.error("验证码发送失败" ); } @PostMapping("/login") @ApiOperation("用户登录接口") @ApiImplicitParam(name = "map",value = "map集合接收数据",required = true) public Result<User> login (@RequestBody Map map, HttpSession session) { log.info(map.toString()); String phone = map.get("phone" ).toString(); String code = map.get("code" ).toString(); Object codeInRedis = redisTemplate.opsForValue().get(phone); log.info("你输入的code{},redis中的code{},计算结果为{}" , code, codeInRedis, (code != null && code.equals(codeInRedis))); if (code != null && code.equals(codeInRedis)) { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(User::getPhone, phone); User user = userService.getOne(queryWrapper); if (user == null ) { user = new User (); user.setPhone(phone); user.setName("用户" + codeInRedis); userService.save(user); } session.setAttribute("user" , user.getId()); redisTemplate.delete(phone); return Result.success(user); } return Result.error("登录失败" ); } @PostMapping("/loginout") @ApiOperation("用户登出接口") public Result<String> logout (HttpServletRequest request) { request.getSession().removeAttribute("user" ); return Result.success("退出成功" ); } }
项目部署
配置环境说明
一共需要三台服务器
192.168.238.131(服务器A)
Nginx:部署前端项目、配置反向代理
MySql:主从复制结构中的主库
192.168.238.132(服务器B)
jdk:运行java项目
git:版本控制工具
maven:项目构建工具
jar:Spring Boot 项目打成jar包基于内置Tomcat运行
MySql:主从复制结构中的从库
101.xxx.xxx.160(服务器C,我用的我的云服务器)
部署前端项目
在服务器A中安装Nginx,将前端项目打包
目录上传到Nginx的html目录下
修改Nginx配置文件nginx.conf,新增如下配置
1 2 3 4 5 6 7 8 9 10 11 12 13 server { listen 80; server_name localhost; location / { root html/dist; index index.html; } location ^~ /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://192.168.238.132; } }
启动Nginx服务器测试,看到如下画面则说明没错
部署后端项目
在服务器B中安装JDK,Git,MySql
将项目打成jar包,手动上传并部署(当然你也可以选择git拉取代码,然后shell脚本自动部署)
部署完后端项目之后,我们就能完成正常的登录功能了,也能进入到后台系统进行增删改查操作
完结撒花
陆陆续续花了三个星期才把这个项目做完,果然在学校只会降低效率,接下来打算去系统性地学习一下Redis,然后再学SpringCloud微服务系统架构和常用中间件之类的