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

@[toc]

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

项目地址:

补充:问题: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);
    }
}

相关博客链接:

缓存预热

缓存预热:问题:第一个用户访问还是很慢(加入第一个老板),比如:双十一,第一次就是很多用户呢,也能一定程度上保护数据库。

缓存预热的优点:

  1. 解决上面的问题,可以让用户始终访问很快。

缺点:

  1. 增加了开发成本,访问人数不多。(你要额外的开发,设计)
  2. 预热的时机和时间如果错了,有可能你缓存的数据不对或者数据太旧了
  3. 需要占用空间。拿空间换时间。

分析优缺点的时候,要打开思路,从整个项目从 0 到 1 的链路上分析。

怎么缓存预热,预热操作

两种方式:

  1. 定时任务预热。
  2. 模拟触发(手动触发)

这里我们采用定时任务预热

定时任务实现:

  1. Spring Scheduler (Spring Boot 默认整合了)
  2. Quartz (独立于 Spring 存在的定时任务框架)
  3. XXL-Job 之类的分布式任务调度平台(界面+sdk)

用定时任务,每天刷新所有用户的推荐列表

注意点:

  1. 缓存预热的意义(新增少,总用户多)
  2. 缓存的空间不能太大,要预留给其他缓存空间
  3. 缓存数据的周期(此处每天一次)

采用第一种方式:步骤:

  1. 主类开启:@EnableScheduling
  2. 给要定时执行的方法添加上 @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 个 服务器能执行。

为什么呢?

  1. 浪费资源,想象 1W 台服务器能执行。
  2. 脏数据,比如重复插入。

怎么做?几种方案:

  1. 分离定时任务程序和主程序,只在 1 个服务器运行定时任务,成本太大。
  2. 写死配置,每个服务器都执行定时任务,但是只有 IP 符合配置的服务器才真实执行业务逻辑,其他的直接返回。成本最低;但是我们的 IP 可能不是固定的,把 IP 写死的方式太死了。
  3. 动态配置:配置是可以轻松的,很方便地更新的(代码无需重启),但是只有 IP 符合配置的服务器才真实执行业务逻辑。可以使用
    1. 数据库
    2. Redis
    3. 配置中心(Nacos,Apollo,Spring Cloud Config)

**问题:**服务器多了,IP 不可控还是很麻烦,还是要人工修改。

  1. 分布式锁,只有抢到锁的服务器才能执行业务逻辑,坏处:增加成本;好处:就是不用手动配置,多少个服务器都一样。

**注意:**只要是单机,就会存在单点故障。

有限资源的情况下,控制同一时间(段)只有某些线程(用户/服务器)能访问到资源。

Java 实现锁:sychronized 关键字,并发包的类

但存在问题:只对单个 JVM 有效。

分布式锁实现的关键

枪锁机制:

怎么保证同一时间只有 1 个服务器能抢到锁?

**核心思想:**就是:先来的人先把数据改成自己的标识(服务器 IP),后来的人发现标识已存在,就抢锁失败,继续等待。

等先来的人执行方法结束,把标识清空,其他的人继续抢锁。

MySQL 数据库:select for update 行级锁(最简单),或者乐观锁

Redis 实现:内存数据库,读写速度快,支持 setnx,lua 脚本,比较方便我们实现分布式锁。

setnx:set if not exists 如果不存在,则设置;只有设置成功才会返回 true,否则返回 false。

分布式锁的注意事项:

  1. 用完锁一定要释放锁
  2. 一定要设置锁的过期时间,防止对应占用锁的服务器宕机了,无法释放锁。导致死锁。
  3. 如果方法执行过长的话,锁被提前过期,释放了,怎么办。------续期
java 复制代码
boolean end = false;  // 方法没有结束的标志

new Thread(() -> {
    if (!end)}{  // 表示执行还没结束,续期
    续期
})

end = true;  // 到这里,方法执行结束了

问题:

  • 连锁效应:释放掉别人的锁
  • 这样还是会存在多个方法同时执行的情况。
  1. 释放锁的时候(判断出是自己的锁了,准备执行释放的锁的过程中时,很巧的时,锁过期了,然后,这个时候就有一个新的东东插入进来了,这样锁就删除了别人的锁了)。

解决方案:Redis + lua 脚本保证操作原子性

java 复制代码
// 原子操作
if(get lock == A) {
    // set lock B
    del lock
}

