视频学习:黑马程序员Java项目实战瑞吉外卖

部分内容参考:Kyle’s Blog

Git管理代码

这里将瑞吉外卖的不同版本纳入Git管理,方便观察优化历程

  1. 将原本的Reggie导入版本控制。VCS–>Great Git Repository–>选择要上传的项目

    image-20230708102116611

  2. 创建好后会出现.gitignore文件,这里从资料中拷贝相应内容覆盖

    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
    .git
    logs
    rebel.xml
    target/
    !.mvn/wrapper/maven-wrapper.jar
    log.path_IS_UNDEFINED
    .DS_Store
    offline_user.md

    ### STS ###
    .apt_generated
    .classpath
    .factorypath
    .project
    .settings
    .springBeans

    ### IntelliJ IDEA ###
    .idea
    *.iws
    *.iml
    *.ipr

    ### NetBeans ###
    nbproject/private/
    build/
    nbbuild/
    dist/
    nbdist/
    .nb-gradle/
    generatorConfig.xml

    ### nacos ###
    third-party/nacos/derby.log
    third-party/nacos/data/
    third-party/nacos/work/

    file/
  3. 然后右键项目-->Git-->Add,将文件添加到暂存区

  4. 右键项目-->Git-->Commit,将文件提交到版本库

    • 此时会指定远程仓库地址,将在github上创建的仓库地址拿来即可

      image-20230708103053400

    • 然后Push到远程仓库

    • 此时就在master分支上创建完成

  5. 因为我们想要保存不同的版本,所以这里在此基础上创建新的分支,进行后续的改动

    • IDEA右下角点击master,然后创建分支v1.0,此时右下角显示v1.0表明切换到了这个分支

    • 然后将这个分支也推送到远程仓库,此时右键项目-->Git-->Push即可,并且显示新的分支

      image-20230708103650752

    • 然后直接push即可

此时就会有两个分支,目前内容一致

  • master

    image-20230708103805596

  • v1.0

    image-20230708103825995


缓存优化

为什么需要缓存优化?

  • 当用户数量过多时,系统访问量大,频繁的访问数据库,系统性能下降,用户体验差。
  • 所以一些通用、常用的数据,可以使用Redis来缓存,避免用户频繁访问数据库

环境搭建

导入坐标

使用SpringDataRedis来开发

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置文件

配置redis的相关设置,在yml配置文件下添加即可。

这里因为我用的window端的,没有创建密码

1
2
3
4
redis:
host: 172.0.0.1
port: 6379
database: 0

配置类

config包下新建RedisConfig

  • 配置序列化器,方便在图形化界面中查看存入的数据
  • 但是也可以不配置RedisConfig,而是直接用框架自动创建的SpringRedisConfig,但它的默认序列化器是JdkSerializationRedisSerializer。这种不便于观察
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<>();
//默认key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

然后提交到远程仓库,首先进行Add,然后直接点击图标集成commit和push

缓存短信验证码

实现思路

前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在HttpSession中的,

现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:

  1. 在服务端UserController中注入RedisTemplate对象,用于操作Redis;
  2. 在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟;
  3. 在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码;

代码改造

  1. 在服务端UserController中注入RedisTemplate对象,用于操作Redis;

    1
    2
    @Autowired
    private RedisTemplate redisTemplate;
  2. 在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session) throws MessagingException {
    //获取手机号/邮箱
    String phone = user.getPhone();

    //调用工具类完成验证码发送
    if (!phone.isEmpty()) {
    //随机生成一个验证码
    String code = MailUtils.achieveCode();

    //这里的phone其实就是邮箱,code是我们生成的验证码
    MailUtils.sendTestMail(phone, code); //这部分抛出异常

    //[old]将要发送的验证码保存在session,然后与用户填入的验证码进行对比
    //session.setAttribute(phone, code);

    //[new]将生成的验证码缓存到Redis中,并设置有效期为5分钟
    redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);

    return R.success("验证码发送成功");
    }
    return R.error("验证码发送失败");
    }
  3. 在服务端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
    33
    34
    35
    36
    37
    38
    39
    40
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session) {
    log.info(map.toString());
    //获取邮箱
    String phone = map.get("phone").toString();
    //获取验证码
    String code = map.get("code").toString();

    //[old]从session中获取保存的验证码
    //Object codeInSession = session.getAttribute(phone);

    //[new]从Redis中获取缓存验证码
    Object codeInSession = redisTemplate.opsForValue().get(phone);

    //进行验证码的比对
    if (codeInSession != null && codeInSession.equals(code)) {
    //判断一下当前用户是否存在
    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.setStatus(1);
    userService.save(user);
    user.setName("游客" + codeInSession);
    }
    //登录成功后,需要保存session,表示登录状态,因为前面的过滤器进行了用户登录判断
    session.setAttribute("user", user.getId());

    //[new]如果用户登录成功,删除redis中缓存的验证码
    redisTemplate.delete(phone);

    //并将其作为结果返回
    return R.success(user);
    }
    return R.error("登录失败");
    }

然后此时再登录手机端,可以看到redis数据库中获取到验证码,登陆后数据库中该条数据就会被清除

image-20230708114815574

缓存菜品数据

菜品数据是登录移动端之后的展示页面,所以每当访问首页的时候,都会调用数据库查询一遍菜品数据。对于这种需要频繁访问的数据,可以将其缓存到Redis中以减轻服务器的压力

实现思路

移动端对应的菜品查看功能,是DishController中的list方法,此方法会根据前端提交的查询条件进行数据库查询操作(用户选择不同的菜品分类)。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。所以现在我们需要对此方法进行缓存优化,提高系统性能。

