homie 匹配系统笔记

前端项目初始化

  1. 使用vite初始化项目,根据你的需求选择相应地选项
1
npm create vite
  1. 安装依赖
1
npm install
  1. 按需整合vant组件库
1
npm i vite-plugin-style-import@1.4.1 -D
  1. 关闭vite语法检查(前提使用build启动项目),在package.json文件中,将原始的build改为build”: “vite build”
  2. 添加axios库
1
2
3
npm install axios
或者
yarn add axios
  1. 引入vue-router组件,使用yarn add vue-router@4,如果报错请先删除node_modules和yarn.lock文件,再去执行命令
1
yarn add vue-router@4

main.ts:

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
27
28
29
30
31
32
33
import { createApp } from 'vue'
import App from './App.vue'
// 1. 引入你需要的组件
import {Button, Icon, NavBar, Tabbar, TabbarItem, Tag, Divider, TreeSelect, Row, Col, Cell, CellGroup, Form, Field } from 'vant';
// 2. 引入组件样式
import * as VueRouter from 'vue-router';
import routes from './components/config/route.ts';

const app= createApp(App)
app.use(Button);
app.use(NavBar);
app.use(Icon);
app.use(Tabbar);
app.use(TabbarItem);
app.use(Tag);
app.use(Divider);
app.use(TreeSelect);
app.use(Row);
app.use(Col);
app.use(Cell);
app.use(CellGroup);
app.use(Form);
app.use(Field);
app.use(Button);

const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})

app.use(router);
app.mount('#app')

src/components/config/route.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Index from '../pages/Index.vue';
import Team from '../pages/TeamPage.vue';
import User from '../pages/UserPage.vue';
import SearchPage from '../pages/SearchPage.vue';
import UserEditPage from "../pages/UserEditPage.vue";
import SearchResultPage from "../pages/SearchResultPage.vue";
import UserLoginPage from "../pages/UserLoginPage.vue";

const routes = [
{ path: '/', component: Index },
{ path: '/team', component: Team },
{ path: '/user', component: User },
{ path: '/search', component: SearchPage },
{ path: '/user/list', component: SearchResultPage },
{ path: '/user/edit', component: UserEditPage },
{ path: '/user/login', component: UserLoginPage },
]

export default routes;

后端配置

后端的全部依赖:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.4</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.7.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
</dependencies>

application.yml文件:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
application:
name: user-center
profiles:
active: dev
#DataSource config
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hjj
username: root
password: 123456
#session失效时间(分钟)
session:
timeout: 86400
store-type: redis
redis:
port: 6379
host: localhost
database: 0
server:
port: 8080
servlet:
context-path: /api
session:
cookie:
# domain: localhost
same-site: none
secure: true
#禁止将驼峰转为下划线
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
# mybatis-plus打印日志信息
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

Redis相关配置

  1. redis依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.4</version>
</dependency>
  1. spring - session redis(自动将session存入redis中)
1
2
3
4
5
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.7.4</version>
</dependency>
  1. 修改spring-session存储配置 spring.session.store-type默认为none,表示存在单机服务器
    store-type: redis,表示从redis读写session
1
2
3
4
5
6
7
8
9
server:
port: 8080
servlet:
context-path: /api
session:
cookie:
# domain: localhost
same-site: none
secure: true

整合Swagger/knife4j接口文档

目标

  1. 后端整合Swagger + knife4j接口文档

  2. 存量用户信息导入及同步(爬虫)

  3. 前后端联调:搜索页面、用户信息页、用户信息修改页

  4. 标签内容整理

  5. 部分细节优化

后端

Swaggere/knife4j接口文档(本项目使用了knife4j)
Swagger/knife4j原理:

  1. 自定义Swagger配置类
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

