跳转至

后端

一、新增标签表

1、需求分析

建议用标签,不要用分类,更灵活。

  1. 性别:男、女
  2. 方向:Java、C++、Go、前端
  3. 正在学:Spring
  4. 目标:考研、春招、秋招、社招、考公、竞赛(蓝桥杯)、转行、跳槽
  5. 段位:初级、中级、高级、王者
  6. 身份:小学、初中、高中、大一、大二、大三、大四、学生、待业、已就业、研一、研二、研三
  7. 状态:乐观、有点丧、一般、单身、已婚、有对象

2、字段

  1. id, int, 主键
  2. 标签名, varchar, 非空(必须唯一,唯一索引)
  3. 上传标签的用户, userId, int(如果要根据 userId 查已上传标签的话,最好加上,普通索引)
  4. 父标签 id ,parentId,int(分类)
  5. 是否为父标签 isParent,tinyint(0 不是父标签、1 - 父标签)
  6. 创建时间 createTime,datetime
  7. 更新时间 updateTime,datetime
  8. 是否删除 isDelete, tinyint(0、1)

3、使用分析

怎么查询所有标签,并且把标签分好组?

按父标签 id 分组,能实现 ✅

根据父标签查询子标签?

根据 id 查询,能实现 ✅

根据标签查询用户?

①直接在用户表补充 tags 字段,存 json 字符串 ✅

['Java', '男'] 
  • 优点:查询方便、不用新建关联表,标签是用户的固有属性(除了该系统、其他系统可能要用到,标签是用户的固有属性)节省开发成本
  • 查询用户列表,查关系表拿到这 100 个用户有的所有标签 id,再根据标签 id 去查标签表。哪怕性能低,可以用缓存。
  • 缺点:用户表多一列

②加一个关联表,记录用户和标签的关系关联表的应用场景 💬

  • 查询灵活,可以正查反查
  • 缺点:要多建一个表、多维护一个表
  • 重点:企业大项目开发中尽量减少关联查询,很影响扩展性,而且会影响查询性能

4、SQL语言分类

SQL:Structured Query Language 结构化查询语言

  1. DDL define 建表、操作表
  2. DML manage 管理,更新删除数据,影响实际表里的内容
  3. DCL control 控制,权限
  4. DQL query 查询,select

二、根据标签搜索用户

1、逻辑实现

  1. SQL 查询(实现简单,可以通过拆分查询进一步优化)
  2. 内存查询(灵活,可以通过并发进一步优化)
  • 如果参数可以分析:根据用户的参数去选择查询方式,比如标签数
  • 如果参数不可分析:并且数据库连接足够、内存空间足够,可以并发同时查询,谁先返回用谁。
  • 还可以 SQL 查询与内存计算相结合,比如先用 SQL 过滤掉部分 tag,再去内存过滤。

2、解析JSON字符串

序列化:java对象转成 json

反序列化:把 json 转为 java 对象

java json 序列化库:

  1. gson(google 的)
  2. fastjson alibaba(阿里出品,快,但是漏洞太多)
  3. jackson
  4. kryo

三、整合接口文档

1、接口文档意义

  1. 有个书面内容(背书或者归档),便于大家参考和查阅,便于 沉淀和维护 ,拒绝口口相传
  2. 接口文档便于前端和后端开发对接,前后端联调的 介质 。后端 => 接口文档 <= 前端
  3. 好的接口文档支持在线调试、在线测试,可以作为工具提高我们的开发测试效率

2、Swagger

① 引入依赖(Swagger 或 Knife4j)

<!-- swagger 接口文档 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

② 自定义 Swagger 配置类,定义需要生成接口文档的代码位置(Controller)

// 配置类
@Configuration 
// 开启 swagger2 的自动配置
@EnableSwagger2 
public class SwaggerConfig {
    @Bean
    public Docket docket() {
        // 创建一个 swagger 的 bean 实例
        return new Docket(DocumentationType.SWAGGER_2)
            // 配置接口信息
            .select() // 设置扫描接口
            // 配置如何扫描接口
            // 扫描指定包下的接口
            .apis(RequestHandlerSelectors
                  .basePackage("com.yupi.usercenter.controller"))
            .paths(PathSelectors
                   .any() // 满足条件的路径,该断言总为true
                   //.none() // 不满足条件的路径,该断言总为false(可用于生成环境屏蔽 swagger)
                   //.ant("/user/**") // 满足字符串表达式路径
                   //.regex("") // 符合正则的路径
                  )
            .build();
    }