但是还有存在一个问题:是将所有的菜品缓存一份,还是按照菜品/套餐分类,来进行缓存数据呢?

  • 答案是后者,当我们点击某一个分类时,只需展示当前分类下的菜品,而其他分类的菜品数据并不需要展示,所以我们在缓存的时候,根据菜品的分类,缓存多分数据,页面在查询时,点击某个分类,则查询对应分类下的菜品的缓存数据

具体实现思路如下

  1. 修改DishController中的list方法,先从Redis中获取分类对应的菜品数据,如果有,则直接返回;如果无,则查询数据库,并将查询到的菜品数据存入Redis
  2. 修改DishController的save、update和delete方法,加入清理缓存的逻辑,避免产生脏数据(在后台修改/更新/删除了某些菜品,但由于缓存数据未被清理,未重新查询数据库,导致用户看到的还是修改之前的数据)

代码改造

  1. 注入RedisTemplate对象,用于操作Redis;

    1
    2
    @Autowired
    private RedisTemplate redisTemplate;
  2. 修改DishController的list方法,先从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
    @GetMapping("/list")
    public R<List<DishDto>> get(Dish dish) {
    String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();
    List<DishDto> dishDtoList;
    //先从redis中获取缓存数据
    dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
    //如果存在,则直接返回,无需查询数据库
    if (dishDtoList != null){
    return R.success(dishDtoList);
    }
    //如果不存在则执行下述数据库查询操作,查询后将数据缓存在redis
    LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
    lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
    lqw.eq(Dish::getStatus, 1);
    lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
    List<Dish> list = dishService.list(lqw);

    dishDtoList = list.stream().map((item) -> {
    DishDto dishDto = new DishDto();
    BeanUtils.copyProperties(item, dishDto);

    Long id = item.getId();
    LambdaQueryWrapper<DishFlavor> flavorlqw = new LambdaQueryWrapper<>();
    flavorlqw.eq(DishFlavor::getDishId, id);
    List<DishFlavor> flavors = dishFlavorService.list(flavorlqw);

    dishDto.setFlavors(flavors);
    //将dishDto作为结果返回
    return dishDto;
    //将所有返回结果收集起来,封装成List
    }).collect(Collectors.toList());

    //将数据缓存在redis中,设置持续时间60min
    redisTemplate.opsForValue().set(key, dishDtoList, 60, TimeUnit.MINUTES);

    return R.success(dishDtoList);
    }

    这里顺便完善了foreach,将其修改为stream流形式遍历,因为之前的方法需要将每个dishDto add到dishDtoList,但由于更改了List<DishDto> dishDtoList的初始位置,并且首次从redis中会进行查询赋值,可能导致dishDtoList为空,调用add方法导致空指针异常的出现。

  3. 此时redis数据库中获取到了数据

    image-20230708125325431

  4. 修改DishController里的save、update和批量修改方法(status),加入清理缓存的逻辑

    • save函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @PostMapping
      public R<String> save(@RequestBody DishDto dishDto) {
      log.info("菜品新增数据{}", dishDto.toString());
      dishService.saveWithFlavor(dishDto);
      //新增清除原本redis中缓存对应类别的数据
      String key = "dish_" + dishDto.getCategoryId() + "_1";
      redisTemplate.delete(key);
      return R.success("菜品新增成功");
      }
    • update函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @PutMapping
      public R<String> update(@RequestBody DishDto dishDto) {
      log.info("菜品修改数据{}", dishDto.toString());
      dishService.updateWithFlavor(dishDto);
      //修改商品后清除原本redis中保存的该类别的缓存数据
      String key = "dish_" + dishDto.getCategoryId() + "_1";
      redisTemplate.delete(key);
      return R.success("菜品修改成功");
      }
    • 修改菜品/套餐状态函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      @PostMapping("/status/{status}")
      public R<String> stop(@PathVariable int status, @RequestParam List<Long> ids) {
      LambdaUpdateWrapper<Dish> luw = new LambdaUpdateWrapper<>();
      luw.in(Dish::getId, ids);
      luw.set(Dish::getStatus, status);

      //新增redis清除缓存
      LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
      lqw.in(Dish::getId, ids);
      for (Dish dish : dishService.list(lqw)) {
      String key = "dish_" + dish.getCategoryId() + "_1";
      redisTemplate.delete(key);
      }

      dishService.update(luw);
      return R.success("状态更新成功");
      }

测试

由于没有编写套餐数据的缓存,所以可以用菜品数据和套餐数据做对比

  • 先手动点击一遍所有的分类,让Redis缓存(包括菜品分类和套餐分类)。清空控制台输出,方便后续对比
  • 再次点击菜品分类,控制台日志不会输出SQL语句的日志
  • 但是每次点击套餐分类时,控制台都会输出SQL语句的日志
  • 当对菜品数据进行任意形式的修改(修改/添加/删除/改状态)时,缓存数据将被清理,同时重新查询,避免出现脏数据

Spring Cache

介绍

  • SpringCache是一个框架,实现了基本注解的缓存功能,只需要添加一个注解,就能实现缓存功能
  • SpringCache提供了一层抽象,底层可以切换不同的cache实现,具体就是通过CacheManager接口来统一不同的缓存技术
  • 针对不同的缓存技术,需要实现不同的CacheManager
CacheManger 描述
EhCacheCacheManager 使用EhCache作为缓存技术
GuavaCacheManager 使用Google的GuavaCache作为缓存技术
RedisCacheManager 使用Redis作为缓存技术

常用注解

注解 说明
@EnableCaching 开启缓存注解功能
@Cacheable 在方法执行前spring先查看缓存中是否有数据。如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或者多条数据从缓存中删除