@Configuration
@EnableSwagger2WebMvc
@EnableKnife4j
public class Knife4jConfig {

@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.hjj.homieMatching.controller"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("文档标题")
.description("文档描述")
.termsOfServiceUrl("服务条款地址")
.version("文档版本")
.license("开源版本号")
.licenseUrl("开源地址")
.contact(new Contact("作者名", "作者网址", "作者邮箱"))
.build();
}

}
  1. 定义需要生成文档接口的位置(controller)千万注意:在线上环境不要把接口暴露出去,可以在Swagger配置类添加@Profile({“dev”,”test”})注解
  2. 启动即可
  3. 若Spring Boot版本大于2.6,需在yml文件中要更换Spring Boot的路径匹配策略或者在Spring Boot启动类中添加@EnableWebMvc注解(不推荐使用,因为还是需要更换匹配策略)
1
2
3
4
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
  1. 引入对应依赖(Swagger或者Knife4j)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>

前端遇到的问题

Toast报错,那是因为视频中用的是vant3,你可能用的是vant4。直接把Toast替换为showSuccessToast,在引入一下就好了。

1.
引用:

1
import { showSuccessToast } from 'vant';

使用:

1
const onChange = (index) => showSuccessToast(`标签 ${index}`);

如果出现组件在中间的问题,请把style.css文件给删除

前端向后端发送请求出现400

1.
一般4开头的错误码是由于客户端发送请求的有问题,很明显我们在第三期发请求出现的400是由于前端传参出现了问题

  1. 请求参数错误
  2. 前后端请求不一致
  3. 服务器端错误

解决办法
先安装qs库,并且要引入qs

1
2
3
npm install qs
或者
yarn add qs

修改后的代码:
SearchResultPage.vue:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<user-card-list :user-list="userList" />
<van-empty v-show="!userList || userList.length < 1" description="暂无符合要求的用户" />
</template>

<script setup>
import {useRoute} from "vue-router";
import {onMounted, ref} from "vue";
import myAxios from "../../plugins/myAxios.ts";
import qs from 'qs';
import UserCardList from "../UserCardList.vue";

const route = useRoute();
const { tags } = route.query;

const userList = ref([]);

onMounted(async() => {
const userListData = await myAxios.get('/user/search/tags', {
params: {
tagNameList: tags
},
paramsSerializer: params => {
return qs.stringify(params, { indices: false })
}
}).then(function (response) {
console.log('/user/search/tags succeed', response);
return response?.data;
}).catch(function(error) {
console.log('/user/search/tags error', error)
})
if(userListData) {
userListData.forEach(user => {
if(user.tags){
user.tags = JSON.parse(user.tags);
}
})
// 如果请求成功,就把响应结果返回给userList
userList.value = userListData;
}
})

</script>

<style scoped>

</style>

myAxios.ts:

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
import axios from 'axios';

const myAxios = axios.create({
baseURL: 'http://localhost:8080/api'
})

myAxios.defaults.withCredentials = true; //设置为true

// Add a request interceptor
myAxios.interceptors.request.use(function (config) {
console.log('我要发请求啦')
return config;
}, function (error) {
return Promise.reject(error);
});

myAxios.interceptors.response.use(function (response) {
console.log('我收到你的响应啦')
return response.data;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});

export default myAxios;

前端报错 import myAxios from “../../plugins/myAxios.ts”;、

1.
引入文件时去掉拓展名就好了
比如:

1
2
3
import myAxios from "../../plugins/myAxios.ts";
改为
import myAxios from "../../plugins/myAxios";

Axios发送请求获取data失败

1.
因为Axios自动帮我们封装了一层data,所以我们取的时候要多取一层data
myAxios.ts:

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
import axios from 'axios';

const myAxios = axios.create({
baseURL: 'http://localhost:8080/api'
})

myAxios.defaults.withCredentials = true; //设置为true

// Add a request interceptor
myAxios.interceptors.request.use(function (config) {
console.log('我要发请求啦')
return config;
}, function (error) {
return Promise.reject(error);
});