    // 基本信息设置
    private ApiInfo apiInfo() {
        Contact contact = new Contact(
            "shayu", // 作者姓名
            "shayuyu.cn", // 作者网址
            "shayu-yusha@qq.com"); // 作者邮箱
        return new ApiInfoBuilder()
            .title("鱼泡伙伴匹配系统-接口文档") // 标题
            .description("众里寻他千百度,慕然回首那人却在灯火阑珊处") // 描述
            .termsOfServiceUrl("https://www.baidu.com") // 跳转连接
            .version("1.0") // 版本
            .license("Swagger-的使用(详细教程)")
            .licenseUrl("https://xxxx")
            .contact(contact)
            .build();
    }
}

线上环境不要把接口暴露出去!!!

  • 可以通过在 SwaggerConfig 配置文件开头加上 @Profile({"dev", "test"}) 限定配置仅在部分环境开启
  • 可以通过在 controller 方法上添加注解来自定义生成的接口描述信息

如果 springboot version >= 2.6,需要添加如下配置:

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

③ 启动:http://localhost:8080/api/swagger-ui.html

3、Knife4j

① 引入依赖

<!-- knife4j 接口文档 -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.7</version>
</dependency>

② 创建SwaggerConfig文件

@Configuration
@EnableSwagger2WebMvc
//版本控制访问
@Profile({"dev", "test"})  
public class SwaggerConfig {

    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // 这里一定要标注你控制器的位置
                .apis(RequestHandlerSelectors
                      .basePackage("com.yupi.usercenter.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * api 信息
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("鱼皮用户中心")
                .description("鱼皮用户中心接口文档")
                .termsOfServiceUrl("https://github.com/liyupi")
                .build();
    }
}

如果 springboot version >= 2.6,需要添加如下配置:

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

③ 启动:http://localhost:8080/api/doc.html

四、EasyExcel

1、概述

EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel (alibaba.com)

两种读对象的方式:

  1. 确定表头:建立对象,和表头形成映射关系
  2. 不确定表头:每一行数据映射为 Map

两种读取模式:

  1. 监听器
    1. 先创建监听器、在读取文件时绑定监听器。
    2. 单独抽离处理逻辑,代码清晰易于维护;
    3. 一条一条处理,适用于数据量大的场景。
  2. 同步读
    1. 无需创建监听器,一次性获取完整数据。
    2. 方便简单,但是数据量大时会有等待时间,也可能内存溢出。

2、使用流程

1)导入依赖

<!-- easy Excel -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.0</version>
</dependency>

2)创建数据接收对象

@Data
public class User {
    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("学号")
    private String id;
}

3)方式一:监听器读取

① 新建监听器

public class UserListener implements ReadListener<User> {
    // 这个每一条数据解析都会来调用
    @Override
    public void invoke(User data, AnalysisContext context) {
        System.out.println(data);
    }

    // 所有数据解析完成后调用
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        System.out.println("已解析完成");
    }
}

② 编写读取脚本

