@[toc]
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04

项目地址:
- Github:github.com/China-Rainb...
- Gitee:gitee.com/Rainbow--Se...
补充:问题:CORS ,跨域问题
我这边的解决方法是:

typescript
myAxios.defaults.withCredentials = true; // 配置为true,表示前端向后端发送请求的时候,需要携带上凭证cookie
整体的:
typescript
import axios from "axios";
// axios.defaults.withCredentials = true; // 允许携带凭证
// const isDev = process.env.NODE_ENV === 'development';
// 创建实例时配置默认值
const myAxios = axios.create({
LookupAddress: undefined, LookupAddressEntry: undefined,
baseURL: 'http://localhost:8080/api'
});
// const myAxios: AxiosInstance = axios.create({
// baseURL: isDev ? 'http://localhost:8080/api' : '线上地址',
// });
myAxios.defaults.withCredentials = true; // 配置为true,表示前端向后端发送请求的时候,需要携带上凭证cookie
// 创建实例后修改默认值
// 添加请求拦截器
myAxios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
console.log('我要发请求了')
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
myAxios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
console.log('我收到你的响应了',response)
return response.data;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
// Add a request interceptor
// myAxios.interceptors.request.use(function (config) {
// console.log('我要发请求啦', config)
// // Do something before request is sent
// return config;
// }, function (error) {
// // Do something with request error
// return Promise.reject(error);
// });
//
//
// // Add a response interceptor
// myAxios.interceptors.response.use(function (response) {
// console.log('我收到你的响应啦', response)
// // 未登录则跳转到登录页
// if (response?.data?.code === 40100) {
// const redirectUrl = window.location.href;
// window.location.href = `/user/login?redirect=${redirectUrl}`;
// }
// // Do something with response data
// return response.data;
// }, function (error) {
// // Do something with response error
// return Promise.reject(error);
// });
export default myAxios;
后端配置:

在 Spring Boot 中,可以通过在配置类中添加 <font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@CrossOrigin</font>
注解或实现 <font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">WebMvcConfigurer</font>
接口并重写 <font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">addCorsMappings</font>
方法来允许特定来源的跨域请求:
java
package com.rainbowsea.yupao.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置
*
*/
@Configuration
public class WebMvcConfg implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
//当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
.allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http" +
"://127.0.0.1:8083","http://127.0.0.1:8080","http://127.0.0.1:5173")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*")
//跨域允许时间
.maxAge(3600);
}
}
相关博客链接:
- blog.csdn.net/yuanlong121... 参考该 blog 解决的
- blog.csdn.net/xhmico/arti... 这篇也不错。
缓存预热
缓存预热:问题:第一个用户访问还是很慢(加入第一个老板),比如:双十一,第一次就是很多用户呢,也能一定程度上保护数据库。
缓存预热的优点:
- 解决上面的问题,可以让用户始终访问很快。
缺点:
- 增加了开发成本,访问人数不多。(你要额外的开发,设计)
- 预热的时机和时间如果错了,有可能你缓存的数据不对或者数据太旧了
- 需要占用空间。拿空间换时间。
分析优缺点的时候,要打开思路,从整个项目从 0 到 1 的链路上分析。
怎么缓存预热,预热操作
两种方式:
- 定时任务预热。
- 模拟触发(手动触发)
这里我们采用定时任务预热:
定时任务实现:
- Spring Scheduler (Spring Boot 默认整合了)
- Quartz (独立于 Spring 存在的定时任务框架)
- XXL-Job 之类的分布式任务调度平台(界面+sdk)
用定时任务,每天刷新所有用户的推荐列表
注意点:
- 缓存预热的意义(新增少,总用户多)
- 缓存的空间不能太大,要预留给其他缓存空间
- 缓存数据的周期(此处每天一次)
采用第一种方式:步骤:
- 主类开启:
@EnableScheduling
- 给要定时执行的方法添加上
@Scheduling
注解,指定 cron 表达式或者执行频率。
不需要去背 cron 表达式,用现成的工具即可:

typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 导出配置对象,使用ES模块语法
export default defineConfig({
plugins: [vue()], // 启用Vue插件
server: { // 注意:在Vite的新版本中,配置项`devServer`已更名为`server`
proxy: {
'/api': {
target: 'http://localhost:8080/api', // 目标服务器地址
changeOrigin: true, // 是否改变源
// 如果需要路径重写,可以取消以下行的注释
// pathRewrite: { 1'^/api': '' }
}
}
}
});