myAxios.interceptors.response.use(function (response) {
console.log('我收到你的响应啦')
return response.data;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});

export default myAxios;

前端发送登录请求没有携带Cookie

1.
在myAxios.ts文件中添加

1
myAxios.defaults..withCredentials = true;//向后端发送请求携带cookie

如果添加了上面的代码还是无效的话可以试试下面的两个方法(可以都试试)
可以在后端common包中添加一个解决跨域配置类,这样的话就好了哈
CorsConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/*
* 解决跨域问题:重写WebMvcConfigurer的addCorsMappings方法(全局跨域配置)
* @author rabbiter
* @date 2023/1/3 1:30
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//是否发送Cookie
.allowCredentials(true)
//放行哪些原始域
.allowedOriginPatterns("*")
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
.allowedHeaders("*")
.exposedHeaders("*");
}
}

除此之外呢还可以看看这位鱼友的帖子:https://articles.zsxq.com/id_2v7g78iofjn7.html

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘username’) 出现了user.username undefined

1.
可以看看这位鱼友的帖子,https://wx.zsxq.com/dweb2/index/topic_detail/814245214541452
2.

出现 $setup.user.createTime.toISOString is not a function问题

1
2
3
4

<van-cell title="注册时间" is-link to="/user/edit" :value="user.createTime.toISOString()" />
改为
<van-cell title="注册时间" is-link to="/user/edit" :value="user?.createTime" /> 试试

没有任何的报错,但是个人信息就是显示不出来

1.
如果没有其他的报错的,但是还是显示不出来用户的信息,可试试下面的方法

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

<template if="user">
<van-cell title="昵称" is-link to="/user/edit" :value="user?.username"/>
<van-cell title="账号" is-link to="/user/edit" :value="user?.userAccount" />
<van-cell title="头像" is-link to="/user/edit">
<img style="height: 48px" :src="user?.avatarUrl"/>
</van-cell>
<van-cell title="性别" is-link to="/user/edit" :value="user?.gender" @click="toEdit('gender', '性别', user.gender)"/>
<van-cell title="电话" is-link to="/user/edit" :value="user?.phone" @click="toEdit('phone', '电话', user.phone)"/>
<van-cell title="邮箱" is-link to="/user/edit" :value="user?.email" />
<van-cell title="星球编号" is-link to="/user/edit" :value="user?.planetCode" />
<van-cell title="注册时间" is-link to="/user/edit" :value="user.createTime" />
</template>

换成

<van-cell title="昵称" is-link to="/user/edit" :value="user?.username"/>
<van-cell title="账号" is-link to="/user/edit" :value="user?.userAccount" />
<van-cell title="头像" is-link to="/user/edit">
<img style="height: 48px" :src="user?.avatarUrl"/>
</van-cell>
<van-cell title="性别" is-link to="/user/edit" :value="user?.gender" @click="toEdit('gender', '性别', user.gender)"/>
<van-cell title="电话" is-link to="/user/edit" :value="user?.phone" @click="toEdit('phone', '电话', user.phone)"/>
<van-cell title="邮箱" is-link to="/user/edit" :value="user?.email" />
<van-cell title="星球编号" is-link to="/user/edit" :value="user?.planetCode" />
<!-- <van-cell title="注册时间" is-link to="/user/edit" :value="user.createTime" />-->

后端遇到的问题

Mybatis-Plus分页查询查不出来?或者报错?

  1. ‘Page’ 为 abstract;无法实例化或者’com.baomidou.mybatisplus.extension.service.IService’ 中的 ‘page(com.baomidou.mybatisplus.core.metadata.IPage<com.hjj.homieMatching.model.domain.User>, com.baomidou.mybatisplus.core.conditions.Wrapper<com.hjj.homieMatching.model.domain.User>)’ 无法应用于 ‘(org.springframework.data.domain.Page<com.hjj.homieMatching.model.domain.User>, com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.hjj.homieMatching.model.domain.User>)’
    这是因为Page的包引错了,而第二个错误是因为类型不兼容
    应该引的是

1
2
3
4
5
6
7
8
9
10
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
// 对应的controller部分的代码改为
@GetMapping("/recommend")
public BaseResponse<IPage<User>> recommendUsers(long pageSize, long pageNum, HttpServletRequest request){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
IPage<User> page = new Page<>(pageNum, pageSize);
IPage<User> userList = userService.page(page, queryWrapper);
return ResultUtils.success(userList);
}

如果出现无法解析符号 ‘MybatisPlusInterceptor’

1.
package com.hjj.homieMatching.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
这个问题说明你的Mybatis-Plus依赖版本过低,请调整为3.4.0以上。
2.

配置MyBatis-Plus分页查询配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDbType(DbType.MYSQL);
paginationInnerInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}

}

利用RedisTemplate往Redis中存值,发现乱码

1.
为什么在这里我们往redis存值取值发现是存在的,但是在redis客户端取值却显示不存在呢?
因为redistemplate帮我们序列化了,。这就是为什么鱼皮哥在Redis GUI软件查看键是乱码的

想用RedisTemplate往Redis中进行crud操作时,得提前配置一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<String, Object> template=new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}

爬虫

存量用户信息导入

  1. 把所有星球用户的信息导入
  2. 把写了自我介绍的同学的标签信息导入

怎么抓取网上信息

  1. 分析原网站是怎么获取这些数据的?哪个接口?按 F 12 打开控制台,查看网络请求,复制 curl 代码便于查看和执行
  2. 用程序去调用接口 (java okhttp httpclient / python 都可以)
  3. 处理(清洗)一下数据,之后就可以写到数据库里

具体流程

  1. 从 excel 中导入全量用户数据,判重 。 easy excel:https://alibaba-easyexcel.github.io/index.html
  2. 抓取写了自我介绍的同学信息,提取出用户昵称、用户唯一 id、自我介绍信息
  3. 从自我介绍中提取信息,然后写入到数据库中
EasyExcel

两种读对象的方式:

  1. 确定表头:建立对象,和表头形成映射关系
  2. 不确定表头:每一行数据映射为 Map<String, Object>

两种读取模式:

  1. 监听器:先创建监听器、在读取文件时绑定监听器。单独抽离处理逻辑,代码清晰易于维护;一条一条处理,适用于数据量大的场景。
    TableListener:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

// 有个很重要的点 TableListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class TableListener implements ReadListener<PlanetUserInfo> {

@Override
public void invoke(PlanetUserInfo planetUserInfo, AnalysisContext analysisContext) {

}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {

}
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
ReadListener.super.invokeHead(headMap, context);
}
}

ImportExcel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.alibaba.excel.EasyExcel;

/**
* 导入excel数据
*/
public class ImportExcel {
/**
* 读取数据
*/
public static void main(String[] args) {
// 写法1:JDK8+ ,不用额外写一个DemoDataListener
// since: 3.0.0-beta1
String fileName = "E:\\Java星球项目\\homieMatching\\homieMatching\\src\\main\\resources\\alarm.csv";
// 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
EasyExcel.read(fileName, PlanetUserInfo.class, new TableListener()).sheet().doRead();
}
}