public class App {
    public static void main(String[] args) {
        String FileName = "/xxxx/test.xlsx";
        try {
            EasyExcel.read(FileName, User.class, 
                           new UserListener()).sheet().doRead();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4)方式二:同步读

public static void main(String[] args) {
    String FileName = "/xxxx/test.xlsx";
    List<User> result = EasyExcel.read(FileName).head(User.class)
        .sheet().doReadSync();
    for (User user : result) {
        System.out.println(user);
    }
}

五、banner.txt

在resource文件夹下新建banner.txt,即可替换Springboot加载的打印信息

Spring Boot banner在线生成工具,制作下载英文banner.txt,修改替换banner.txt文字实现自定义,个性化启动banner-bootschool.net

                        _                  _   _      _                    
                       | |                (_) | |    (_)                   
__      __  _ __ ___   | |__   __      __  _  | | __  _        ___   _ __  
\ \ /\ / / | '_ ` _ \  | '_ \  \ \ /\ / / | | | |/ / | |      / __| | '_ \ 
 \ V  V /  | | | | | | | | | |  \ V  V /  | | |   <  | |  _  | (__  | | | |
  \_/\_/   |_| |_| |_| |_| |_|   \_/\_/   |_| |_|\_\ |_| (_)  \___| |_| |_|

六、Redis

1、Docker安装Redis

使用宝塔面板演示

一定要选择「自定义部署」,才能设置Redis密码

可以使用「QuickRedis」可视化工具连接Redis数据库

QuickRedis:https://quick123.net/

2、Session共享(单点登录)

整合Springboot,将登录状态的Session信息,存入Redis数据库。

① 引入Redis依赖,注意版本需要和Springboot版本一致(不写版本默认与父版本一致)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

② 引入 spring-session 和 redis 的整合,使得自动将 session 存储到 redis 中

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

③ 进行application.yml的配置(Springboot3.1.5)

# redis配置
spring:
    data:
        redis:
            port: 6379
            host: localhost
            # password: 1234
            database: 0
            timeout: 100000

# redis与session整合配置
spring:
    session:
        redis:
            # 缓存刷新模式:保存时才刷新
            flush-mode: on_save
            # 缓存保存模式:读写操作都进行缓存
            save-mode: always

补充Springboot2的配置

spring:
    redis:
        port: 6379
        host: localhost
        database: 0

spring:
    session:
        store-type: redis

3、缓存

数据查询慢怎么办 → 提前把数据取出来保存好(通常保存到读写更快的介质,比如内存)

Spring Data:通用的数据访问框架,定义了一组 增删改查 的接口mysql、redis、jpa

① 引入Redis整合依赖,注意版本需要和Springboot版本一致(不写版本默认与父版本一致)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

② 引入 spring-data-redis

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
</dependency>

③ 进行application.yml的配置(Springboot3.1.5)

# redis配置
spring:
    data:
        redis:
            port: 6379
            host: localhost
            # password: 1234
            database: 0
            timeout: 100000

④创建自定义序列化

@Configuration
public class RedisTemplateConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        // 创建RedisTemplate对象
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);
        // 设置Key的序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        // 创建Json序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置Value的序列化
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        return redisTemplate;
    }
}

⑤在Service中添加Redis缓存

@Resource
private RedisTemplate<String, Object> redisTemplate;

@Override
public BaseResponse<List<Download>> getDownloadList(Integer typeId, HttpServletRequest httpServletRequest) {
    // 鉴权
    Admin admin = (Admin) httpServletRequest.getSession().getAttribute("admin");
    String redisKey = admin == null ? "hpan:list:user" : "hpan:list:admin";
    // 查询redis
    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    List<Download> list = (List<Download>) valueOperations.get(redisKey);
    // 存在缓存 => 返回
    if (list != null) {
        System.out.println("Redis Get Data");
        return ResultUtils.success(list);
    }
    // 不存在缓存 => 查询
    QueryWrapper<Download> queryWrapper = new QueryWrapper<>();
    if (typeId != null) queryWrapper.eq("type", typeId);
    if (admin == null) queryWrapper.eq("is_enable", 1);
    list = this.list(queryWrapper);
    // 存入redis
    valueOperations.set(redisKey, list, 30 * 1000, TimeUnit.MILLISECONDS);
    // 返回数据
    System.out.println("MySQL Get Data");
    return ResultUtils.success(list);
}

通过QuickRedis查看redis数据:

运行结果:

补充:spring-data-redis默认使用了lettuce连接池,提高了连接的复用性

4、缓存预热

优点:

  1. 解决了第一个加载很慢的问题
  2. 保护数据库

缺点:

  1. 增加开发成本(额外的开发、设计)
  2. 预热的时机和时间如果错了,可能你缓存的数据不对或者太老
  3. 需要占用额外空间(避免内存溢出)

实现 → 使用定时任务:

  1. Spring Scheduler(spring boot 默认整合)
  2. Quartz(独立于 Spring 存在的定时任务框架)
  3. XXL-Job 分布式任务调度平台(通过界面 + sdk)

使用Spring Scheduler实现定时任务:

① 主类开启 @EnableScheduling

② 给需要定时的方法开启@Scheduled注解,指定 cron 表达式或者执行频率

在线crontab表达式执行时间计算 - 码工具 (matools.com)

在线Cron表达式生成器 (qqe2.com)

@Component
public class RefreshJob {
    @Scheduled(cron = "0/5 * * * * ? ")
    public void refresh() {
        System.out.println("RefreshJob: " + new Date());
    }
}