伙伴匹配系统
约 1630 字大约 5 分钟
2026-04-04
移动端网站
项目地址
编程导航:https://www.code-nav.cn/course/1790950013153095682
github代码地址:https://github.com/731016/friend-mach
主要技术栈
redi缓存,分布式锁
easy excel数据导入
spring scheduler定时任务
前端
vue3 + vantui + vite + 状态管理
后端
java + mysql + 缓存 + springboot + mybatis-plus + swagger + knife4j + gson
分布式session
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.13</version>
</dependency>
<!-- 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.1</version>
</dependency>spring:
session:
timeout: 3600
store-type: redis问题
yarn create vite 文件名、目录名或卷标语法不正确
yarn 的安装路径和缓存路径
查看各种路径命令
查看 yarn 全局bin位置
yarn global bin
查看 yarn 全局安装位置
yarn global dir
查看 yarn 全局cache位置
yarn cache dir
修改路径命令
改变 yarn 全局bin位置
yarn config set prefix "D:\software\Yarn\Data"
改变 yarn 全局安装位置
yarn config set global-folder "D:\software\Yarn\Data\global"
改变 yarn 全局cache位置
yarn config set cache-folder "D:\software\Yarn\Cache"
改变 yarn 全局 link 位置
yarn config set link-folder "D:\software\Yarn\Data\link"swagger配置
@Configuration
@EnableSwagger2WebMvc
@Profile("dev")
public class SwaggerConfiguration {
@Bean
public Docket defaultApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(groupApiInfo())
.groupName("默认接口")
.select()
.apis(RequestHandlerSelectors.basePackage("com.tuaofei.friendmatch.controller"))
//.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo groupApiInfo(){
return new ApiInfoBuilder()
.title("用户中心")
.description("用户中心 RESTful APIs")
.termsOfServiceUrl("https://github.com/731016")
.version("1.0")
.build();
}
}web配置
/**
* 跨域问题
*
* @date 2022/3/31 21:39
*/
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 跨域相关配置, 并让 authorization 可在响应头中出现
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("*")
.allowedOriginPatterns("*")
.allowCredentials(true);
}
// @Override
// public void addInterceptors(InterceptorRegistry registry) {
// List<String> patterns = new ArrayList<>();
// patterns.add("/user/login");
// patterns.add("/user/register");
// patterns.add("/user/logout");
// patterns.add("/swagger-ui.html/**");
// patterns.add("/webjars/**");
// patterns.add("/v2/**");
// patterns.add("/swagger-resources/**");
// patterns.add("/doc.html/**");
// //注册拦截器类,添加黑名单(addPathPatterns("/**")),‘/*’只拦截一个层级,'/**'拦截全部
// // 和白名单(excludePathPatterns("List类型参数")),将不必拦截的路径添加到List列表中
// registry.addInterceptor(null).addPathPatterns("/**").excludePathPatterns(patterns);
// }
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(
"classpath:/static/");
registry.addResourceHandler("/swagger-ui.html").addResourceLocations(
"classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
super.addResourceHandlers(registry);
}
}异步任务
/**
* 并发批量插入用户
*/
@Test
public void doConcurrencyInsertUsers() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 分十组
int batchSize = 5000;
int j = 0;
List<CompletableFuture<Void>> futureList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
List<User> userList = new ArrayList<>();
while (true) {
j++;
User user = new User();
user.setUserName("假鱼皮");
user.setUserAccount("fakeyupi");
user.setAvatarUrl("https://636f-codenav-8grj8px727565176-1256524210.tcb.qcloud.la/img/logo.png");
user.setGender(0);
user.setUserPassword("12345678");
user.setPhone("123");
user.setEmail("123@qq.com");
user.setTags("[]");
user.setUserStatus(0);
user.setUserRole(0);
user.setPlanetCode("11111111");
userList.add(user);
if (j % batchSize == 0) {
break;
}
}
// 异步执行
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("threadName: " + Thread.currentThread().getName());
userService.saveBatch(userList, batchSize);
}, executorService);
futureList.add(future);
}
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{})).join();
// 20 秒 10 万条
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}使用mybatis分页Page
/**
* MyBatisPlus 配置
*/
@Configuration
@MapperScan("com.tuaofei.friendmatch.mapper")
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}spring data redis
如果直接使用redisTemplate因为默认使用的jdk的序列化方式,可能会出现乱码
使用StringRedisTemplate,key,value都是string
自定义redis配置
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
return redisTemplate;
}
}定时任务
启动类加上 @EnableScheduling
缓存预热:新增少,总用户多
@Scheduled(cron = "0 31 0 * * *")
public void doCacheRecommendUser() {
for (Long userId : mainUserList) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
String redisKey = String.format("yupao:user:recommend:%s", userId);
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 写缓存
try {
valueOperations.set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("redis set key error", e);
}
}
}分布式锁
关键
抢锁机制
怎么保证同一时间只有 1 个服务器能抢到锁?
核心思想: 就是:先来的人先把数据改成自己的标识(服务器 ip),后来的人发现标识已存在,就抢锁失败,继续等待。
等先来的人执行方法结束,把标识清空,其他的人继续抢锁。
MySQL 数据库:select for update 行级锁(最简单),或者用乐观锁。
Redis 实现:内存数据库,读写速度快。支持 setnx、lua 脚本,比较方便我们实现分布式锁。
setnx:set if not exists 如果不存在,则设置;只有设置成功才会返回 true,否则返回 false。
注意:
1)用完锁要释放(腾地方)
2)锁一定要加过期时间
3)如果方法执行时间过长,锁提前过期了?
连锁效应:释放掉别人的锁
这样还是会存在多个方法同时执行的情况
续期:判断方法执行未完成延长过期时间
4)释放锁的时候,有可能先判断出是自己的锁,但这时锁过期了,最后还是释放了别人的锁
reids + lua脚本 保证操作原子性使用redisson
用法和list,map相同
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
private String port;
private String host;
@Bean
public RedissonClient redissonClient() {
// 1. Create config object
Config config = new Config();
config.useClusterServers()
.addNodeAddress(String.format("redis://%s:%s", host, port));
return Redisson.create(config);
}
}
示例方法
public void doCacheRecommendUser() {
RLock lock = redissonClient.getLock("friendMatch:job:doCache:lock");
try {
// 只有一个线程能获取到锁
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
System.out.println("getLock: " + Thread.currentThread().getId());
for (Long userId : mainUserList) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
String redisKey = String.format("yupao:user:recommend:%s", userId);
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 写缓存
try {
valueOperations.set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("redis set key error", e);
}
}
}
} catch (InterruptedException e) {
log.error("doCacheRecommendUser error", e);
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
}Redisson 看门狗机制
开一个监听线程,如果方法还没执行完,就帮你重置 redis 锁的过期时间。
监听当前线程,默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期
Redisson 分布式锁的watch dog自动续期机制_redisson 续期_zhifeng687的博客-CSDN博客
Redisson--红锁(Redlock)--使用/原理_IT利刃出鞘的博客-CSDN博客
事务
注解式事务
有很多种失效情况,不建议使用
https://blog.csdn.net/mccand1234/article/details/124571619
@Transactional(rollbackFor = Exception.class)编辑距离算法
https://blog.csdn.net/DBC_121/article/details/104198838
最小编辑距离:字符串 1 通过最少多少次增删改字符的操作可以变成字符串 2
余弦相似度算法
https://blog.csdn.net/m0_55613022/article/details/125683937(如果需要带权重计算,比如学什么方向最重要,性别相对次要)
分库分表
mycat、sharding sphere 框架
上线
前端:Vercel(免费)
https://vercel.com/
后端:微信云托管(部署容器的平台,付费)
https://cloud.weixin.qq.com/cloudrun/service
贡献者
更新日志
fb8bc-更新为vuepress于