这里以@Cacheable为例,其余两个用法一致,更详细用法可以点击注解到源码中查看

@Cacheable根据方法的请求参数对其结果进行缓存。在方法执行前spring先查看缓存中是否有数据。如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中

  • 格式:@Cacheable(value= , key= , condition= )

    • value是缓存的名称,不能为空,每个缓存名称下面可以有多个key
    • key是缓存数据的key,可以为空,指定需要按照SpEL表达式编写;缺省按照方法的所有参数进行组合。
    • condition是缓存的条件,可以为空,使用SpEL编写,返回true或者false,只有为true 才进行缓存

    具体key和condition中SpEL中可以写什么,可以直接ctrl点击注解,在其上方注释中查看

  • 示例:

    1
    2
    3
    4
    5
    6
    //以传入的id为key,以返回值user为value缓存,缓存条件是返回值不为空
    @Cacheable(value="wzyCache" , key="#id", condition="#result != null")
    public User getById(@PathVariable Long id){
    User user = userService.getById(id);
    return user;
    }
    1
    2
    3
    4
    5
    6
    //以user的id为key,将返回值user作为value缓存
    @Cacheable(value="wzyCache" , key="#user.id")
    public User save(User user){
    userService.save(user);
    return user;
    }

    同样的方法key还可以写为:key=#p0.idkey=root.args[0].id 等作用都一致。

使用方式

  • 导入依赖坐标

    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>
  • 配置application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    redis:
    host: localhost
    port: 6379
    database: 0
    cache:
    redis:
    time-to-live: 3600000 #设置存活时间为一小时,如果不设置,则一直存活
  • 启动类上使用@EnableCaching开启缓存技术支持。

  • 然后在controller方法上标注@Cacheable@CacheEvict等注解。

缓存套餐数据

实现思路

前面我们已经实现了移动端查看套餐的功能,对应SetmealController中的list方法。此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增强。现在需要对此方法进行缓存优化,提高系统性能

具体实现思路如下:

  1. 导入Spring Cache和Redis相关的Maven坐标
  2. 在application.yml中配置缓存数据的过期时间
  3. 在启动类上加上@EnableCaching注解,开启缓存注解功能
  4. 在SetmealController的list方法上加上@Cacheable注解
  5. 在SetmealController的save、delete的方法上加上CacheEvict注解

