FightPart项目疑问记录
需求分析
- 用户自定义添加标签,标签的分类(类型,怎么分类)比如Java/C++/网络安全,工作/大学
- 主动搜索,允许用户根据标签去搜索其他用户
- 组队
- 创建队伍
- 加入队伍
- 根据标签查询队伍
- 邀请其他人
- 退出队伍
- 允许用户修改标签
- 推荐
- 相似度计算算法+本地分布式计算
后端技术栈
- Java+SpringBoot框架
- SpringMVC+MyBatis+MyBatisPlus(提高开发效率)
- MySQL数据库
- Redis缓存
- Swagger+Knife4j接口文档
数据库表设计
标签的分类(有哪些、怎么分)
不建议使用boolean定义,不灵活
新增标签表(分类表)
建议用标签,不使用分类,更灵活.
性别:男、女
学习方向:Java、C++、Go、前端、网络安全
目标:考研、考公、春招、社招、竞赛、转行、跳槽
段位:
身份:
状态:
着重自定义|
表设计
- id int 主键、
- 标签名 varchar 非空(必须唯一,唯一索引)
- 上传标签的用户 userld int(如果要根据 userld 查已上传标签的话,最好加上,普通索引)
- 父标签 id ,parentld,int(分类)
- 是否为父标签 isParent, tinyint(0 不是父标签、1-父标签)
- 创建时间 createTime, datetime
- 更新时间 updateTime,datetime
- 是否删除 isDelete, tinyint(0、1)
索引最好加在有很多个值的列
验证设计
- 通过父标签id分组实现查询所有标签
- 根据父标签id查询子标签
修改用户表
hh又是经典大小写踩坑环节,记得选择实际的列.(className也注意一下)
lambda表达式进化
初始化
1 | userList.forEach(user -> { |
2.0
Java 8 的函数式编程特性,将当前对象的 getSafetyUser
方法作为参数传递给 forEach
.
1 | userList.forEach(this::getSafetyUser); |
map
是一种中间操作,用于将流中的每个元素应用某个函数并返回一个新的流.
Java 8 Stream API
操作类型 | 方法 | 描述 |
---|---|---|
中间操作 | filter(Predicate) |
筛选符合条件的元素,返回一个新的流。 |
map(Function) |
将每个元素转换为另一种形式,返回新的流。 | |
flatMap(Function) |
将每个元素转换为一个流,然后将多个流合并为一个流。 | |
sorted() |
根据自然顺序或提供的比较器对元素进行排序,返回排序后的流。 | |
distinct() |
去除流中的重复元素,返回一个只含唯一元素的流。 | |
limit(long n) |
截取前 n 个元素,返回一个新的流。 |
|
skip(long n) |
跳过前 n 个元素,返回剩下的元素组成的新流。 |
|
终端操作 | collect(Collector) |
将流中的元素收集到一个集合中,如 List 、Set 。 |
forEach(Consumer) |
对每个元素执行给定的动作,没有返回值。 | |
reduce(BinaryOperator) |
将流中的元素组合成一个值,常用于求和、乘积等聚合操作。 | |
count() |
返回流中元素的数量。 | |
findFirst() |
返回流中的第一个元素(如果存在则返回一个 Optional )。 |
|
findAny() |
返回流中的任意一个元素(适用于并行流)。 | |
allMatch(Predicate) |
检查是否所有元素都匹配给定的条件,返回布尔值。 | |
anyMatch(Predicate) |
检查是否至少有一个元素匹配给定的条件,返回布尔值。 | |
noneMatch(Predicate) |
检查是否没有任何元素匹配给定的条件,返回布尔值。 |
shift + alt + -/+ 折叠/展开所有方法
Mybatis-Plus开启SQL日志打印
1 | mybatis-plus: |
使用stream API filter过滤不满足条件的用户.
1 | userList.stream().filter(user -> {}) ; |
性能优化
根据实际业务场景做取舍.【不断测试,注意考虑连接数据库耗时】
搜索标签
- 允许用户传入多个标签,多个标签同时存在才能搜索的出.and like’%Java%’ and like ‘%Python%’
- 允许用户传入多个标签,有任何一个标签存在就能搜索出来.or like’%Java%’ or like ‘%Python%’
- SQL查询(简单易实现,可以拆分查询)
- 内存查询(灵活,可以通过并发进一步优化)
- SQL和内存计算相结合,比如先用SQL过滤掉部分tag
如果参数可以分析,根据用户的参数去选择查询方式,比如标签数.
如果参数不可以分析,并且数据库连接足够、内存空间足够,可以同时并发查询,谁先返回用谁.
通过实际测试决定.
表示过时的方法
1 |
SQL查询
1 |
|
并发处理parallelStream()
原查询(先数据库过滤再内存过滤)
1 |
|
并发处理
1 |
|
由于parallelStream()使用的是一个公共线程池,如果是一个特别复杂的并发查询,此时一个线程池中可能会全部是parallelStream()执行的相应查询操作,线程池无法将其他任务进行分配,导致性能降低.
TODO 补充parallel陷阱.
Optional.ofNullable().orElse()
,判断一个可能为空的值,如果为空返回对应的值,不为空返回orElse中的值.
后端整合Swagger + Knife4j接口文档
什么是接口文档
写接口的文档,每条接口包括:
- 请求参数
- 相应参数
- 错误码
- 接口地址
- 接口名称
- 请求类型
- 请求格式
- 备注
一般由后端/负责人提供,前端和后端使用.
为什么需要接口文档?
- 书面内容(归档/背书),便于参考和查阅以及维护和沉淀,
- 接口文档便于前后端做开发对接,前后端联调的介质.
- 好的接口文档支持在线调试、在线测试,作为工具提高我们的开发效率.
怎么做接口文档?
- 手写(比如腾讯文档、Markdown笔记)
- 自动化接口文档生成,自动根据项目代码生成完整的文档或在线调试的网页.Swagger,Postman(侧重接口管理)
接口文档有哪些技巧
swagger原理
- 引入依赖(Swagger/Knife4j)
- 自定义Swagger配置类
- 定义需要生成接口文档的代码位置(Controller)
- 注意!!线上环境不要把接口暴露出去,可以通过SwaggerConfig配置文件开头加上
@Profile({"dev","test"})
让定义的Bean在特定的环境生效【限定配置仅在部分环境开启】 - 启动即可
- 可以通过在Conroller方法上添加@Api、@ApilmplicitParam(name =”name”,value=”姓名”,required=true)、@ApiOperation(value=”向客人问好”)等注解来自定义生成的接口描述信息
Swagger和 SpirngBoot版本 >=2.6,不兼容问题.需配置默认的路径匹配策略(同步配置Knif4j ,修改注解)@EnableSwagger2WebMvc
+@Bean(value = "defaultApi2")
1 | <dependency> |
1 | # 修改swagger的路径匹配配置,使其兼容新版的SpringBoot |
之后访问http://ip:port/doc.html,就可以看到对应的管理页面.
抓取网页信息
分析原网站是根据哪个接口获取数据(F12打开控制台,网络中复制CURL)
1
2
3
4
5
6
7
8
9
10curl "https://api.zsxq.com/v2/hashtags/48844541281228/topics?count=20" ^
-H "authority: api.zsxq.com" ^
-H "accept: application/json, text/plain, */*" ^
-H "accept-language: zh-CN,zh;q=0.9" ^
-H "cache-control: no-cache" ^
-H "origin: https://wx.zsxq.com" ^
-H "pragma: no-cache" ^
-H "referer: https://wx.zsxq.com/" ^
--compressedcookie使用自己的
2.用程序调用接口
3.处理(清洗)数据,之后写入数据库
EasyExcel
两种读对象的方式
- 确定表头:建立对象,和表头形成映射关系
- 不确定表头:每一行数据映射为Map<Stirng, Object>
两种读取模式
- 监听器:先创建监听器、在读取文件时绑定监听器.单独抽离处理逻辑,代码清晰易维护,一条条处理,适用于数量大的场景.
- 同步读:无序创建监听器,一次性获取完整数据.方便简单,但数据量大时会有等待异常,也可能内存溢出,
使用Terminal运行SpringBoot项目
- 先利用Maven自带打包工具打包好jar包
- 打开Terminal
1 | java -jar .\user-center-0.0.1-SNAPSHOT.jar --server.port=8080 |
哦豁,出现”0.0.1-SNAPSHOT.jar中没有主清单属性”
解决:在pom文件的maven插件中增加repackage
1 | <plugin> |
前端请求的时候带上一个JSESSIONID,和每个服务器的后台建一个唯一的会话(一一对应),服务器能根据JSESSIONID中定位到请求到对象,进而从请求对象的session中找到请求对象的信息.
Session共享
种session的时候注意范围.
1 | servlet: |
比如两个域名:aaa.yub.com bbb.yub.com
如果要共享cookie,可以种一个更高层的公共域名,比如yub.com
为什么服务器A登录后,请求发送到服务区B,不认识该用户?
用户在A登录,所以session(用户登录信息)存在了A上.
结果请求B,B没有用户信息,无法登录.
如何共享内存?
- Redis(基于内存的K / V数据库)此处选择Redis。用户信息/是否登录的判断及其频繁,Redis基于内存,读写性能很高,简单的数据 qps 5W -10W.
- MySQL
- 文件服务器ceph
Session共享实现
github下载Redis
Maven仓库中找到喝spring-starter版本一致的redis依赖版本
Redis管理工具 QuickRedis QuickOfficial - QuickRedis
引入Redis,操作Redis
1
2
3
4
5
6<!-- 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.13</version>
</dependency>
5.引入spring-session和Redis的整合,使得session自动存储到redis中
1 | <!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis --> |
6.修改spring-session存储配置,spinrg-session.store-type
,默认是null表示存储在单台服务器.【可以修改为redis,表示默认存储在redis中】
1 | store-type: redis |
记得测试时,使用不同的端口(debug+.\jar),在观察redis确定验证是否成功.
1 | java -jar .\user-center-0.0.1-SNAPSHOT.jar --server.port=8081 |
主页开发
最简单:直接list列表
模拟1000w个用户,再去查询
批量导入数据
- 用可视化界面:适合一次性导入、数据量可控
- 写程序:for循环,建议分批,不要一把梭哈(可以使用接口来控制)
- 执行SQL语句,适用于小数据量(方法和以下操作相同,选择SQL Inserts)
保证可控性、幂等【注意线上环境和测试库有区别】
导出csv文件
编写一次性任务
定时任务,spring提供的StopWatch类
1 | StopWatch stopWatch = new StopWatch(); |
在Application类中使用@EnableScheduling
开启spring对定时任务的支持之后,创建单次任务
使用@Scheduled注解,其中限制只能出现一个,否则单次限定失败.
1 | //能成功限定单次执行任务(不优雅 |
for循环插入数据库的问题
- 建立和释放数据库连接(批量查询解决)
- for循环是绝对线性的,一条卡住下一条就得等待
并发要注意执行的先后顺序无所谓,不要用到非并发类的集合(List x Collection.synchronizedList(new ArrayList<>()) √)
1 |
|
自定义并发线程池测试(由于并发测试和自身机器的CPU核数有关,自定义约束)
1 | private ExecutorService executorService = new ThreadPoolExecutor( |
- CPU密集型:分配的核心线程数 = CPU - 1(依赖于CPU的计算能力,需要大量的数学运算或逻辑处理,几乎始终占用 CPU 资源的任务)
- IO密集型:分配的核心线程数可以大于CPU核数(依赖于输入/输出操作(如网络通信、磁盘读写、数据库查询等)的任务)
批量查询由Mybatis-Plus框架封装好,saveBatch
方法实现.
1 |
|
问题
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4f93bf0a]
解决
- 添加事务注释
数据库慢?
预先把数据查出来,放到一个更快读取的地方,不再查询数据库.
预加载缓存,定时更新缓存.【找一个用户少的时间更新】(定时任务)
多个机器都要执行任务吗?
分布式锁,控制只有一台机器去执行定时任务,其他机器不再重复执行了.
应用场景:限制只有一部分用户访问(限制数量),例如抢票.
数据查询慢怎么办?
用缓存:提前把数据去出来保存好(通常保存到读写更快的介质,比如内存),就可以更快的读写.
缓存的实现
Redis(分布式)
memcached(分布式)
Etcd(云原生架构的一个分布式存储,存储配置,扩容能力很强)
ehcache(早期纯净单机)
本地缓存(Java内存Map)
caffeine(Java内存缓存,高性能)
Google Guava
什么是单机缓存
- 单机缓存是将数据存储在本地内存中的缓存机制,用于提升单个应用服务器的性能
- 用户A访问服务器A(有缓存A),用户B访问服务器B,但是B没有缓存A.
- 数据不一致性,用户多次访问服务器的时候可能拿到的数据不一致(比如查询余额)
Redis
NoSQL 数据库
key-value(键值对)存储系统(区别MySQL,存储的是键值对)
Redis的数据结构
- String字符串类型:name:”yub”
- List列表:names:[“yub”,”dogYub”]
- Set集合:naems[“yub”,”dogyub”](值不能重复)
- Hash哈希:nameAge{ “yub” : 1, “dogYub”:2}(键不能重复)
- Zset集合 :names:{yub - 1, dogYub - 9 } (值多一个分数)
- bloomfilter(布隆过滤器,主要从大量的数据中快速过滤值,比如邮件黑名单拦截)
- geo(计算地理位置)
- hyperloglog(pv/uv)
- pub/sub(发布订阅,类似消息队列)
- BitMap(01010101010101,存储大量可以压缩的信息)
Java中的实现方法
Spring Data Redis(推荐)
Spring Data:通用的数据访问框架,定义了一组增删改查的接口(操作mysql、jpa、redis)
Spring Data Redis就可以看成其对应的实现类
1)引入
1 | <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis --> |
2)配置Redis地址
1 | spring: |
Jedis
独立于Spring操作 Redis 的 Java 客户端(默认线程不安全)
配合Jedis Pool使用
Lettuce
高阶的操作 Redis 的Java客户端
支持异步和连接池(复用,减少开销)
Redisson
分布式操作Redis的Java客户端,像在使用本地的集合一样操作Redis(分布式Redis数据网格)
JetCache
对比
- 使用的是Spring,没有过多的定制化要求,可以用Spring Data Redis,最方便
- 用的不是Spring,且追求简单化没有过高的性能要求,可以用Jedis+Jedis Pool
- 项目不是Spring,且追求高性能、高定制化,可以用Lettuce
- 项目是分布式的,需要用到一些分布式的特性(比如分布式锁、分布式集合),推荐使用redission
问题
设置Redis增、查测试,再QuickRedis中查询不到内容.(乱码引起)
1 | //大多情况都使用String【灵活】 |
利用cmd写入新的yubString再观察(cmd默认存储到db0库)
1 | >redis-cli |
追踪源码发现Redis默认的(反)序列化构造器是JDK原生序列化影响
且查找源码发现只有一个StringRedisTemplate
实现(),那么测试类Integer就无法转换.
1 | public class StringRedisTemplate extends RedisTemplate<String, String> { |
此时只能自定义RedisTemplate,ConnectionFactory + 自己需要的序列化方式
1 |
|
发现成功,对应传进来的也是对象(序列化之故).
查询发现ok
1 | 127.0.0.1:6379> select 1 |
设计缓存Key
目标:不同的用户看到的数据不同
systemId:modleId:func:
yub:user:recommed:userId
redis内存不能无限增加,一定要设置过期时间
缓存预热
问题:第一个用户访问还是很慢怎么办?
- 缓存预热,可以让用户访问很快(解决该问题)且在一定程度上能保护数据库.
- 但增加了开发成本(需要额外的开发、设计),预热的时机和时间如果错了,有可能缓存的数据不对或太老
- 需要占用空间
怎么缓存?
- 定时触发(常用)
- 模拟触发(手动触发)
实现
用定时任务,每天刷新所有用户的推荐
注意
- 缓存预热的意义(系统每日新增不多但是总的用户量多,提前一天缓存数据量也不大且能提高用户的加载速度)
- 缓存的空间不能太大,要预留给其他缓存空间(可能很多项目用同一个Redis)
- 缓存数据的周期(此处每天一次)
定时任务实现
- Spring Scheduler(SpringBoot默认整合)
- Quartz(独立于Spring存在的定时任务框架)
- XXL—Job之类的分布式任务调度平台
第一种方式
主类开启@EnableScheduling
给要执行定时任务的方法添加@Scheduling注解,指定cron表达式或者执行频率(注意Spring中cron只接收六位如
0 20 15 * * *
,和Linux有所不同)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
public class PreCacheJob {
private UserService userService;
private RedisTemplate<String, Object> redisTemplate;
//重点用户
private List<Long> mainUserList = Arrays.asList(1L);
//每天执行预热推荐用户
public void doCacheRecommendUser() {
for (Long userId : mainUserList) {
QueryWrapper queryWrapper = new QueryWrapper<>();
Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
String redisKey = String.format("yub:user:recommed:userId:%s", userId);
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
try {
valueOperations.set(redisKey, userPage,30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("redis set ket error",e);
}
}
}}
控制定时任务的执行
- 避免资源浪费,1w台服务器同时“打鸣”
- 脏数据,比如重复插入
要控制定时任务在同一时间只有1个服务器能运行定时任务.
怎么做?
分离定时任务和主程序,只在1个服务器能运行定时任务,成本太高.
写死配置,每个服务器都能执行定时任务,但是只有ip符合配置的服务器才真实执行业务逻辑(写死),其他的直接返回,成本最低.但是我们的IP可能是不固定的.
动态配置,配置方便更新(代码无需重启).只有ip符合配置的服务器才真实执行业务逻辑.
数据库
Redis
配置中心(Nacos、Apollo、Spring Cloud Config)
Q:服务器多了,IP不可控还是很麻烦要人工修改
分布式锁,只有抢到锁的服务器才能执行业务逻辑.缺点:增加成本;优点:不用手动配置,多少个服务器都一视同仁.
单机就会存在单点故障.
锁
在有限资源的情况下,控制同一时间(段)只有某些线程(用户/服务器)能访问到资源.
Java实现锁:synchronized关键字、并发包的类
分布式锁
为啥需要分布式锁?
- 在有限资源的情况下,控制同一时间(段)只有某些线程(用户/服务器)能访问到资源.
- 单个锁只对单个JVM有效.
分布式锁实现的关键
抢锁机制
怎么保证同一时间只有1个服务器能抢到锁?
核心思想
先来的人吧数据改成自己的标识(服务器ip),后来的人发现标识已存在,抢锁失败,继续等待.等先来的人执行方法结束,清空标识之后其他的人继续抢锁.
MySQL数据库:select for update
行级锁,保证同一时间段有一个线程对数据库进行查询和更改,这个期间其他线程无法插足.(最简单)也可以用乐观锁实现.
Redis存标识:内存数据库,读写速度快.支持setnx、lua脚本,比较方便实现分布锁.
setnx:set if not exists 如果不存在,则设置;只有设置成功次啊会返ture,否则返回false.
注意事项
用完锁要释放
del lock
锁一定要加过期时间
set lock uum ex 10 nx
如果方法执行过长,锁提前过期?
问题:
- 连锁效应:释放掉别人的锁.(还是会存在多个方法同时执行的情况,执行完的时间>加锁时间)比如A执行40s,加锁30s,B执行30s,加锁30s;A没执行完,数据库标识已经更改为B的了.b
- 还是会存在多个方法同时执行的情况?
- 解决连锁效应:在释放锁之前检查一下当前的锁是不是自己标识的,不是则忽略.
- 解决锁未执行完:续期
set lock uum ex 10 nx
1
2
3
4
5
6boolean end = false;
new Thread (() -> {
if(!end) {
续期
})
end = true;释放锁的时候,有可能先判断出是自己的锁,结果锁刚好过期了,最后还是释放了别人的锁
1
2
3
4
5//原子操作
if(get lock == A) {
// set lock B 不允许
del lock
}Redis+lua脚本实现
Redis如果是集群(而不是只有一个Redis),如果分布式锁的数据不同步怎么办?
Zookeeper实现
Redisson实现分布式锁
Java客户端,数据网格,实现了很多Java里支持的接口和数据结构.
是一俄格Java操作Redis的客户端,提供了大量的分布式数据集来简化对Redis的操作和使用,可以让开发者像使用本地集合一样使用Redis,完全感知不到Redis的存在.
2种引入方式
- spring boot starter引入(不推荐)
- 直接引入:Getting Started -
Redis
son Reference Guide
实例代码
1 | //list 数据存储在 JVM 中 |
定时任务+锁
- waitTime设置为0,只抢一次,抢不到就放弃
- 注意释放锁要写在finally中
面试官:说一下红锁RedLock的实现原理? - Java中文社群 - SegmentFault 思否
看门狗机制
redisson中提供的续期机制
开一个监听线程,如果方法还没执行完,就帮你重置redis锁的过期时间.
原理
- 监听当前线程,默认过期时间是30s,每10s续期一次(补到30s)
- 如果线程挂掉(debug模式也会被当成服务器宕机),则不会续期
- Redisson 分布式锁的watch dog自动续期机制_redisson续期-CSDN博客
实现代码
1 | void testWatchDog() { |
组队功能
需求分析
用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 P0
队长、剩余的人数
聊天?
公开 or private or 加密
信息流中不展示已过期的队伍
展示队伍列表,根据标签或名称搜索队伍 P0
修改队伍信息 P0~P1
用户创建队伍上限5个
用户可以加入队伍(其他人、未满、未过期),允许加入多个队伍,但是有上限 P0
是否需要队长同意?筛选审批?转让队伍?
用户可以退出队伍(转让队伍给第二时间来的成员)P1
队长可以解散队伍 P0
分享队伍,邀请队员 P1
队伍人满之后发送消息通知 P1
系统(接口)设计
1.创建队伍
用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 P0
队长、剩余的人数
聊天?
公开 or private or 加密
信息流中不展示已过期的队伍
- 请求参数是否为空?
- 是否登录,未登录不允许创建
- 校验信息
- 队伍人数 > 1 且 <= 20
- 队伍名称 <= 20
- 队伍描述 <= 512
- status 是否公开(int)不传默认为0(公开)
- status 为加密,必须有密码,密码 <= 32
- 超时时间 > 当前时间
- 校验用户最多创建5个队伍
- 插入队伍信息到队伍表
- 插入用户 = > 队伍关系用户表
- 关联查询已加入队伍的用户信息
2.查询队伍列表
展示队伍列表,根据名称、最大人数等搜索队伍,信息流中不展示已过期的队伍信息
- 从请求参数中去除队伍名称,如果存在则作为查询条件
- 不展示已经过期的队伍(根据时间筛选)
- 可以通过某个关键词同时对名称和描述查询
- 只有管理员才能查看加密的房间
- 关联查询已加入的队伍的用户信息(可能会很耗费性能,建议自己写SQL实现)
实现方式
自己写SQL
1
2
3
4
5
6
7
8
91. 自己写SQL
// 查询队伍和创建人的信息
select * from team t left join user u on t.userId = u.id
// 查询队伍和已加入队伍成员的信息
select * from team t join user_team ut on t.id = user_teamId
select *
from team t
left join user_team ut on t.id = ut.teamId;
left join user u om ut.userId = u.id;
3. 修改用户信息
- 判断请求参数是否为空
- 查询队伍是否存在
- 只有管理员或者用户创建者可以修改
- 如果用户传入的新值和老值一致,就不用update
- 如果队伍改成加密,必须要有密码
- 更新成功
4. 用户可以加入队伍
其他人、未满、未过期,允许加入多个队伍,但是有上限 P0
- 用户最多加入5个队伍
- 队伍必须存在,只能加入未满、未过期
- 不能重复加入已加入的队伍(幂等性)
- 禁止加入私有的队伍
- 如果加入队伍是加密的,密码必须匹配
- 新增队伍 - 用户关联信息
注意:并发可能出现问题,一定加上事务注解防止数据不一致
5. 用户可以退出队伍
如果队长退出,权限转移给第二早的用户
请求参数:用户id
- 校验参数
- 校验队伍是否存在
- 校验用户是否加入队伍
- 如果队伍
- 只剩1人,队伍解散
- 还有其他人
- 队长退出,权限转移给第二早的用户(取id最小的数据)
- 非队长直接退出
6. 队长可以解散队伍
请求参数:队伍id
业务流程:
- 校验请求参数
- 校验队伍是否存在
- 校验你是不是队长
- 移除所有加入队伍的关联信息
- 删除队伍
7. 分享队伍
业务流程:
- 生成分享链接/二维码
- 用户访问链接,可以点击加入
数据库表设计
队伍表 team
字段:
- id 主键 bigint(最简单、连续,放在url上比较简短,但缺点是怕爬虫)
- name 队伍名称
- description 描述
- maxNum 最大人数
- expireTime 超时时间
- userId 创建人 id
- status 0 - 公开,1 - 私有,2 - 加密
- password 密码
- creatTime 创建时间
- updateTime 更新时间
- isDelete 是否删除
两个关系:
- 用户加了那些队伍?
- 队伍有哪些用户?
方式:
- 建立用户-队伍关系表 teamld userld(便于修改,查询性能高一点,可以选择这个,不用全表遍历)
- 用户表补充已加入的队伍字段,队伍表补充已加入的用户字段(便于查询,不用写多对多的代码,可以直接根据队伍查用户、根据用户查队伍)
1 | create table team |
用户-队伍表 user_team表
字段:
- id 主键
- userId 用户 id
- teamId 队伍 id
- joinTime 加入时间
- creatTime 创建时间
- updateTime 更新时间
- isDelete 是否删除
1 | create table user_team |
为什么需要请求参数包装类?
- 请求参数名称/类型和实体类不一样
- 有一些参数用不到,如果要自动生成接口文档,会增加理解成本
- 对个体类映射到同一个对象
为什么需要包装类
- 可能有些字段需要隐藏,不能返回给前端
- 或者有些字段是不关心的
实现
库表设计
增删改查
业务逻辑开发(P0优先)
随机匹配
高效匹配志同道合的朋友
1. 怎么匹配
- 匹配多个,按相似度从高到低匹配
- 根据标签匹配
- 根据user_team 匹配加入相同队伍的用户
本质:找到相似标签的用户
举例:
用户A:[Java, 大一,女]
用户B:[Java, 大二,女]
用户C:[Python, 大二,女]
- 找到有共同标签最多的用户(TopN)
- 共同标签越多,分数越高,越在前面
- 如果没有匹配用户,随机推荐几个(降级方案)
两种算法
编辑距离算法: 详解编辑距离算法-Levenshtein Distance-CSDN博客
最小编辑距离:字符串 1 通过最少多少次增删改字符的操作可以编程字符串2
余弦相似度算法:用户画像标签数据开发之标签相似度计算-CSDN博客
2. 怎么对所有用户进行匹配,取TOP
直接取出所有用户,一次和当前用户计算分数,取TopN
优化:
- 切记不要在数据量大的时候循环输出日志(取消日志20s)
- Map存储所有分数信息,占用内存
解决:维护一个固定长度的有序集合(sortedSet),只保留分数最高的几个用户【TOP 5】,时间换空间 - 注意需要除去自己
- 尽量只查需要的用户
- 过滤掉标签为空的用户
- 根据部分标签取用户(前提是能区分出来哪个标签比较重要)
- 只查需要的数据(比如id和tags 只用了7s)
- 提前查?
- 提前缓存所有用户(不适合标签频繁更新的数据)
- 提前运算出结果,缓存(针对重点用户,提前缓存)
大数据推荐流程:检索 => 召回 => 粗排 => 精排 => 重排序等等
检索:尽可能多地查符合要求得数据
召回:查询可能要用到的数据(不做运算)
粗排:粗略排序,简单地运算
精排:精细排序,确定固定排位
优化
使用Redis GEO实现距离编辑和搜索附近用户功能.
- 数据库表增加字段
- 新增经纬度字段 decimal
- 新增纬度字段 decimal
- RedisZset实现,
- UserVO类增加字段
- 添加distance字段(向前端返回用户间位置信息) double类型
- 编写测试类
- 使用Spring Data Redis提供的StringRedisTemplate(Key/Value都是Stirng类型)
Tips
controller校验是否为空,service层校验是否合法.
内外层均进行鉴权,对性能影响微乎其微.
数据类型为非包装类(如long Id),可以不用判空,默认为0.
在Application类中使用
@EnableScheduling
开启spring对定时任务的支持.分页使用Page(MybatisPlus提供)current自动计算不需利用
(pageNum - 1) * pageSize
1
2
3
4
5
6
public BaseResponse<Page<User>> recommendUsers(long pageSize, long pageNum, HttpServletRequest request) {
QueryWrapper queryWrapper = new QueryWrapper<>();
Page<User> userList = userService.page(new Page<>(pageNum, pageSize), queryWrapper);
return ResultsUtils.success(userList);
}MybatisPlus提供批量插入,
saveBatch
方法实现.SpringMVC负责post、get等等请求处理
引入一个库时,先写测试类
ctrl + alt + t可以抛异常包裹
写缓存,使用
@Slf4j
中的log.error()
,即使失败也可以讲数据库查到的内容返回给前端 故不直接使用全局异常处理器(返回错误给用户)分析有缺点时要从整个项目由0到1的链路上分析(比如设计、开发)
白名单内容不要写死,动态(比如给数据库中做标记)
synchronized只对当前的线程有效(只能控制单个JVM【服务器】)
设置分布式锁时统一使用setnnx(使用set会进行更改)
响应值有关安全性
包装类要使用
Optional.ofNullable().orElse()
设置默认值1
int maxNum = Optional.ofNullable(team.getMaxNum()).orElse(0);
开启事务在类上方引入(有增删改事务最好加上)
1
开发者工具可以帮助我们获取时间,配合Knif4j测试
1
2>console.log(JSON.stringify(new Date()))
"2025-01-31T05:47:02.462Z"dto:业务封装类;vo:返回给前端的封装类
关联多个表推荐自己写SQL
使用equals()时注意翻转,确保不为空对象在前,避免不必要的NPE
对数据库的操作,建议把用户传参
ctrl + alt + v 提取变量替换
复用listTeam方法,只新增查询条件,不做修改(开闭原则)
涉及查数据的地方限制页数,防止数据库泄露、
加锁
synchronized
加锁接收的是对象使用String.valueOf(userId).intern()
锁住常量(同一个对象intern()
实现)ErrorCode
枚举类中不使用final修饰description
取代反射来动态设置description