yaml
server:
port: 8080
servlet:
context-path: /api
session:
cookie:
domain: localhost
secure: true
same-site: none # 上述方法不行,就配置上
spring:
# session 失效时间
session:
timeout: 86400
store-type: redis
# Redis 配置
redis:
port: 6379
host: localhost
database: 1
控制定时任务的执行
要控制定时任务在同一时间只有 1 个 服务器能执行。
为什么呢?
- 浪费资源,想象 1W 台服务器能执行。
- 脏数据,比如重复插入。
怎么做?几种方案:
- 分离定时任务程序和主程序,只在 1 个服务器运行定时任务,成本太大。
- 写死配置,每个服务器都执行定时任务,但是只有 IP 符合配置的服务器才真实执行业务逻辑,其他的直接返回。成本最低;但是我们的 IP 可能不是固定的,把 IP 写死的方式太死了。
- 动态配置:配置是可以轻松的,很方便地更新的(代码无需重启),但是只有 IP 符合配置的服务器才真实执行业务逻辑。可以使用
- 数据库
- Redis
- 配置中心(Nacos,Apollo,Spring Cloud Config)
**问题:**服务器多了,IP 不可控还是很麻烦,还是要人工修改。
- 分布式锁,只有抢到锁的服务器才能执行业务逻辑,坏处:增加成本;好处:就是不用手动配置,多少个服务器都一样。
**注意:**只要是单机,就会存在单点故障。
锁
有限资源的情况下,控制同一时间(段)只有某些线程(用户/服务器)能访问到资源。
Java 实现锁:sychronized 关键字,并发包的类
但存在问题:只对单个 JVM 有效。
分布式锁实现的关键
枪锁机制:
怎么保证同一时间只有 1 个服务器能抢到锁?
**核心思想:**就是:先来的人先把数据改成自己的标识(服务器 IP),后来的人发现标识已存在,就抢锁失败,继续等待。
等先来的人执行方法结束,把标识清空,其他的人继续抢锁。
MySQL 数据库:select for update 行级锁(最简单),或者乐观锁
Redis 实现:内存数据库,读写速度快,支持 setnx,lua 脚本,比较方便我们实现分布式锁。
setnx:set if not exists 如果不存在,则设置;只有设置成功才会返回 true,否则返回 false。
分布式锁的注意事项:
- 用完锁一定要释放锁
- 一定要设置锁的过期时间,防止对应占用锁的服务器宕机了,无法释放锁。导致死锁。
- 如果方法执行过长的话,锁被提前过期,释放了,怎么办。------续期
java
boolean end = false; // 方法没有结束的标志
new Thread(() -> {
if (!end)}{ // 表示执行还没结束,续期
续期
})
end = true; // 到这里,方法执行结束了
问题:
- 连锁效应:释放掉别人的锁
- 这样还是会存在多个方法同时执行的情况。
- 释放锁的时候(判断出是自己的锁了,准备执行释放的锁的过程中时,很巧的时,锁过期了,然后,这个时候就有一个新的东东插入进来了,这样锁就删除了别人的锁了)。
解决方案:Redis + lua 脚本保证操作原子性
java
// 原子操作
if(get lock == A) {
// set lock B
del lock
}
**步骤:**缓存预热数据:
- 在项目启动类上,添加上
@EnableScheduling
注解。表示启动定时任务。



- 编写具体要执行的定时任务,程序,这里我们名为
PreCacheJob
注意:这里我们使用上 注解,使用 cron 表达式表示一个定时时间上的设置
这里往下看,我们使用 Redisson 进行一个实现分布式锁的操作。
相关 Redissson 配置使用如下:
Redisson-实现分布式锁
Redisson 是一个 Java 操作的 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis,完全感知不到 Redis 的存在,提供了大量的 API 。
2 种引入方式:
- Spring boot starter 引入(不推荐,因为版本迭代太快了,容易发生版本冲突):github.com/redisson/re...
- 直接引入(推荐):github.com/redisson/re...
xml
<!--https://github.com/redisson/redisson#quick-start -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.5</version>
</dependency>
使用 Ression
- 配置 Ression 配置类:

