homie 匹配系统笔记 前端项目初始化
使用vite初始化项目,根据你的需求选择相应地选项
安装依赖
按需整合vant组件库
1 npm i vite-plugin-style-import@1.4.1 -D
关闭vite语法检查(前提使用build启动项目),在package.json文件中,将原始的build改为build”: “vite build”
添加axios库
1 2 3 npm install axios 或者 yarn add axios
引入vue-router组件,使用yarn add vue-router@4,如果报错请先删除node_modules和yarn.lock文件,再去执行命令
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相关配置
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>
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>
修改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接口文档
目标
后端整合Swagger + knife4j接口文档
存量用户信息导入及同步(爬虫)
前后端联调:搜索页面、用户信息页、用户信息修改页
标签内容整理
部分细节优化
后端 Swaggere/knife4j接口文档(本项目使用了knife4j) Swagger/knife4j原理:
自定义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(); } }
定义需要生成文档接口的位置(controller)千万注意:在线上环境不要把接口暴露出去,可以在Swagger配置类添加@Profile({“dev”,”test”})注解
启动即可
若Spring Boot版本大于2.6,需在yml文件中要更换Spring Boot的路径匹配策略或者在Spring Boot启动类中添加@EnableWebMvc注解(不推荐使用,因为还是需要更换匹配策略)
1 2 3 4 spring: mvc: pathmatch: matching-strategy: ant_path_matcher
引入对应依赖(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是由于前端传参出现了问题
请求参数错误
前后端请求不一致
服务器端错误
解决办法 先安装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分页查询查不出来?或者报错?
‘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; } }
爬虫 存量用户信息导入
把所有星球用户的信息导入
把写了自我介绍的同学的标签信息导入
怎么抓取网上信息
分析原网站是怎么获取这些数据的?哪个接口?按 F 12 打开控制台,查看网络请求,复制 curl 代码便于查看和执行
用程序去调用接口 (java okhttp httpclient / python 都可以)
处理(清洗)一下数据,之后就可以写到数据库里
具体流程
从 excel 中导入全量用户数据,判重 。 easy excel:https://alibaba-easyexcel.github.io/index.html
抓取写了自我介绍的同学信息,提取出用户昵称、用户唯一 id、自我介绍信息
从自我介绍中提取信息,然后写入到数据库中
EasyExcel 两种读对象的方式:
确定表头:建立对象,和表头形成映射关系
不确定表头:每一行数据映射为 Map<String, Object>
两种读取模式:
监听器:先创建监听器、在读取文件时绑定监听器。单独抽离处理逻辑,代码清晰易于维护;一条一条处理,适用于数据量大的场景。 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;@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;public class ImportExcel { public static void main (String[] args) { String fileName = "E:\\Java星球项目\\homieMatching\\homieMatching\\src\\main\\resources\\alarm.csv" ; 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 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获取用户信息时会失败
解决办法
Redis(基于内存的 K/V 数据库) 将Cookie存储在Redis中实现分布式登录,在A中登录的Cookie存在Redis中,那么B要获取登录信息先从Redis中获取对应Cookie再拿到登录信息
MySQL
文件服务器ceph
导入数据
用户可视化界面:适合一次性导入,数据量可控
写程序:for循环,建议分批,不要一次梭哈(可以用接口控制)要保证可控,幂等,注意线上环境和测试环境是有区别的 导入1000万条,for i 1000w
编写一次性任务 for循环插入数据的问题:
建立和释放数据库的链接(批量插入)
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的方式
Spring Data Redis(推荐) 通用的数据库访问框架,定义了一组增删改查 的接口
Jedis(独立于Spring操作Redis)
Redisson
Jedis
独立于Spring操作Redis的Java客户端
要配合Jedis Pool使用
Jedis与commons pool会有冲突
Lettuce 高阶 的操作Redis的Java客户端
Redisson(写在简历上,是个亮点) 分布式操作Redis的Java客户端,像操作本地的集合一样操作Redis
JetCache 操作方式对比:
如果你用Spring开发,并且没有过多的定制化要求选Spring Data Redis
如果你没有用Spring,并且追求简单,没有过多的性能要求,可以用Jedis + Jedis Pool
如果你的项目不是Spring,并且追求高性能,高定制化,可以用lettuce。支持异步、连接池(技术大牛使用)
如果你的项目是分布式的,需要用到一些分布特性(比如分布式锁,分布式集合),推荐使用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一定要设置过期时间
缓存预热 问题:第一个用户访问还是很慢(加入第一个勇士),也能一定程度上保护数据库
缓存预热的优缺点:
解决上述问题,让用户始终访问很快
缺点:
增加开发成本(需要额外的开发和设计)
预热的时机和时间不合适的话,有可能你缓存的数据不对或者太老
空间换时间
缓存预热目的 用定时任务,每天刷新所有用户的推荐列表
注意点:
缓存预热的意义(新增少、总用户多)
缓存的空间不能太大,要预留给其他缓存空间
缓存数据的周期
怎么预热缓存?
定时触发(常用)
手动触发
定时任务实现
Spring Scheduler(Spring Boot默认整合的)
Quartz(独立于Spring Boot存在的定时任务框架)
XXL-Job之类的分布式任务调度平台(界面 + SDK)
第一种方式:
在主类添加@EnableScheduling注解
给要执行的方法添加@Scheduling注解,指定cron表达式或者执行频率
不要去背cron表达式
缓存穿透 用户访问的数据既不在缓存也不再数据库中,导致大致请求到达数据库,对数据库造成巨大压力
缓存击穿 大量的Key同时失效或者Redis宕机,导致大量请求访问数据库,带来巨大压力
缓存雪崩 也叫热点Key问题。在一段时间内,被高并发访问并且缓存重建业务较为复杂的Key突然失效,巨量的请求会抵达数据库,对其造成巨大冲击
控制定时任务的发布 why?
浪费资源,想象10000条服务器同时“打鸣”
脏数据,比如重复插入
要控制定时任务在同一时间只有一个服务器执行
实现方式:
分离定时任务,只安排一个服务器执行定时任务。成本太大
写死配置,每个服务器都执行定时任务,但是只有IP地址符合配置的服务器才会执行。适合于并发量不大的场景,成本低。问题:IP可能是不固定的。
动态配置,配置是可以轻松得、方便更新得,但还是只有ip符合配置的服务器才真会执行业务逻辑代码
数据库
Redis
配置中心(Nacos,Apollo,Spring Cloud Config)
问题:服务器多了,IP不可控还是很麻烦,还要人工修改
分布式锁,只有抢到锁的服务器才能执行对应的业务逻辑。
坏处:增加成本
好处:不用手动配置,不管有多少个服务器在抢锁
单机就会存在故障
锁 在资源有限的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能够访问资源
Java实现锁:synchronized,并发包
分布式锁 为啥需要分布式锁?
从锁的必要性出发。在资源有限的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能够访问资源
单个锁只对单个JVM有效
分布式锁实现的关键 抢锁机制 怎么保证同一时间只有一个服务器能抢到锁?
核心思想:先来的人先把数据改为自己独有的标识(比如服务器IP),后来的人发现标识存在,则抢锁失败,继续等到。等先来的人的执行方法结束,把标识清空,其他人继续抢锁。
实现方式:
MySQL数据库:select for update行级锁(最简单)
乐观锁
Redis实现 :内存数据库,速度快。支持setnx,lua脚本支持原子性操作 setnx: set if not exists如果不存在,则设置;只有设置成功才会返回true
Zookeeper实现(不推荐)
注意事项
用完就删掉锁
锁一定要添加过期时间,防止因为服务器宕机没释放锁
过期时间要大于业务执行时间
分布式锁导致其他服务器数据不一致