缓存与前端
# 缓存与前端
# 常见的缓存有哪些
- Mybatis 的一级缓存和二级缓存
- 本地缓存
- 分布式缓存(Redis)
- 前端 H5 的
sessionStorage(会话缓存) - 前端 H5 的
localStorage
非常适合【读多写少】的场景。
# 详解 Mybatis 的一级缓存
在当前会话中自动使用。一级缓存可以帮助我们减少重复的数据库查询。
示例一:会执行两条 SQL 语句
public List<TrainQueryVO> queryAll() {
List<Train> trainList = this.selectAll();
LOG.info("再查一次");
trainList = this.selectAll();
return BeanUtil.copyToList(trainList, TrainQueryVO.class);
}
public List<Train> selectAll() {
TrainExample trainExample = new TrainExample();
trainExample.setOrderByClause("code asc");
return trainMapper.selectByExample(trainExample);
}
2
3
4
5
6
7
8
9
10
11
12
示例二:只会执行一次 SQL 语句,也就是第一个 selectAll() 方法
@Transactional(rollbackFor = Exception.class)
public List<TrainQueryVO> queryAll() {
List<Train> trainList = this.selectAll();
LOG.info("再查一次");
trainList = this.selectAll();
return BeanUtil.copyToList(trainList, TrainQueryVO.class);
}
public List<Train> selectAll() {
TrainExample trainExample = new TrainExample();
trainExample.setOrderByClause("code asc");
return trainMapper.selectByExample(trainExample);
}
2
3
4
5
6
7
8
9
10
11
12
13
如何关闭一级缓存?
在配置文件中修改 Mybatis 缓存策略,有两个选项:
statement—— SQL 级别,每执行一条 SQL 语句都会清除缓存session—— 会话级别,关闭会话时才会清除缓存
mybatis:
configuration:
local-cache-scope: statement
2
3
# 详解 Mybatis 的二级缓存
- 默认没有开启,开启条件为对应的实体类需要实现序列化,并且在 mapper.xml 下添加
<cache></cache>标签- 当一个类需要保存起来,下次再还原成类时就需要序列化,或者需要远程传输,比如放到 Redis 里,也需要序列化
- 在【命名空间
namespace】内添加缓存标签
- 每一个 mapper 的二级缓存都是不一样的
什么时候二级缓存会失效?
对同个 namespace 做增删改操作时,二级缓存就会清空。
缺点
在实际项目中很少会使用二级缓存。
假设现在有【两个节点】,同时执行了一条一样的查询操作,并缓存了起来,第一个节点接着执行了删除操作,那么下一次再执行这个查询操作的时候,第一个节点就会从数据库中重新查询,而第二个节点则是直接使用二级缓存,这就导致了两个节点查询出来的【数据可能不一致】。
# SpringBoot 的内置缓存
1、添加依赖
<!--spring内置缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2
3
4
5
2、开启使用缓存
启动类中,添加 @EnableCaching 注解
作用:本地缓存可以解决 Mybatis 二级缓存需要修改 mapper 的问题,但同样存在多个节点缓存不一样的问题(数据不一致)。
使用小记:
1、使用缓存注解
// value 可自定义
@Cacheable(value = "业务类名.方法名")
2
解释:开辟一块空间,根据不同的请求参数,空间内会缓存多个结果。会根据请求参数生成一个 key,需要对请求参数生成 hashCode 和 equals 方法,用于生成 key。
2、缓存刷新注解
@CachePut(value = "业务类名.方法名")
解释:结合上一个注解一起使用,此注解会刷新缓存,每次都查询数据库。
# 分布式缓存
比如 Redis,不建议在 common 模块下直接引入相关依赖,不方便后期统计哪些模块使用了 Redis。
使用 Redis 解决了两个问题:
- 提高访问速度,MySQL 单机 QPS 约为 2000,Redis 约 10 万
- 解决多节点共享缓存,机器重启也不会丢缓存数据
Redis 常用于放用户的登录信息,早期没有 Redis 时,登录信息都放在 session 中,应用一重启,登录就没有了,多节点 session 又是另一个头大的问题。
# 前端使用缓存
第一次请求调用后端接口,然后将数据存入会话缓存,下次用户查询数据时就会直接从浏览器会话缓存中获取数据。
- 关闭网站,重新进入即可获取最新数据
- 现实中的场景,比如做了什么配置的改动,就会要求重新登录,这样就可以重新查询最新数据并缓存
# 缓存问题
| 问题 | 说明 | 解决方法 |
|---|---|---|
| 缓存击穿 | 一个热点的 key 失效后,导致大量请求直接访问数据库 | 1. 写一个定时任务,定时刷新缓存(可避免缓存超时失效问题,但缓存机器坏了就不能解决了) 2. 针对数据库查询,增加分布式锁,100 个请求,只有一个拿到锁去查数据库,其它 99 个请求都快速失败,告诉用户稍候重试 —— 相当于查询失败,不能让请求一直等待,会占用大量资源(可解决缓存机器坏了的情况) |
| 缓存穿透 | 去缓存里取数据,因数据库本身就没数据而造成缓存穿透 | 1. 给缓存设置空值,并给数据库查询加分布式锁 |
| 缓存雪崩 | 由于短时间内,大量的 key 失效导致数据库压力剧增 | — |
# 缓存在高并发场景中的生产问题分享
场景: 每天的会员数很多,百万级别,但每个会员一天只会有几次请求,一个会员信息在同一次请求中,会被用到多次,且会员信息较大。
问题 1: 多次调用查询会员方法,组装信息多,多次访问数据库
解决: 使用本地缓存,1 分钟有效(请求不多且不频繁,没必要使用第三方组件 Redis,且会增加 IO 消耗)
问题 2: Full GC(Stop the World)频繁,导致短时间内大量请求失败
解决: 去掉本地缓存,使用线程本地变量
# 何谓 Full GC
这问题的描述涉及到 Java 虚拟机(JVM)垃圾回收(Garbage Collection)的概念。
- Full GC(Full Garbage Collection): Full GC 是指进行一次完整的垃圾回收,包括新生代和老年代。这通常会触发一次长时间的停顿(Stop-the-World)。在这个停顿期间,所有的应用线程都被暂停,垃圾回收线程负责清理整个堆内存。
- Stop-the-World: 在进行垃圾回收时,为了确保一致性和安全,Java 应用线程会被暂停。这个停顿的时间就是 Stop-the-World 时间。对于 Full GC,这个停顿时间较长,可能导致应用在这段时间内无法响应请求。
频繁的 Full GC 可能是由于以下原因之一导致:
- 内存不足: 当堆内存中的对象越来越多,且垃圾回收器无法及时清理出足够的空间时,就会频繁触发 Full GC
- 对象生命周期长: 如果系统中存在大量生命周期较长的对象,它们可能会晋升到老年代,导致老年代空间不足时频繁进行 Full GC
- 堆大小不合适: 如果分配给 Java 应用的堆内存不足以容纳应用的工作负载,也可能导致频繁的 Full GC
解决这个问题可能需要调整堆大小、优化代码以减少对象生命周期,或者考虑使用不同的垃圾回收算法。要深入了解问题的具体原因,可能需要进行性能分析和监控,使用工具如 Java Mission Control、VisualVM 等。
# 场景问题分析
1. 缓存过期时间过短导致频繁失效:
- 问题描述: 如果本地缓存的过期时间设置得很短,而且缓存中的数据量很大,可能会导致频繁失效和重新加载,增加 GC 的负担
- 解决方案: 考虑调整缓存的过期时间,使其更适应实际业务需求。过短的缓存时间可能不划算,因为它会导致频繁的缓存加载
2. 缓存对象大小过大:
- 问题描述: 会员信息较大,如果缓存中的对象占用大量内存,会增加 GC 的频率和负担
- 解决方案: 考虑对缓存中的大对象进行优化,可能可以使用更节省内存的数据结构,或者对大对象进行分片处理
3. 缓存数据并发更新问题:
- 问题描述: 如果缓存中的数据需要频繁更新,可能存在并发更新的问题,导致缓存不一致
- 解决方案: 可以考虑使用更高级别的缓存方案,例如分布式缓存,以解决并发更新和一致性的问题
4. 线程本地变量可能带来的问题:
- 问题描述: 使用线程本地变量可能会导致资源泄漏和对资源的持有时间过长,需要小心管理线程本地变量的生命周期
- 解决方案: 确保在线程本地变量的使用中适时清理和释放资源,防止长时间持有导致的问题
# 查看端口占用问题
前言:打开酷狗音乐,会占用
8000端口
Windows 排查端口占用问题:
1、查看被占用端口对应的 PID
netstat -aon|findstr "[端口号]"
2、查看指定 PID 的进程
tasklist|findstr "[PID]"
3、结束进程
强制(/F 参数)杀死 pid 为 9088 的所有进程包括子进程(/T 参数):
taskkill /T /F /PID 9088
参考文章:Windows 下如何查看某个端口被谁占用 | 菜鸟教程 (opens new window)
# 接口隔离
控制台接口、会员接口、第三方接口等,做接口隔离,不要混用。
# 跨域
跨域是指:
- 前后端不在同一个域
- IP 一样,端口不一样,也算跨域
# 日志的使用
在各种分支、循环、关键业务点,都打上日志,从日志就能看出代码执行到哪里了。
# 日志流水号的打印
- 日志流水号不能放在 AOP 里面打印,因为拦截器比 AOP 的优先级高
- 所以可以新建一个
日志拦截器来专门打印日志流水号
# 前端
# 前端模块的搭建
- 脚手架:Vue CLI 5 = Vue + 一大堆第三方组件
- Vue 3:页面开发基于 Vue 3
- Ant Design Vue 3:阿里团队开源的基于 Vue 的 UI 组件
UI 组件有很多可选,一种是选择基于 CSS 的,如:Bootstrap,适合各种前端框架。
一种是选择基于 Vue 的 UI 组件,只能用于 Vue 框架。
# 文件讲解
在 Vue3 项目中,App.vue 和 main.js 都是核心文件,它们各自承担不同的职责。
# App.vue
App.vue 是项目的主组件,它是 Vue 应用程序的起点,也是所有其他子组件的父组件。
App.vue 的作用包括:
- 布局设计:通常,
App.vue用于定义整个应用程序的基本布局结构。例如,可能包含导航栏、侧边栏、页脚等基本布局元素 - 提供数据/方法:虽然不常见,但有时可能会在
App.vue中定义一些共享的数据属性或方法,供其子组件使用 - 渲染子组件:
App.vue中的模板会作为顶级内容被渲染到浏览器中,因此可以通过在模板中插入其他组件<component>标签来展示各个功能模块
# main.js
main.js 是整个项目的入口文件,它主要负责以下几个方面:
- 实例化 Vue 应用:使用
createApp()函数创建一个新的 Vue 应用程序实例。这是将 Vue 与实际 HTML 页面关联起来的地方 - 注册全局组件:在这个文件中,你可以注册在整个项目中都可以使用的全局组件,这通常通过调用
app.component()方法来实现 - 安装插件:如果你的项目依赖于一些外部插件(如状态管理库 Vuex、路由库 Vue Router 等),你可以在
main.js中安装它们,通常是通过调用app.use()方法 - 挂载根组件:最后一步是在 DOM 中指定一个挂载点,并将 Vue 应用程序实例挂载到这个元素上,通常通过调用
app.mount()方法完成 - 设置全局变量或配置:根据需要,可以在这里定义全局变量或修改 Vue 的一些默认配置
总之:
main.js主要负责初始化和配置整个 Vue 应用程序App.vue则更多地关注应用程序的实际 UI 结构和呈现
# package.json
类似 Maven 的 pom.xml,用于引入依赖。
# package-lock.json
用于锁定版本号:
- 锁定当前依赖的版本
- 锁定当前依赖的第三方依赖的版本
# store
Vuex 状态管理,用于在全局共享数据。
# VueCLI 多环境配置
# 创建文件
创建 .env.dev 和 .env.prod 文件:
NODE_ENV是内置变量- 自定义变量用
VUE_APP_开头
1、配置开发环境:
NODE_ENV=development
VUE_APP_SERVER=http://localhost:8000
2
2、配置生产环境:
NODE_ENV=production
VUE_APP_SERVER=http://train-server.jiawablog.com
2
# 打印日志
在 main.js 文件下,添加以下内容:
// 给axios添加基础URL, 配置之后所有axios请求都会自动带上这个URL
axios.defaults.baseURL = process.env.VUE_APP_SERVER;
// 打印当前环境配置
console.log('环境:', process.env.NODE_ENV);
console.log('服务端:', process.env.VUE_APP_SERVER);
2
3
4
5
# 配置运行参数
修改 package.json 文件,以【开发环境】为例:在运行脚本("scripts")中添加参数:--mode dev
完整参数如下(开发环境和生产环境分开配置):
"scripts": {
"web-dev": "vue-cli-service serve --mode dev --port 9000",
"web-prod": "vue-cli-service serve --mode prod --port 9000",
"build-web-prod": "vue-cli-service build --mode prod",
"lint": "vue-cli-service lint"
},
2
3
4
5
6
# 引入公共组件
引入公共组件时,不要直接复制粘贴引入:
- 应该让编译器自动补全
- 自动补全,会自动引入
import语句 - 会自动引入
components属性值
# 解决浏览器刷新问题
# 设置会话存储
public/js/目录下创建session-storage.js文件- Vuex 配合 H5 的会话存储(sessionStorage)解决浏览器刷新问题
# 引入 js 文件
坐标:public/ 目录下的 index.html 文件
<script src="<%= BASE_URL %>js/session-storage.js"></script>
注意
js前面没有斜杠。
# 使用
1、登录成功后设置会话存储值
// 给变量赋值, _member 是外部传进来的参数
mutations: {
setMember (state, _member) {
state.member = _member;
// 存入会话存储, 设置值
window.SessionStorage.set(MEMBER, _member);
}
},
2
3
4
5
6
7
8
2、变量获取值
// 创建全局变量
state: {
// 从会话存储中获取值
member: window.SessionStorage.get(MEMBER) || {}
},
2
3
4
5
设计细节:
|| {}的使用 —— 空对象的设置,避免了会话存储中没有值时引发的空指针异常。
# 避免用户通过修改路径而访问主页
# 为路由页面增加登录拦截
1、设置登录开关
{
path: '/',
component: () => import('../views/main.vue'),
// 设置登录开关
meta: {
loginRequire: true
},
}
2
3
4
5
6
7
8
2、路由登录拦截逻辑处理
// 路由登录拦截
router.beforeEach((to, from, next) => {
// 要不要对meta.loginRequire属性做监控拦截
if (to.matched.some(function (item) {
console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);
return item.meta.loginRequire
})) {
const _member = store.state.member;
console.log("页面登录校验开始:", _member);
if (!_member.token) {
console.log("用户未登录或登录超时!");
notification.error({description: "未登录或登录超时"});
next('/login');
} else {
next();
}
} else {
next();
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 与后端交互的拦截
操作方法:在 axios 拦截器拦截响应时,进行响应状态码 401 判断。(单纯设置这部分内容,不做登录拦截,用户会看到静态页面)
核心判断:
// 判断状态码是401 跳转到登录页
if (status === 401) {
console.log("未登录或登录超时,跳到登录页");
store.commit("setMember", {});
notification.error({ description: "未登录或登录超时" });
router.push('/login');
}
2
3
4
5
6
7
全部代码:
axios.interceptors.response.use(function (response) {
console.log('返回结果:', response);
return response;
}, error => {
console.log('返回错误:', error);
const response = error.response;
const status = response.status;
// 判断状态码是401 跳转到登录页
if (status === 401) {
console.log("未登录或登录超时,跳到登录页");
store.commit("setMember", {});
notification.error({ description: "未登录或登录超时" });
router.push('/login');
}
return Promise.reject(error);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 规则配置
坐标: package.json 的 "eslintConfig" 属性中
规则代码:
"rules": {
"vue/multi-word-component-names": 0,
"no-undef": 0
}
2
3
4
规则解析:
在 ESLint 配置中,规则的设置是为了确保代码的一致性和避免潜在的问题。以上两个规则的作用如下:
vue/multi-word-component-names:- 这个规则是 Vue.js 的一个特定规则,用于检查组件名称是否符合最佳实践。默认情况下,该规则要求组件名必须包含多个单词(即由空格分隔)。这是为了提高代码的可读性和一致性
- 通过将这个规则设置为
0,你可以关闭这个规则,允许单词或任何其他类型的组件名称。这样做的原因可能是项目中有特殊的命名约定,或者开发团队认为单个单词的组件名称足够清晰和易于理解
no-undef:- 这是一个通用的 JavaScript 规则,用于禁止使用未声明的变量。如果启用了这个规则,ESLint 将会警告你在代码中使用但没有先声明的变量
- 通过将这个规则设置为
0,你可以关闭这个规则,允许在代码中使用未声明的变量。这可能是因为你的项目有一些全局变量或其他依赖库定义了这些变量,或者你希望在某些情况下允许使用未声明的变量 - 不设置并且使用未声明变量的话,运行时会报错。比如使用全局变量的时候
# 小技巧
做页面修改时,比如抽象公共组件使用时,修改一些【显而易见】的词,来检查代码修改是否生效,避免查询到的是缓存。
# 收获
- 学会纯前端项目的搭建
- 理解前后端分离架构
# 编程规范
- 内部变量(局部变量):使用
_开头 - 程序设计:约定优于配置
# 思考
- 程序员需要经常反思自己写过的代码,有没有 BUG?
- 能不能提升性能?
- 能不能提高开发效率?
- 能不能扩展出新功能?
# 项目部署
# 阿里云资源准备
RDS(数据库)、ECS(相当于一台 Linux 服务器)、OSS(对象存储,可配置域名,可部署前端)、SLB(相当于一个配置公网的东西)、Redis 等
生产的 ECS 中,可以配置只开放 gateway 的 8000 端口,其它 8001、8002 端口都不开放,这样就不用担心外部请求绕过 gateway,直接访问 member 模块(端口根据自己的项目来定)
# 前后端的发布
JDK、Nacos、SpringBoot、Vue CLI
# 生产必备:多节点与域名配置
- 多节点是服务高可用最基本的要求,否则,一旦我们遇到服务器宕机、服务器升级重启、应用发布,此时,服务就不可用了
- 多节点要求:应用是无状态的(思考 session 和 token)
- 无状态:应用内没有保存特殊的数据,比如将登录信息保存在 session 里,这种就是保存了和别的节点不一样的数据,属于有状态的应用
# CDN 配置
- 配置 CDN 加速域名
- CDN 也是按流量付费,但流量费会比 OSS 便宜,所以配置 CDN 会更划算,又快又便宜
- CDN 费用:外部流量费(大头),回源流量费
# HTTPS 配置
- 需要 SSL 证书或自定义证书
- 阿里云可免费领取证书,每年有 20 个额度