java
package com.rainbowsea.yupao.config;
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redisson 配置
*
*/
@Configuration
@ConfigurationProperties(prefix = "spring.redis") // 同时这个获取到 application.yaml 当中前缀的配置属性
@Data
public class RedissonConfig {
private String host;
private String port;
@Bean
public RedissonClient redissonClient() {
// 1. 创建配置
Config config = new Config();
String redisAddress = String.format("redis://%s:%s", host, port);
config.useSingleServer().setAddress(redisAddress).setDatabase(3);
// 2. 创建实例
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
- 操作 Redis : 测试是否,能操作 Redis ,通过 Ression
java
// list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));
list.remove(0);
// 数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list"); // redis操作的 key 的定义
rList.add("yupi");
System.out.println("rlist:" + rList.get(0));
rList.remove(0);

java
package com.rainbowsea.yupao.service;
import org.junit.jupiter.api.Test;
import org.redisson.api.RList;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
public class RedissonTest {
@Resource
private RedissonClient redissonClient;
@Test
void test() {
// list 数据存在代 本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
list.get(0);
System.out.println("list: " + list.get(0));
list.remove(0);
// 数据存入 redis 的内存中
RList<Object> rList = redissonClient.getList("test-list"); // 表示 redis 当中的 key
rList.add("yupi");
System.out.println("rlist:" + rList.get(0));
}
}

其中 Redisson 操作 Redis 当中的 set,map 都是一样的道理,就不多赘述了。
分布式锁保证定时任务不重复执行:
实现代码如下:
java
void testWatchDog() {
RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
try {
// 只有一个线程能获取到锁
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
// todo 实际要执行的方法
doSomeThings();
System.out.println("getLock: " + Thread.currentThread().getId());
}
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) { // 判断该锁是不是当前线程创建的锁
System.out.println("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
}
注意:
- waitTime 设置为 0,其他线程不会去等待这个锁的释放,就是抢到了就用,没抢到就不用了,只抢一次,抢不到就放弃。
java
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
- 注意释放锁要放到 finally 中,不然,发生了异常就被中断,无法释放锁了。
缓存预热,定时执行具体的任务的具体代码:

java
@Scheduled(cron = "0 5 21 25 6 ?")

java
package com.rainbowsea.yupao.job;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 缓存预热任务
*
*/
@Component
@Slf4j
public class PreCacheJob {
@Resource
private UserService userService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedissonClient redissonClient;
// 重点用户
private List<Long> mainUserList = Arrays.asList(1L);
// 每天执行,预热推荐用户,每个月的 31号,0:00执行
@Scheduled(cron = "0 31 0 * * *")
public void doCacheRecommendUser() {
RLock lock = redissonClient.getLock("yupao:precachejob: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();
}
}
}
}
运行测试:


java
\yupao\yupao-backend\target>java -jar .\yupao-backend-0.0.1-SNAPSHOT.jar --server.port=9090

Redisson 看门狗机制:
Redisson 中提供的续期机制。
开一个监听线程,如果方法还没执行完,就帮你重置 Redis 锁的过期时间。
原理:
- 监听当前线程, 默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
- 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期。
为什么 Redisson 续期时间是 30 秒
因为方式 Redis 宕机了,就成了,占着茅坑不拉屎。


(Redis 如果是集群(而不是只有一个 Redis),如果分布式锁的数据不同步怎么办 )如果 Reids 分布式锁导致数据不一致的问题------> Redis 红锁。
组队
用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间PO。
队长、剩余的人数
聊天?
公开或private或加密
用户创建队伍最多5个
展示队伍列表,根据名称搜索队伍PO,信息流中不展示已过期的队伍
修改队伍信息PO~P1
用户可以加入队伍(其他人、未满、未过期),允许加入多个队伍,但是要有个上限PO
是否需要队长同意?筛选审批?
用户可以退出队伍(如果队长退出,权限转移给第二早加入的用户------先来后到)P1
队长可以解散队伍PO
分享队伍=>邀请其他用户加入队伍P1
业务流程:
- 生成分享链接 (分享二维码)
- 用户访问链接,可以点击加入
数据库表设计
队伍表 team
字段:
- id 主键bigint (最简单、连续,放 url 上比较简短,但缺点是爬虫)
- name 队伍名称
- description 描述
- maxNum最大人数
- expireTime 过期时间)userld 创建人 id
- status 0-公开,1-私有,2-加密
- password 密码)
- createTime 创建时间
- updateTime 更新时间
- isDelete是否删除
sql
create table team
(
id bigint auto_increment comment 'id'
primary key,
name varchar(256) not null comment '队伍名称',
description varchar(1024) null comment '描述',
maxNum int default 1 not null comment '最大人数',
expireTime datetime null comment '过期时间',
userId bigint comment '用户id',
status int default 0 not null comment '0 - 公开,1 - 私有,2 - 加密',
password varchar(512) null comment '密码',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
isDelete tinyint default 0 not null comment '是否删除'
)
comment '队伍';
用户-队伍表 user_team
两个关系:
- 用户加入了哪些队伍?
- 队伍有哪些用户?
两种实现方式:
- 建立用户-队伍关系表 teamid userid(便于修改,查询性能高一点,可以选择这个,不用全表遍历)
- 用户表补充已加入的队伍字段,队伍表补充已加入的用户字段(便于查询,不用写多对多的代码,可以直接根据队伍查用户,根据用户查队伍)。
字段:
- id 主键
- userld 用户 id
- teamld 队伍 id
- joinTime 加入时间
- createTime 创建时间
- updateTime 更新时间
- isDelete是否删除
sql
create table user_team
(
id bigint auto_increment comment 'id'
primary key,
userId bigint comment '用户id',
teamId bigint comment '队伍id',
joinTime datetime null comment '加入时间',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
isDelete tinyint default 0 not null comment '是否删除'
)
comment '用户队伍关系';
为什么需要请求参数包装类?
- 请求参数名称 / 类型和实体类不一样。
- 有些参数用不到,如果要自动生成接口文档,会增加理解成本。
- 对个实体类映射到同一个对象。
为什么需要包装类?
- 可能有些字段需要隐藏,不能返回给前端。
- 或者有些字段某些方法是不关心的。
前端不同页面传递数据
- url querystring(xxx?id=1)比较适用于页面跳转
- url (/team/:id, xxx/1)
- hash (/team#1)
- localStorage
- context(全局变量,同页面或整个项目要访问公共变量)
最后:
"在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。"