**步骤:**缓存预热数据:

  1. 在项目启动类上,添加上 @EnableScheduling 注解。表示启动定时任务。
  1. 编写具体要执行的定时任务,程序,这里我们名为 PreCacheJob

注意:这里我们使用上 注解,使用 cron 表达式表示一个定时时间上的设置

这里往下看,我们使用 Redisson 进行一个实现分布式锁的操作。

相关 Redissson 配置使用如下:

Redisson-实现分布式锁

Redisson 是一个 Java 操作的 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis,完全感知不到 Redis 的存在,提供了大量的 API 。

2 种引入方式:

  1. Spring boot starter 引入(不推荐,因为版本迭代太快了,容易发生版本冲突):github.com/redisson/re...
  2. 直接引入(推荐):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

  1. 配置 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;
    }
}
  1. 操作 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();
        }
    }
}

注意:

  1. waitTime 设置为 0,其他线程不会去等待这个锁的释放,就是抢到了就用,没抢到就不用了,只抢一次,抢不到就放弃。
java 复制代码
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
  1. 注意释放锁要放到 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 锁的过期时间。

原理:

  1. 监听当前线程, 默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
  2. 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期。

为什么 Redisson 续期时间是 30 秒

因为方式 Redis 宕机了,就成了,占着茅坑不拉屎。

(Redis 如果是集群(而不是只有一个 Redis),如果分布式锁的数据不同步怎么办 )如果 Reids 分布式锁导致数据不一致的问题------> Redis 红锁。

组队

用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间PO。

队长、剩余的人数

聊天?

公开或private或加密

用户创建队伍最多5个

展示队伍列表,根据名称搜索队伍PO,信息流中不展示已过期的队伍

修改队伍信息PO~P1

用户可以加入队伍(其他人、未满、未过期),允许加入多个队伍,但是要有个上限PO

是否需要队长同意?筛选审批?

用户可以退出队伍(如果队长退出,权限转移给第二早加入的用户------先来后到)P1

队长可以解散队伍PO

分享队伍=>邀请其他用户加入队伍P1

业务流程:

  1. 生成分享链接 (分享二维码)
  2. 用户访问链接,可以点击加入

数据库表设计

队伍表 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

两个关系:

  1. 用户加入了哪些队伍?
  2. 队伍有哪些用户?

两种实现方式:

  1. 建立用户-队伍关系表 teamid userid(便于修改,查询性能高一点,可以选择这个,不用全表遍历)
  2. 用户表补充已加入的队伍字段,队伍表补充已加入的用户字段(便于查询,不用写多对多的代码,可以直接根据队伍查用户,根据用户查队伍)。

字段:

  • 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 '用户队伍关系';

为什么需要请求参数包装类?

  1. 请求参数名称 / 类型和实体类不一样。
  2. 有些参数用不到,如果要自动生成接口文档,会增加理解成本。
  3. 对个实体类映射到同一个对象。

为什么需要包装类?

  1. 可能有些字段需要隐藏,不能返回给前端。
  2. 或者有些字段某些方法是不关心的。

前端不同页面传递数据

  1. url querystring(xxx?id=1)比较适用于页面跳转
  2. url (/team/:id, xxx/1)
  3. hash (/team#1)
  4. localStorage
  5. context(全局变量,同页面或整个项目要访问公共变量)

最后:

"在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。"

相关推荐
AirMan几秒前
深入解析 Spring Caffeine:揭秘 W-TinyLFU 缓存淘汰策略的高命中率秘密
后端
isyangli_blog23 分钟前
(2-10-1)MyBatis的基础与基本使用
java·开发语言·mybatis
小码编匠26 分钟前
C# Bitmap 类在工控实时图像处理中的高效应用与避坑
后端·c#·.net
一乐小哥28 分钟前
从面试高频到实战落地:单例模式全解析(含 6 种实现 + 避坑指南)
java·设计模式
布朗克16832 分钟前
Spring Boot项目通过RestTemplate调用三方接口详细教程
java·spring boot·后端·resttemplate
最初的↘那颗心1 小时前
Java 泛型类型擦除
java·flink
uhakadotcom2 小时前
使用postgresql时有哪些简单有用的最佳实践
后端·面试·github
IT毕设实战小研2 小时前
基于Spring Boot校园二手交易平台系统设计与实现 二手交易系统 交易平台小程序
java·数据库·vue.js·spring boot·后端·小程序·课程设计
bobz9652 小时前
QT 字体
后端
泉城老铁2 小时前
Spring Boot 中根据 Word 模板导出包含表格、图表等复杂格式的文档
java·后端