PlanetUserInfo:

1
2
3
4
5
6
7
8
import com.alibaba.excel.annotation.ExcelProperty;

public class PlanetUserInfo {
@ExcelProperty("ID")
private String ID;
@ExcelProperty("alarm")
private String alarm;
}
  1. 同步读:无需创建监听器,一次性获取完整数据。方便简单,但是数据量大时会有等待时常,也可能内存溢出。
1
2
3
4
public static void synchronousRead(String fileName){
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 同步读取会自动finish
List<PlanetUserInfo> totalDataList = EasyExcel.read(fileName).head(PlanetUserInfo.class).sheet().doReadSync();
}

干货

router.push()和router.replace()的区别

router.push()会在原有的历史记录中压入新的历史记录,而router.place()则会代替上一条历史记录,这样用户点击返回不会回到登录页了

Session共享

种 session 的时候注意范围,cookie.domain

如果想要共享cookie,可以种一个更高层的域名,将要共享session的两个域名设为二级域名

如何开启同一后端项目,但配置不同的端口号

1
java -jar .\homieMatching-0.0.1-SNAPSHOT.jar --server.port=8081

为什么在服务器A登录后,服务器B拿不到用户信息

因为用户在A登录,session只存在于A中,而B中没有,所以服务器B获取用户信息时会失败

