伙伴匹配系统

空~2022年12月19日大约 5 分钟

伙伴匹配系统

领域模型中的实体类

VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。 DTO(Data Transfer Object):数据传输对象,这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。 DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。 PO(PersistentObject):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。

通过标签查询全部用户

  1. 允许用户传入多个标签,多个标签都存在才搜索出来 and。like '%Java%' and like '%C++%'。
  2. 允许用户传入多个标签,有任何一个标签存在就能搜索出来 or。like '%Java%' or like '%C++%'

SQL 查询

实现简单,可以通过拆分查询进一步优化

基本思路:

拼接 SQL 语句, 执行, 数据脱敏, 返回

private List<UserVO> sqlSearch(List<String> tagsList) {
    // sql 语句 like %java% and like %python%
    QueryWrapper<User> query = new QueryWrapper<>();
    for (String tag : tagsList) {
        query = query.like("tags", tag);
    }
    // 脱敏 返回
    return userMapper.selectList(query).stream().map(this::getSafetyUser).collect(Collectors.toList());
}

内存查询

灵活,可以通过并发进一步优化

基本思路:

查询全部用户, 在内存中过滤掉不符合条件的用户, 数据脱敏, 返回

private List<UserVO> memorySearch(List<String> tagsList) {
    // 查询全部用户
    QueryWrapper<User> query = new QueryWrapper<>();
    List<User> userList = userMapper.selectList(query);
    // 在内存中过滤掉不符合条件的用户
    List<User> tmpUserTagsList = userList.stream().filter(user -> {
        String userTags = user.getTags();
        if (StringUtils.isBlank(userTags)) {
            return false;
        }
        Gson gson = new Gson();
        Set<String> userTagsList = gson.fromJson(userTags, new TypeToken<Set<String>>() {
        }.getType());
        for (String tag : userTagsList) {
            if (!tagsList.contains(tag)) {
                return false;
            }
        }
        return true;
    }).collect(Collectors.toList());
    // 脱敏 返回
    return tmpUserTagsList.stream().map(this::getSafetyUser).collect(Collectors.toList());
}

实际业务优化思路

  • 如果参数可以分析,根据用户的参数去选择查询方式,比如标签数量

  • 如果参数不可分析,并且数据库连接足够、内存空间足够,可以并发同时查询,谁先返回用谁

  • 还可以 SQL 查询与内存计算相结合,比如先用 SQL 过滤掉部分 tag

Swagger + Knife4j 接口文档

引入依赖(Swagger 或 Knife4j:https://doc.xiaominfo.com/knife4j/documentation/get_start.htmlopen in new window

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
    <version>4.0.0</version>
</dependency>

自定义 Swagger 配置

knife4j:
  enable: true
  openapi:
    title: Knife4j官方文档
    description: "`我是测试`,**你知道吗**
    # aaa"
    email: xiaoymin@foxmail.com
    concat: 八一菜刀
    url: https://docs.xiaominfo.com
    version: v4.0
    license: Apache 2.0
    license-url: https://stackoverflow.com/
    terms-of-service-url: https://stackoverflow.com/
    group:
      test1:
        group-name: 分组名称
        api-rule: package
        api-rule-resources:
        # 定义需要生成接口文档的代码位置(Controller)
          - com.yupi.usercenter

可以通过在 controller 方法上添加 @Api @ApiImplicitParam(name = "name",value = "姓名",required = true) @ApiOperation(value = "向客人问好") 等注解来自定义生成的接口描述信息

千万注意:线上环境不要把接口暴露出去!!!配置类可以通过在 SwaggerConfig 配置文件开头加上 @Profile({"dev", "test"}) 限定配置仅在部分环境开启, 配置文件可以直接切换不同环境的下的配置

Session 共享实现

安装 Redis

引入 redis,能够操作 redis:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.4</version>
</dependency>

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

<!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.6.3</version>
</dependency>

修改 spring-session 存储配置 spring.session.store-type

默认是 none,表示存储在单台服务器

store-type: redis,表示从 redis 读写 session

数据分页

https://baomidou.com/pages/2976a3/#spring-bootopen in new window

配置分页插件

@Configuration
public class MybatisPlusConfig {

    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

查询时分页

@GetMapping("/recommend")
public BaseResponse<Page<User>> recommendUsers(long pageSize, long pageNum) {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    Page<User> page = userService.page(new Page<>(pageNum, pageSize), queryWrapper);
    return ResultUtils.success(page);
}

定时任务

  1. Spring Scheduler(spring boot 默认整合了)
  2. Quartz(独立于 Spring 存在的定时任务框架)
  3. XXL-Job 之类的分布式任务调度平台(界面 + sdk)
  1. 主类开启 @EnableScheduling
  2. 给要定时执行的方法添加 @Scheduling 注解,指定 cron 表达式或者执行频率

缓存热点数据

@Resource
private UserService userService;

@Resource
private RedisTemplate<String, Object> redisTemplate;

private final List<Long> mainUserList = Collections.singletonList(1L);

@Scheduled(cron = "0 59 0 * * *")
public void doCacheRecommendUser() {
    for (Long userId : mainUserList) {
        String redisKey = String.format("user:recommend:%s", userId);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
        redisTemplate.opsForValue().set(redisKey, userPage, 1, TimeUnit.DAYS);
    }
}

不要去背 cron 表达式!!!!!

  • https://cron.qqe2.com/
  • https://www.matools.com/crontab/

控制定时任务的执行

利用分布式锁控制定时任务在同一时间只有 1 个服务器能执行

为啥需要分布式锁?

  1. 有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问到资源
  2. 单个锁只对单个 JVM 有效

使用 Redission 的分布式锁:

引入依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

配置 Redisson 客户端:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.202.100:6379")
                .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

使用 Redission 的分布式锁:

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");
        }finally{
            //释放锁
            lock.unlock();
        }
    }
}