代码修改

  1. 首先进行工作准备:maven依赖坐标导入、配置文件添加redis和缓存、启动类标注@EnableCaching

  2. SetmealControllerlist方法上加上@Cacheale注解

    在方法执行前,Spring先查看缓存中是否有数据;如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @GetMapping("/list")
    @Cacheable(value = "setmealCache",key = "#setmeal.categoryId+'_'+#setmeal.status")
    public R<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 R.success(setmealList);

    }

    此时因为方法返回的是R封装的数据,直接执行会报错:DefaultSerializer requires a Serializable payload but received an object of type

    这是因为要缓存的JAVA对象必须实现Serializable接口,因为Spring会先将对象序列化再存入Redis,将缓存实体类继承Serializable。所以这里将R类实现Serializable接口

  3. SetmealControllersave、update和status方法,加入清理缓存的逻辑,加上@CacheEvict注解

    • save方法

      1
      2
      3
      4
      5
      6
      7
      8
      @PostMapping
      //设置allEntries为true,清空缓存名称为setmealCache的所有缓存
      @CacheEvict(value = "setmealCache", allEntries = true)
      public R<String> save(@RequestBody SetmealDto setmealDto){
      log.info("新建套餐:{}",setmealDto.toString());
      setmealService.saveWithDish(setmealDto);
      return R.success("新建成功");
      }
    • delete方法

      1
      2
      3
      4
      5
      6
      7
      @DeleteMapping
      @CacheEvict(value = "setmealCache", allEntries = true)
      public R<String> delete(@RequestParam List<Long> ids){
      log.info("删除套餐的id:{}",ids);
      setmealService.removeWithDish(ids);
      return R.success("删除成功");
      }
    • status方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @PostMapping("/status/{status}")
      @CacheEvict(value = "setmealCache", allEntries = true)
      public R<String> stop(@PathVariable int status, @RequestParam List<Long> ids){
      log.info("套餐状态:{},套餐ids:{}",status,ids);

      LambdaUpdateWrapper<Setmeal> luw = new LambdaUpdateWrapper<>();
      luw.in(Setmeal::getId, ids);
      luw.set(Setmeal::getStatus, status);
      setmealService.update(luw);
      return R.success("修改销售状态成功");


读写分离

问题分析

  • 目前我们所有的读和写的压力都是由一台数据库来承担,如果数据库服务器磁盘损坏,则数据会丢失(没有备份)
  • 解决这个问题,就可以用MySQL的主从复制,写操作交给主库,读操作交给从库,同时将主库写入的内容,同步到从库中

image-20230709113111583

image-20230709113307624

MySQL主从复制

介绍

MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台NysQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具。

MySQL复制过程分成三步:

  1. master将改变记录到二进制日志(binary log)
  2. slavemasterbinary log拷贝到它的中继日志(relay log)
  3. slave重做中继日志中的事件,将改变应用到自己的数据库中

img

配置

前置条件:准备好两台服务器,分别安装MySQL并启动服务成功。

虚拟机克隆

这里我通过克隆虚拟机实现两台服务器,教程:VMware 虚拟机克隆详细教程

  1. 首先通过克隆复制原本的虚拟机环境。

  2. 然后使用ip addr命令获取两个虚拟的IP地址,便于远程连接

    • 192.168.186.128
    • 192.168.186.129
  3. 修改克隆机的MySQL的uuid

    • 执行SQL语句,记住生成的uuid

      1
      2
      3
      4
      5
      6
      7
      mysql> select uuid();
      +--------------------------------------+
      | uuid() |
      +--------------------------------------+
      | d570dfcf-1e2b-11ee-a470-000c29495816 |
      +--------------------------------------+
      1 row in set (0.01 sec)
    • 查看配置文件目录

      1
      2
      3
      4
      5
      6
      7
      mysql> show variables like 'datadir';
      +---------------+-----------------+
      | Variable_name | Value |
      +---------------+-----------------+
      | datadir | /var/lib/mysql/ |
      +---------------+-----------------+
      1 row in set (0.12 sec)
    • 编辑配置文件目录:vi /var/lib/mysql/auto.cnf,修改uuid为刚刚我们生成的uuid

      1
      2
      [auto]
      server-uuid=d570dfcf-1e2b-11ee-a470-000c29495816
    • 重启服务:service mysqld restart

配置主从服务器

主库配置:

  1. 修改MySQL数据库的配置文件vim /etc/my.conf

    找到[mysqld],在下面插入两行

    1
    2
    log_bin=mysql-bin #[必须]启用二进制日志
    server-id=128 #[必须]服务器唯一ID,只需要确保其id是唯一的就好
  2. 重启mysql服务systemctl restart mysqld

  3. 登录Mysql数据库,mysql -uroot -p。执行下面的SQL

    1
    grant replication slave on *.* to 'wzy'@'%' identified by 'Root@123456';

    上面的SQL的作用是创建一个用户wzy,密码为Root@123456,并且给wzy用户授予replication slave权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制,这是因为主库和从库之间需要互相通信,处于安全考虑,只有通过验证的从库才能从主库中读取二进制数据。

  4. 登录Mysql数据库,执行下述SQL:show master status,记录结果中File和Position的值

    1
    2
    3
    4
    5
    6
    7
    mysql> show master status;
    +------------------+----------+--------------+------------------+-------------------+
    | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
    +------------------+----------+--------------+------------------+-------------------+
    | mysql-bin.000001 | 436 | | | |
    +------------------+----------+--------------+------------------+-------------------+
    1 row in set (0.00 sec)

    上面sql的作用是查看Master的状态,执行完此SQL后不要再执行任何操作,否则上述内容就会发生变化。

从库配置:

  1. 修改MySQL数据库的配置文件/etc/my.cnf

    找到[mysqld],在下面插入一行

    1
    server-id=127 #[必须]服务器唯一ID,只需要确保其id是唯一的就好
  2. 重启mysql服务systemctl restart mysqld

  3. 登录到Mysql数据库,执行下面SQL

    1
    2
    3
    change master to master_host='192.168.186.128',master_user='wzy',master_password='Root@123456',master_log_file='mysql-bin.000001',master_log_pos=436;

    start slave;

    注意:如果执行第一句报错,可能是该虚拟机之前配置过slave,执行stop slave即可

  4. 登录Mysql数据库,执行SQL:show slave status \G;,查看从库的状态(这里\G是竖排查看)

    此时会出现一堆内容,着重看下面三个内容,如果显示为如下状态则成功

    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
       Slave_IO_State   : Waiting for master to send event
    Slave_IO_Running : Yes
    Slave_SQL_Running: Yes

    5. 在上一步中,我得到的结果并不和上面一致,根据弹幕大佬指点,需要修改克隆机的MySQL的uuid,这一步放在虚拟机配置部分。修改后再使用上述命令得到如下:

    ![image-20230709154519132](image-20230709154519132.png)

    ### 测试

    1. 这里我们通过Navicate连接两个虚拟机的数据库:

    ![image-20230709154943347](image-20230709154943347.png)

    2. 我们在主库新建一个数据库,并且创建一个`user`表,然后刷新从库,发现也出现了该数据库和其下面的表。并且修改主库,对应从库也会发生变化

    ![image-20230709155326829](image-20230709155326829.png)

    ---

    ## 读写分离案例

    ### 背景

    面对日益增加的系统访问量,数据库的吞吐量面临着巨大的瓶颈。对于同一时刻有**大量并发读操作****较少的写操作**类型的应用系统来说,将数据库拆分为`主库`和`从库`。`主库`主要负责处理事务性的**增删改**操作。`从库`主要负责**查询**操作

    这样就能有效避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善

    ### Sharding-JDBC介绍

    Sharding-JDBC定位为轻量级的Java框架,在Java的JDBC层提供额外的服务,它使得客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架

    使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离

    - 适用于任何基于JDBC的ORM框架,如:JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC
    - 支持任何第三方的数据库连接池,如:DBCP、C3P0、BoneCP、Druid等
    - 支持任意实现JDBC规范的数据库,目前支持MySQL、Oracle、SQLServer、PostgreSQL

    ### 入门案例

    使用Sharding-JDBC框架的步骤:

    1. 导入对应的maven坐标

    ```xml
    <dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
    </dependency>
  5. 在配置文件中添加读写分离规则

    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
    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.186.128:3306/reggie?serverTimezone=UTC
    username: root
    password: root
    # 从数据源
    slave:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.186.129:3306/reggie?serverTimezone=UTC
    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 #开启SQL显示,默认false
    # 解决冲突
    main:
    allow-bean-definition-overriding: true
  6. 在配置文件中配置允许bean定义覆盖配置项

    在前面配置完成后执行会报错,这是因为Sharding-JDBC中配置数据源和以前导入的德鲁伊连接池发生冲突,二者都想要创建数据源,所以在配置文件中加上上面最后两句,从而让bean定义并覆盖,从而解决冲突

瑞吉项目实现

导入数据

首先在linux主服务器上创建数据库reggie,然后将本地的reggie的数据库导出为sql文件,在主服务器上进行导入即可

  • 本地导出sql文件

    image-20230709195334557

  • 远程导入sql文件

    image-20230709200107819

    注意:此时直接导入会失败,这是因为远程的Mysql版本为5.7,而本地使用的是8.0,会有部分差别。需要将COLLATE utf8mb4_0900_ai_ci NULL 所有内容删掉即可

  • 此时远程主从服务器上都有了reggie的数据

流程实现

  1. 首先在Git上创建一个新的分支v1.1,便于后续维护

  2. 导入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>
  3. 修改配置文件,添加读写分离部分的内容

    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
    server:
    port: 8080
    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.186.128:3306/reggie?characterEncoding=utf-8
    username: root
    password: root
    # 从数据源
    slave:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.186.129:3306/reggie?characterEncoding=utf-8
    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 #开启SQL显示,默认false
    # 解决冲突
    main:
    allow-bean-definition-overriding: true

    # redis缓存
    redis:
    host: localhost
    port: 6379
    database: 0
    cache:
    redis:
    time-to-live: 3600000 #设置存活时间为一小时,如果不设置,则一直存活
    mybatis-plus:
    configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    global-config:
    db-config:
    id-type: assign_id

    reggie:
    path: D:\IDEA\imgs\

    注意:此时部分内容会爆红,但是不影响启动。

  4. 注意开启redis-server

  5. 然后我这里直接启动项目,会报druid相关的错误。进行下述修改后成功启动项目。根据弹幕的建议,将maven依赖中的druid依赖进行修改。

    • 修改前
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
    </dependency>
    • 修改后
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.15</version>
    </dependency>

然后实际的效果就是,新增、修改时候,会显示是在操作master服务器,查询时会显示在操作slave服务器。并且二者的数据库中都出现新增的内容。

  • master新增数据
1
2
2023-07-09 20:54:35.174  INFO 16852 --- [nio-8080-exec-7] ShardingSphere-SQL : Rule Type: master-slave
2023-07-09 20:54:35.174 INFO 16852 --- [nio-8080-exec-7] ShardingSphere-SQL : SQL: INSERT INTO employee ( id,username,name,password,phone,sex,id_number,create_time,update_time,create_user,update_user ) VALUES ( ?,?,?,?,?,?,?,?,?,?,? ) ::: DataSources: master
  • slave查询数据
1
2
2023-07-09 20:54:35.324  INFO 16852 --- [nio-8080-exec-9] ShardingSphere-SQL : Rule Type: master-slave
2023-07-09 20:54:35.325 INFO 16852 --- [nio-8080-exec-9] ShardingSphere-SQL : SQL: SELECT id,username, name,password,phone,sex,id_number,status,create_time,update_time,create_user,update_user FROM employee ORDER BY update_time DESC LIMIT ? ::: DataSources: slave

Nginx

概述

介绍

Nginx是一款轻量级的Web/反向代理服务器以及电子邮件(IMAP/POP3)代理服务器,其特点是占有内存少,并发能力强。事实上Nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用Nginx的网站有:百度、京东、新浪、网易、腾讯、淘宝等。

Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Pam6nep)开发的,第一个公开版本0.1.0发布于2004年10月4日。官网:https://nginx.org/

下载和安装

官网下载链接:https://nginx.org/en/download.html

安装过程:

  1. 首先安装依赖包:yum -y install gcc pcre-devel zlib-devel openssl openssl-devel

  2. 下载Nginx安装包wget:wget https://nginx.org/download/nginx-1.24.0.tar.gz

    • wget是从网络下载资源的命令。如果没有安装wget,执行:yum install wget
  3. 解压:tar -zxvf nginx-1.24.0.tar.gz -C /usr/local/

  4. 进入解压后的文件夹:cd /usr/local/nginx-1.24.0/

  5. 创建安装文件夹:mkdir /usr/local/nginx

  6. 指定安装路径并进行检查工作:./configure --prefix=/usr/local/nginx(此时还没安装,只是完成安装前的检查工作)

  7. 编译并安装:make && make install

目录结构

重点目录/文件:

  • conf/nginx.conf:nginx配置文件
  • html:存放静态文件(html、css、Js等)
  • logs:日志目录,存放日志文件
  • sbin/nginx:二进制文件,用于启动、停止Nginx服务

显示目录树形结构:

  • 在目录下使用tree命令,可以直观获取目录树状结构。如果没有安装此命令,需要执行:yum install tree

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    ├── 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

命令

  • 查看版本命令:进入sbin目录,输入./nginx -v

    1
    2
    [root@localhost sbin]# ./nginx -v
    nginx version: nginx/1.24.0
  • 检查配置文件正确性:进入sbin目录,输入./nginx -t,如果有错误会报错,而且会记录日志

    1
    2
    3
    [root@localhost sbin]# ./nginx -t
    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]# ./nginx
      [root@localhost sbin]# ps -ef | grep nginx
      root 31524 1 0 11:35 ? 00:00:00 nginx: master process ./nginx
      nobody 31525 31524 0 11:35 ? 00:00:00 nginx: worker process
      root 31763 7447 0 11:35 pts/0 00:00:00 grep --color=auto nginx

      启动后,可以通过访问ip地址80端口查看nginx主页,但是首先需要关闭80端口的防火墙

    • 停止:输入./nginx -s stop,停止服务后再次查看进程

      1
      2
      3
      [root@localhost sbin]# ./nginx -s stop
      [root@localhost sbin]# ps -ef | grep nginx
      root 33446 7447 0 11:36 pts/0 00:00:00 grep --color=auto nginx
  • 重新加载配置文件:当修改Nginx配置文件后,需要重新加载才能生效,可以使用下面命令重新加载配置文件:./nginx -s reload

上面的所有命令,都需要在sbin目录下才能运行,比较麻烦,所以可以将Nginx的二进制文件配置到环境变量中,这样无论在哪个目录下,都能使用上面的命令。

  1. 使用vim /etc/profile命令打开配置文件,并配置环境变量,保存并退出

    1
    2
    - PATH=$JAVA_HOME/bin:$PATH
    + PATH=/usr/local/nginx/sbin:$JAVA_HOME/bin:$PATH
  2. 重新加载配置文件,使用source /etc/profile命令。

此时在任意位置输入nginx即可启动服务,nginx -s stop即可停止服务

配置文件结构

Nginx配置文件(conf/nginx.conf)整体分为三部分

  • 全局块 和Nginx运行相关的全局配置

  • events块 和网络连接相关的配置

  • http块 代理、缓存、日志记录、虚拟主机配置

    http块中可以配置多个Server块,每个Server块中可以配置多个location块

    • http全局块
    • Server块
      • Server全局块
      • location块

niginx.conf文件如下:

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
worker_processes  1;                              <-- 全局块,event之前的

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;
}
}
}

具体应用

部署静态资源

Nginx可以作为静态web服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。

相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到Nginx中。

将静态资源部署到Nginx非常简单,只需要将文件复制到Nginx安装目录下的html目录中即可。

反向代理

概念

正向代理:

  • 正向代理是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。

  • 正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。

  • 正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器。(客户端知道代理服务器的存在)

image-20230710134658811

反向代理

  • 反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。
  • 用户不需要知道目标服务器的地址,也无须在用户端作任何设定。(客户端并不知道反向代理服务器的存在)

image-20230710134813300

二者作用

  • 正向代理
    • 访问原来无法访问的资源,如google
    • 可以做缓存,加速访问资源
    • 对客户端访问授权,上网进行认证
    • 代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息
  • 反向代理
    • 保证内网的安全,阻止web攻击,大型网站,通常将反向代理作为公网访问地址,Web服务器是内网。
    • 负载均衡,通过反向代理服务器来优化网站的负载。

二者区别与联系

  • 正向代理即是客户端代理, 代理客户端, 服务端不知道实际发起请求的客户端.
    反向代理即是服务端代理, 代理服务端, 客户端不知道实际提供服务的服务端.
  • 正向代理中,proxy和client同属一个LAN,对server透明。 反向代理中,proxy和server同属一个LAN,对client透明。

配置

首先假定两个服务器,一个是反向代理服务器:192.168.186.128,一个是web服务器:192.168.186.129

  • 首先修改反向代理服务器的配置文件:nginx.conf

    1
    2
    3
    4
    5
    6
    7
    8
    server {
    listen 82;
    server_name localhost;

    location / {
    proxy_pass http://http://192.168.186.129:8080; # 反向代理配置,将请求转发到指定服务
    }
    }
  • 此时的访问流程为:客户端–>192.168.186.128:82–>192.168.186.129:8080

    此时如果web服务器上部署了一个项目,假定一个get请求/hello返回一段字符串,此时在访问192.168.186.128:82/hello就相当于192.168.186.129:8080/hello,从而返回指定的字符串。

负载均衡

其实也是反向代理的一种应用

概念

早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。

  • 应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据。

  • 负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理。

image-20230710141655669

配置

首先假定有一个代理服务器,两个web服务器。

web服务器的ip为192.168.186.127。两个web服务器的ip为192.168.186.128192.168.186.129,然后两台机器都跑一个测试项目,但是修改输出区别不同的服务器。(可以让方法输出ip地址)

  • 修改服务器的配置文件:nginx.conf。并且重新加载配置文件:nginx -s reload

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    upstream targetServer{     #upstream指令可以定义一组服务器
    server 192.168.186.128;
    server 192.168.186.129;
    }
    server {
    listen 82;
    server_name localhost;

    location / {
    proxy_pass http://targetServer;
    }
    }
  • 然后访问代理服务器的192.168.186.127:82端口,就会转发到上面两个web服务器中的一个,且默认是轮询方法,也就是1-2-1-2交替

负载均衡策略

默认是轮询算法,第一次访问是192.168.186.128,第二次访问是192.168.186.129
也可以改用权重方式,权重越大,几率越大,现在的访问三分之二是第一台服务器接收,三分之一是第二台服务器接收
server 192.168.186.128 weight=10; server 192.168.186.129 weight=5

名称 说明
轮询 默认方式
weight 权重方式
ip_hash 依据ip分配方式
least_conn 依据最少连接方式
url_hash 依据url分配方式
fair 依据响应时间方式

前后端分离开发

之前开发存在的问题:

  • 开发人员同时负责前端和后端代码开发,分工不明确,开发效率低
  • 前后端代码混合在一个工程中,不便于管理
  • 对开发人员要求高,人员招聘困难

所以衍生出了一种前后端分离开发。

前后端分离开发

介绍

前后端分离开发,就是在项目开发过程中,对前端代码的开发专门由前端开发人员负责,后端代码由后端开发人员负责,这样可以做到分工明确,各司其职,提高开发效率,前后端代码并行开发,可以加快项目的开发速度。目前,前后端分离开发方式已经被越来越多的公司采用了,成为现在项目开发的主流开发方式。

前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为前端工程和后端工程

image-20230710143916867

开发流程

前后端开发人员都参照接口API文档进行开发。接口(API接口) 就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应参数等内容。

image-20230710144148801

YApi

更多可能是项目经理做的工作

介绍

YApi是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护API,YApi还为用户提供了优秀的交互体验,开发人员只需要利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。

YApi让接口开发更简单高效,让接口的管理更具有可读性、可维护性,让团队协作更合理。Git仓库:https://github.com/YMFE/yapi。要使用YApi,需要自己进行部署。

使用

  • 使用YApi,可以执行下面操作:
    • 添加项目
    • 添加分类
    • 添加接口
    • 编辑接口
    • 查看接口

Swagger

后端常用

介绍

使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做成各种格式的接口文档,以及在线接口调试页面等。官网:https://swagger.io/

这里用的是knife4j,是Java MVC框架集成Swagger生成Api文档的增强解决方案

使用

  1. 导入Maven坐标依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.3</version>
    </dependency>
  2. 导入knife4j相关配置类

    config包下的WebMvcConfig中进行配置。主要添加两个注解@EnableSwagger2@EnableKnife4j和注入一个bean。

    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
    @Slf4j
    @Configuration
    @EnableSwagger2
    @EnableKnife4j
    public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public 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.wzy.controller"))
    .paths(PathSelectors.any())
    .build();
    }

    //新增
    private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
    .title("瑞吉外卖")
    .version("1.0")
    .description("瑞吉外卖接口文档")
    .build();
    }
    }

    注意:将controller路径改为自己的包路径

  3. 设置静态资源,否则接口文档页面无法访问。不过如果是SpringBoot项目,静态资源文件夹默认在那个位置,所以不用设置。

    1
    registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
  4. 在LoginCheckFilter中设置不需要处理的请求路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //定义不需要处理的请求
    String[] urls = new String[]{
    "/employee/login",
    "/employee/logout",
    "/backend/**", //静态资源
    "/front/**",
    "/common/**",

    //对用户登陆操作放行
    "/user/login",
    "/user/sendMsg"

    //Swagger放行
    "/doc.html",
    "/webjars/**",
    "/swagger-resources",
    "/v2/api-docs"
    };
  5. 到这里直接启动,我这里会启动失败,根据网络查询,是因为swagger版本和springboot2.6以上版本出现了不兼容情况,需要在application.yml中做如下配置

    1
    2
    3
    4
    spring:
    mvc:
    pathmatch:
    matching-strategy: ant_path_matcher
  6. 启动服务,访问 http://localhost:8080/doc.html 即可看到生成的接口文档

    image-20230710161123044

常用注释

注解 说明
@Api 用在请求的类上,例如Controller,表示对类的说明
@ApiModel 用在类上,通常是个实体类,表示一个返回响应数据的信息
@ApiModelProperty 用在属性上,描述响应类的属性
@ApiOperation 用在请求的方法上,说明方法的用途、作用
@ApilmplicitParams 用在请求的方法上,表示一组参数说明
@ApilmplicitParam 用在@ApilmplicitParams注解中,指定一个请求参数的各个方面

加上这些注解,可以让生成的接口文档更规范。示例如下:

user.java

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
@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;

//性别 0 女 1 男
@ApiModelProperty("性别 0 女 1 男")
private String sex;

//身份证号
@ApiModelProperty("身份证号")
private String idNumber;

//头像
@ApiModelProperty("头像")
private String avatar;

//状态 0:禁用,1:正常
@ApiModelProperty("状态 0:禁用,1:正常")
private Integer status;
}

UserController.java

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
@RestController
@Slf4j
@RequestMapping("/user")
@Api(tags = "用户相关接口")
public class UserController {

@Autowired
private UserService userService;

@Autowired
private RedisTemplate redisTemplate;

/**
* 发送验证码
*
* @param user
* @param session
* @return
* @throws MessagingException
*/
@PostMapping("/sendMsg")
@ApiOperation("发送验证接口")
public R<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); //这部分抛出异常

//[old]将要发送的验证码保存在session,然后与用户填入的验证码进行对比
// session.setAttribute(phone, code);

//[new]将生成的验证码缓存到Redis中,并设置有效期为5分钟
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);

return R.success("验证码发送成功");
}
return R.error("验证码发送失败");
}

/**
* 移动端用户登录
*
* @param map
* @param session
* @return
*/
@PostMapping("/login")
@ApiOperation("用户登录接口")
@ApiImplicitParam(name = "map",value = "map集合接收数据",required = true)
public R<User> login(@RequestBody Map map, HttpSession session) {
log.info(map.toString());
//获取邮箱
String phone = map.get("phone").toString();
//获取验证码
String code = map.get("code").toString();

//[old]从session中获取保存的验证码
// Object codeInSession = session.getAttribute(phone);
// log.info("session中的缓存验证码:{}", codeInSession);

//[new]从Redis中获取缓存验证码
Object codeInSession = redisTemplate.opsForValue().get(phone);
log.info("redis中的缓存验证码:{}", codeInSession);

//进行验证码的比对
if (codeInSession != null && codeInSession.equals(code)) {
//判断一下当前用户是否存在
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.setStatus(1);
userService.save(user);
user.setName("游客" + codeInSession);
}
//登录成功后,需要保存session,表示登录状态,因为前面的过滤器进行了用户登录判断
session.setAttribute("user", user.getId());

//[new]如果用户登录成功,删除redis中缓存的验证码
redisTemplate.delete(phone);

//并将其作为结果返回
return R.success(user);
}
return R.error("登录失败");
}

/**
* 用户登出
*
* @param request
* @return
*/
@PostMapping("/loginout")
@ApiOperation("用户登出接口")
public R<String> loginout(HttpServletRequest request) {
request.getSession().removeAttribute("user");
return R.success("退出成功");
}
}

此时生成新的文档,可以看出user相关内容已经变成中文说明,并且相应参数也有对应的说明

image-20230710162509580

项目部署

部署环境说明

image-20230710162928826

一共需要三台服务器

  • 192.168.186.128(服务器A)
    • Nginx:部署前端项目、配置反向代理
    • MySql:主从复制结构中的主库
  • 192.168.186.129(服务器B)
    • jdk:运行java项目
    • git:版本控制工具
    • maven:项目构建工具
    • jar:Spring Boot 项目打成jar包基于内置Tomcat运行
    • MySql:主从复制结构中的从库
  • 192.168.186.128(服务器C,这里我用服务器A进行缓存)
    • Redis:缓存中间件

部署前端项目

  1. 在服务器A中安装Nginx,将前端项目打包目录上传到Nginx的html目录下

    这里是直接拿来课程资源中的打包好的dist,如果实际使用需要自己打包,因为部分前端内容做了调整

  2. 修改Nginx配置文件nginx.conf,新增如下配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    server {
    listen 80;
    server_name localhost;

    location / {
    root html/dist;
    index index.html;
    }
    #反向代理配置
    location ^~ /api/ {
    rewrite ^/api/(.*)$ /$1 break;
    proxy_pass http://192.168.186.129:8080;
    }
    }

    记得修改后reload配置文件:nginx -s reload

  3. 启动Nginx服务器测试,显示前端页面:

    image-20230710170053781

补充:针对前面2中的反向代理设置中的rewrite部分内容的作用:

  • 当我们点击了前端登录页面后,会发送如下请求:

image-20230710170236768

  • 而实际后端controller中,只是对192.168.186.129:8080/employee/login请求做处理,二者相差了/api内容,所以通过rewrite去除内容

部署后端项目

在服务器B中安装JDK,Git,MySql。

  • 服务器C开启Redis服务:进入redis的根目录下执行src/redis-server ./redis.conf
1
2
3
42106:C 10 Jul 17:21:47.854 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
42106:C 10 Jul 17:21:47.854 # Redis version=4.0.0, bits=64, commit=00000000, modified=0, pid=42106, just started
42106:C 10 Jul 17:21:47.854 # Configuration loaded
  • 修改配置文件中的图片路径以及redis相关内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
# redis缓存
redis:
host: 192.168.186.128
port: 6379
database: 0
password: 123456
cache:
redis:
time-to-live: 3600000 #设置存活时间为一小时,如果不设置,则一直存活

reggie:
path: /usr/local/img/
  • 将项目打成jar包,手动上传并部署。打包使用maven的package即可,然后将jar包上传服务器B,在包所在目录下执行命令:java -jar [打包好的jar包名]

    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
    [root@localhost reggie]# java -jar springboot_reggie-0.0.1-SNAPSHOT.jar 

    . ____ _ __ _ _
    /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
    \\/ ___)| |_)| | | | | || (_| | ) ) ) )
    ' |____| .__|_| |_|_| |_\__, | / / / /
    =========|_|==============|___/=/_/_/_/
    :: Spring Boot :: (v2.7.12)

    2023-07-10 17:31:25.717 INFO 67520 --- [ main] com.wzy.SpringbootReggieApplication : Starting SpringbootReggieApplication v0.0.1-SNAPSHOT using Java 1.8.0_171 on localhost.localdomain with PID 67520 (/usr/local/reggie/springboot_reggie-0.0.1-SNAPSHOT.jar started by root in /usr/local/reggie)
    2023-07-10 17:31:25.732 INFO 67520 --- [ main] com.wzy.SpringbootReggieApplication : No active profile set, falling back to 1 default profile: "default"
    2023-07-10 17:31:29.678 INFO 67520 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
    2023-07-10 17:31:29.681 INFO 67520 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
    2023-07-10 17:31:29.712 INFO 67520 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 12 ms. Found 0 Redis repository interfaces.
    2023-07-10 17:31:31.122 INFO 67520 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
    2023-07-10 17:31:31.143 INFO 67520 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
    2023-07-10 17:31:31.143 INFO 67520 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.75]
    2023-07-10 17:31:31.931 INFO 67520 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
    2023-07-10 17:31:31.932 INFO 67520 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4434 ms
    Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
    2023-07-10 17:31:33.435 INFO 67520 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
    2023-07-10 17:31:33.927 INFO 67520 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-2} inited
    Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@67c33749'
    Property 'mapperLocations' was not specified.
    _ _ |_ _ _|_. ___ _ | _
    | | |\/|_)(_| | |_\ |_)||_|_\
    / |
    3.4.2
    2023-07-10 17:31:37.631 INFO 67520 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
    2023-07-10 17:31:37.644 INFO 67520 --- [ main] com.wzy.SpringbootReggieApplication : Started SpringbootReggieApplication in 14.327 seconds (JVM running for 16.383)
    2023-07-10 17:31:37.646 INFO 67520 --- [ main] com.wzy.SpringbootReggieApplication : 项目启动成功

    也可以选择git拉取代码,然后shell脚本自动部署,此时还需要安装Maven,具体内容参考Linux入门中项目部署章节)

部署完后端项目之后,就能完成正常的登录功能了,也能进入到后台系统进行增删改查操作

image-20230710173301342

并且修改内容,在对应的主服务器和从服务器上的mysql数据同时变化,实现了MySQL主从分离。

至于缓存功能,代码中只对手机端套餐显示上添加了redis缓存,但是课程资源中并没有给出手机端的页面,所以目前没有测试Redis缓存功能。