解决办法

  1. Redis(基于内存的 K/V 数据库)
    将Cookie存储在Redis中实现分布式登录,在A中登录的Cookie存在Redis中,那么B要获取登录信息先从Redis中获取对应Cookie再拿到登录信息
  2. MySQL
  3. 文件服务器ceph

导入数据

  1. 用户可视化界面:适合一次性导入,数据量可控
  2. 写程序:for循环,建议分批,不要一次梭哈(可以用接口控制)要保证可控,幂等,注意线上环境和测试环境是有区别的
    导入1000万条,for i 1000w

编写一次性任务

for循环插入数据的问题:

  1. 建立和释放数据库的链接(批量插入)
  2. for循环是绝对线性的(并发),并发要注意执行的先后顺序,不要使用非并发类的集合
1
2
//CPU密集型:分配的核心线程数=CPU-1
//I0密集型:分配的核心线程数可以大于CPU核数

数据库慢?预先把数据查出来,放到一个更快读取的地方,不用再
查数据库了。(缓存)
预加载缓存,定时更新缓存。(定时任务)
多个机器都要执行任务么?(分布式锁:控制同一时间只有一台机
器去执行定时任务,其他机器不用重复执行了)

数据查询慢怎么办?

用缓存:提前把数据取出来保存好(通常保存在读写更快地介质,比如内存),就可以更快地读写

缓存的实现:

  • Redis(分布式缓存,支持多个进程或者多个服务器之间的数据共享)
  • memcached(分布式)
  • Etcd(主要用于共享配置和服务发现。云原生架构的一个分布式,扩容能力强)
  • ehcache(单机)
  • 本地缓存(Java的Map集合)
  • Caffeine(是一个Java库,但是呢它是本地的,只能在单个JVM进程中使用,不能再多个进程或服务器之间共享数据)
  • Google Guava

Redis

  • 基于内存的K/V存储中间件
  • NoSQL键值对数据库
  • 也可作为消息队列

Java中操作Redis的方式

  1. Spring Data Redis(推荐)
    通用的数据库访问框架,定义了一组增删改查的接口
  2. Jedis(独立于Spring操作Redis)
  3. Redisson

Jedis

  • 独立于Spring操作Redis的Java客户端
  • 要配合Jedis Pool使用
  • Jedis与commons pool会有冲突

Lettuce

高阶的操作Redis的Java客户端

  • 支持异步、连接池

Redisson(写在简历上,是个亮点)

分布式操作Redis的Java客户端,像操作本地的集合一样操作Redis

JetCache

操作方式对比:

  1. 如果你用Spring开发,并且没有过多的定制化要求选Spring Data Redis
  2. 如果你没有用Spring,并且追求简单,没有过多的性能要求,可以用Jedis + Jedis Pool
  3. 如果你的项目不是Spring,并且追求高性能,高定制化,可以用lettuce。支持异步、连接池(技术大牛使用)
  4. 如果你的项目是分布式的,需要用到一些分布特性(比如分布式锁,分布式集合),推荐使用Redisson

