伙伴匹配系统
伙伴匹配系统
领域模型中的实体类
VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。 DTO(Data Transfer Object):数据传输对象,这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。 DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。 PO(PersistentObject):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。
通过标签查询全部用户
- 允许用户传入多个标签,多个标签都存在才搜索出来 and。like '%Java%' and like '%C++%'。
- 允许用户传入多个标签,有任何一个标签存在就能搜索出来 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.html)
<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-boot
配置分页插件
@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);
}
定时任务
- Spring Scheduler(spring boot 默认整合了)
- Quartz(独立于 Spring 存在的定时任务框架)
- XXL-Job 之类的分布式任务调度平台(界面 + sdk)
- 主类开启
@EnableScheduling
- 给要定时执行的方法添加
@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 个服务器能执行
为啥需要分布式锁?
- 有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问到资源
- 单个锁只对单个 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();
}
}
}