Redis数据结构

  • String类型:sex:”男”
  • List列表:hobby:[“编程”,”睡觉”]
  • Set集合:hobby:[“编程”,”睡觉”],值不重复。可用于点赞,即一个人只能点一次赞
  • Hash哈希:nameAge:{“burger”:1,”hamburger”:2}
  • Zset集合:相比对Set多一个score分数,是一个有顺序的Set集合,一般用作实现排行榜
  • bloomfilter(布隆过滤器,主要从大量数据中快速过滤值,比如邮件黑名单拦截)
  • geo(计算地理位置)
  • hyperloglog(pv / uv)
  • pub / sub (发布订阅,类似消息队列)
  • BitMap(101101011110101001)可用于签到和存储压缩值

如何如何设计缓存Key

目的:使得不同用户看到的数据不同

systemId:moudleId:func:options(不要和别人冲突)

homie:user:recommend:

redis 内存不能无限增加,k一定要设置过期时间

缓存预热

问题:第一个用户访问还是很慢(加入第一个勇士),也能一定程度上保护数据库

缓存预热的优缺点:

  1. 解决上述问题,让用户始终访问很快

缺点:

  1. 增加开发成本(需要额外的开发和设计)
  2. 预热的时机和时间不合适的话,有可能你缓存的数据不对或者太老
  3. 空间换时间

缓存预热目的

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

注意点:

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

怎么预热缓存?

  1. 定时触发(常用)
  2. 手动触发

定时任务实现

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

第一种方式:

  1. 在主类添加@EnableScheduling注解
  2. 给要执行的方法添加@Scheduling注解,指定cron表达式或者执行频率

不要去背cron表达式

缓存穿透

用户访问的数据既不在缓存也不再数据库中,导致大致请求到达数据库,对数据库造成巨大压力

缓存击穿

大量的Key同时失效或者Redis宕机,导致大量请求访问数据库,带来巨大压力

缓存雪崩

也叫热点Key问题。在一段时间内,被高并发访问并且缓存重建业务较为复杂的Key突然失效,巨量的请求会抵达数据库,对其造成巨大冲击

控制定时任务的发布

why?

  1. 浪费资源,想象10000条服务器同时“打鸣”
  2. 脏数据,比如重复插入

要控制定时任务在同一时间只有一个服务器执行

实现方式:

  1. 分离定时任务,只安排一个服务器执行定时任务。成本太大
  2. 写死配置,每个服务器都执行定时任务,但是只有IP地址符合配置的服务器才会执行。适合于并发量不大的场景,成本低。问题:IP可能是不固定的。
  3. 动态配置,配置是可以轻松得、方便更新得,但还是只有ip符合配置的服务器才真会执行业务逻辑代码
  • 数据库
  • Redis
  • 配置中心(Nacos,Apollo,Spring Cloud Config)

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

  1. 分布式锁,只有抢到锁的服务器才能执行对应的业务逻辑。
  • 坏处:增加成本
  • 好处:不用手动配置,不管有多少个服务器在抢锁

单机就会存在故障

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

Java实现锁:synchronized,并发包

分布式锁

为啥需要分布式锁?

  1. 从锁的必要性出发。在资源有限的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能够访问资源
  2. 单个锁只对单个JVM有效

分布式锁实现的关键

抢锁机制

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

核心思想:先来的人先把数据改为自己独有的标识(比如服务器IP),后来的人发现标识存在,则抢锁失败,继续等到。等先来的人的执行方法结束,把标识清空,其他人继续抢锁。

实现方式:

  1. MySQL数据库:select for update行级锁(最简单)
  2. 乐观锁
  3. Redis实现:内存数据库,速度快。支持setnx,lua脚本支持原子性操作
    setnx: set if not exists如果不存在,则设置;只有设置成功才会返回true
  4. Zookeeper实现(不推荐)
注意事项
  1. 用完就删掉锁
  2. 锁一定要添加过期时间,防止因为服务器宕机没释放锁
  3. 过期时间要大于业务执行时间

分布式锁导致其他服务器数据不一致