知识梳理
# 知识梳理
# 基础业务
# 拦截器与 AOP
- 拦截器只能拦截 Controller
- 而 AOP 不仅可以拦截 Controller,还可以拦截业务逻辑层
- 用途之一:打印请求参数和返回结果
# 数据库准备
- MySQL 8.0
- 本地数据库 & 云数据库 二选一
推荐开发和生产部署都用云数据库,本次会介绍 serverless 版数据库极大减少成本。
重点:专库专用,忌用 root
# 本地数据库准备
- 新增数据库 train
- 新增用户 train,配置权限只能操作 train 数据库
可利用【可视化工具】创建使用(Navicat, Dbeaver),sql 预览如下:
CREATE USER `train`@`localhost` IDENTIFIED WITH caching_sha2_password;
GRANT Alter, Alter Routine, Create, Create Routine, Create Temporary Tables, Create View, Delete, Drop, Event, Execute, Grant Option, Index, Insert, Lock Tables, References, Select, Show View, Trigger, Update ON `chenmeng\_train`.* TO `train`@`localhost`;
2
3
# 云数据库的好处
- 免去环境搭建,版本任选
- 方便多台电脑协作开发
- 相当于雇了一帮运维
- 总结:花钱买时间
# 购买云数据库 RDS
阿里云官方小店可以买各种阿里云的产品,而且价格会比【官方】的便宜,非常适合新人。且官网购买时,没有最低配置的选择项,所以推荐在【阿里云官方小店】购买云产品。
地址:https://partner.aliyun.com/shop/1704506477397431 (opens new window)
正常购买的情况
- 按年买比较划算,一年大概是四百多
学习专用,可以选择买 Serverless 版的 RDS
- 按量计费,首月免费
- 关闭连接后,会暂停计费
# Serverless 版的 RDS
购买 Serverless 版的 RDS
- 需配置白名单,设置可外网访问
- 配置专库专用
# 版本问题
为什么开发时,JDK 及各种框架都不建议用最新版本?
其中一个大原因,就是怕其依赖的第三方没有配套升级,导致不兼容。这也是为什么 JDK1.8 还是主流版本,因为本身稳,第三方框架也稳。
springboot 3 集成的 Mybatis 框架也必须是 3.0.0 版本或以上。
# Mybatis 生成器
注意规范
- 开发规范:生成器生成的 4 个文件都不能手动修改
- 自定义 SQL 需要写到自己的 mapper 里,不能放在生成的 mapper 里
- 生成器只能生成单表增删改查
# 自定义异常类
为了区分业务异常和系统异常,需要增加自定义异常类。
# 详解雪花算法
12 bit - 序列号:同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id
// todo
# 雪花算法问题
数据中心,机器 ID 怎么设置?
- 利用 redis 自增序列
- 利用数据库,为每台机器分配 workld,保存 ip 和 workld 的关系
时钟回拨
- 机器时间是 3 点,北京时间是 2 点
- 此时需要把机器时间调回 2 点,那么 2 点 ~ 3 点的 ID 会重新再生成一遍
# 特点
全局唯一,有序增长,生成效率高。
# 短信验证码登录流程
两种短信攻击方式:
- 同一个手机号不断发短信
- 用不同的手机号不断发短信
解决办法:
- 获取验证码之前,使用图形验证码防止短信攻击
图形验证码的制作:
- 应该返回图片,而不是验证码字符串
提升用户体验:
- 第一次登录不需要输入图片验证码,
- 如果第一次输入密码校验失败,就启用【图片验证码】
# 单点登录
# 两种单点登录实现方案
- redis + token
- token 是指一个无意义的,随机的字符串
- 后端校验用户名密码之后,生成 token 后放入 redis
- 前端将 token 放入 header(利用
store
) - 其他页面请求校验时,从 header 获取 token,然后根据 token 到 redis 获取数据进行判断,有数据则登录校验成功。(主要是看 token 是否已失效)
- jwt
- jwt 生成的 token 是有意义的
- 使用工具包来校验 token
# 操作细节
1、前端在用户登录之后保存登录信息
// 变量提交:保存登录信息, 即 token
store.commit("setMember", data.content);
2
2、修改 axios 全局拦截器
为请求 headers 增加 token,并返回配置
axios.interceptors.request.use(function (config) {
console.log('请求参数:', config);
const _token = store.state.member.token;
if (_token) {
config.headers.token = _token;
console.log("请求headers增加token:", _token);
}
return config;
}, error => {
return Promise.reject(error);
});
2
3
4
5
6
7
8
9
10
11
# JWT 单点登录原理与存在的问题及解决方案
Hutool 对 JWT 的介绍:概述 (hutool.cn) (opens new window)
存在的问题分析:
token 被解密
- 加盐值(密钥),每个项目的盐值不能一样,避免一个项目被破解,全部项目都被破解
token 被拿到第三方使用
简单来说就是: 自己的产品,被别人包了一个界面,做成他们收费的产品。(比如 ChatGPT 聊天机器人,外表包装成一个小程序,实际上是利用开发者设置的 token 去官网请求信息)
没啥好办法,只能使用限流(检测流量大的情况)
# 线程本地变量
方案:
- 首先,利用
拦截器
在接口入口处拦截请求; - 然后,在
拦截器
中获取会员信息,并放到线程本地变量
; - 最后,就可以在 controller、service 直接从线程本地变量获取会员信息来使用了。
其他方案:
- 可以在 controller 入口通过
request
获得 header,进而获得 token 及会员信息 - 但这种方案实现起来比较麻烦,需要引入使用
request
# 批量生成功能
批量生成的功能一定要考虑几个问题:
- 能不能支持重复生成?
- 事务
重复生成的两种做法
- 存在就跳过
- 先删除已有数据再插入(此项目用这个)
# 定时调度
# 为什么需要定时调度?
定时调度在企业级系统中非常重要,比如以下几个场景:
- 统计报表
- 功能补偿
- 不紧急的大批量任务
# 定时任务三大要素
- 执行的内容: 功能逻辑
- 执行的策略:
cron
表达式(从左到右,用空格隔开: 秒 分 小时 月份中的日期 月份 星期中的日期 年份 -- 7) - 开关: 开启定时任务
# 实现方案
SpringBoot 自带的定时任务和 quartz 的不同
# SpringBoot 自带的定时任务
- 适合小项目,快速实现定时任务
- 需要开启定时任务注解
@EnableScheduling
- 适合单体应用,不适合集群(增加分布式锁,可解决集群问题)
- 无法实时更改定时任务状态和策略
# quartz
可并发执行
可加注解禁止并发执行:
@DisallowConcurrentExecution
多节点场景下会轮询执行定时任务
并发执行:上一周期还没执行完,下一周期又开始了
# 时间字段问题
两个注解:@DateTimeFormat
和 @JsonFormat
注意:
GET
请求中,请求体的时间字段不能用@JsonFormat
,需要使用@DateTimeFormat
- GET 请求的日期是拼接在 URL 里的,需要用 spring 自带的
@DateTimeFormat(pattern="yyyy-MM-dd")
,后端才能接收到参数。
# 视图和存储过程
视图和存储过程:在以前的项目中,经常用到,因为依数据库内部的算力,就可以帮我们做很多复杂的功能,但因为会占用较多的数据库资源,所以逐渐淘汰了,服务端资源不够可以加机器,数据库就不好加了,所以数据库资源很珍贵,这也是为什么会有数据缓存。
视图依赖于计算,存储过程不好维护!!!
# 超卖问题
需配合 Jmeter 压力测试
# 1、关键字 synchronized
直接用关键字 synchronized
给方法体加锁,会有两个问题:
- 锁的粗粒度太大,会导致售卖效率不高(可以容忍);
- 在多节点(多台服务器)的情况下,还是会出现【超卖】(不能容忍)
可以用 Mysql 数据库来加锁,但是性能不高。
# 2、redis 实现分布式锁
使用类型:String
类型
setIfAbsent
就是对应 redis 的 setnx
// 获取分布式锁
String lockKey = RedisKeyPreEnum.CONFIRM_ORDER + "-" + DateUtil.formatDate(dto.getDate()) + "-" + dto.getTrainCode();
// setIfAbsent就是对应redis的setnx
Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
2
3
4
分布式锁常见的问题:
(1)没拿到锁的线程把别人的锁给删了。
如果加锁的动作放在
try
里,释放锁的操作放在finally
,并且用户没拿到锁之后抛出异常,finally
里面依旧释放锁,就会到导致 没拿到锁的线程把别人的锁给删了 的问题。
把释放锁移出 finally
不是最优解,解法有以下两种
- 加锁的动作不要放在
try
里 - 加锁时,将当前线程 ID 放到锁对应的
value
中,删除时,先去获取value
,比对value
值和当前线程 ID 一致才能删除
(2)过期时间短导致锁失效,两个线程一起执行任务,最后超卖
解决方案有:“看门狗”
假设过期时间为 60 秒,当倒计时到 30 秒的时候,自动刷新过期时间,所以永远不会出现超时问题。
如果主线程出现异常,锁的过期时间会一直刷新吗?
不会。这是使用守护线程的好处。
使用守护线程的好处是:会随主线程的结束而结束,所以不会出现一直重置成 60 秒,永不过期的问题。
使用守护线程的方式不需要手写,可以集成
Redisson
。
# 3、使用 Redisson 看门狗解决锁超时的问题
Redisson
可以实现分布式锁,并且自带 “看门狗” 方案。(项目中比较常用)
(3)使用 Redisson 解决了【超时】的问题,但是会有【redis宕机】的问题。
在 redis 集群中,redis 宕机 会导致锁互斥失效的问题。
比如,线程 a 拿到了锁,然后执行任务。突然 redis 宕机了,然后 redis 集群重新选出了主节点,线程 b 此时发起请求拿到了锁,这就导致了锁互斥失效的问题。
解决方案:使用 redis 红锁(实际项目中不常用)
红锁的获取方式是:
- 假设现在有 5 台机器,那么线程 1 需要获取 ceil(5/2) = 3 把锁才算获取锁成功。这样就即使有某台 redis 宕机了,其他线程也获取不了锁了,从而解决 redis 宕机问题。
- 获取的时候还需按顺序获取,以避免造成死锁问题。
- 锁宕机之后,机器不能马上重启。假设超时时间为 5s 钟,则重启时间至少也为 5s 钟,获取锁的时间也不能超过 5s 钟。
**当锁宕机后,机器不能马上重启或者切换主备。**这是因为锁可能处于一种不确定的状态,如果机器在锁未释放的情况下重启,可能会导致数据不一致或其他问题。
假设超时时间为 5s 钟,则重启时间至少也为 5s 钟,这是为了确保在重启之前,锁有足够的时间超时释放。如果重启时间太短,可能会导致锁在机器重启后仍然被占用,从而导致其他线程无法获取锁。
获取锁的时间也不能超过 5s 钟,这是为了避免线程在获取锁时出现长时间的阻塞。如果获取锁的时间太长,可能会导致其他线程长时间等待,从而影响系统的性能和响应时间。
可阅读:如何解决集群情况下分布式锁的可靠性 | cmty256.github.io (opens new window)
(4)使用红锁,可能会带来以下问题:
- 性能问题:红锁需要在多个 Redis 节点上进行通信和协调,这可能会导致性能下降,特别是在高并发环境下。
- 复杂性增加:红锁的实现相对复杂,需要处理多个节点之间的通信、故障检测和恢复等问题,增加了系统的复杂性。
- 数据不一致性:在分布式环境中,由于网络延迟、节点故障等原因,可能会导致锁的获取和释放出现不一致的情况,从而影响系统的正确性。
- 单点故障:如果 Redis 节点出现故障,可能会导致整个分布式锁系统不可用,从而影响系统的可用性。
- 成本问题:使用 Redis 分布式锁需要额外的 Redis 节点和网络资源,增加了系统的成本。
# 机器人刷票问题
使用 令牌大闸 防止机器人抢票
- 分布式锁和限流都不能解决机器人刷票的问题,1000 个请求抢票,900 个限流快速失败,另外 100 个有可能是同一个人在刷库
- 没有余票时,需要查库存才能知道没票,会影响性能,不如查令牌余量来得快®
# MQ 的使用
项目中的用途:
- 使用 MQ,将购票流程一分为二
- 增加排队购票功能
踩坑小记
实现 RocketMQ 发送,
spring.factories 功能在 Spring Boot 3 会被移除(即使依赖包内的自动配置失效),需要手动引入配置
解决方案:
1、resources 下 创建目录文件
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
2、文件内容填写
org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration
非必要的时候,可以通过开启 SpringBoot 异步注解(
Async
)来异步执行方法。
# 事务问题
- 尽量做短事务,不要做常事务,否则会大量占用 数据库资源
- 本类方法间的调用,事务不生效
# 微服务组件
# 网关模块
# 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2
3
4
注意:
- gateway 只有一个依赖,不能引入 common,也不能入 starter-web
- 网关是基于 netty 的,所以不需要引入 starter-web
# 配置路由转发
生产发布时,只有 gateway 需要配置【外网IP】,其它模块都只开放【内网】访问,【外网】访问不了,保证应用安全。
# 输出请求日志
要输出请求日志需要增加启动参数(VM选项):
-Dreactor.netty.http.server.accessLogEnabled=true
日志输出:
15:07.087 INFO r.netty.http.server.AccessLog :279 reactor-http-nio-2 127.0.0.1 - - [03/12月/2023:21:15:06 +0800] "GET /member/hello HTTP/1.1" 200 12 243
# Fegin
Feign
是 Netflix 公司开发的一个声明式的 REST 调用客户端,SpringCloud 的早期,就是将各种第三方组件,整合到 SpringBoot 项日里,形成了 SpringCloud,现在慢慢的把第三方组件替换成自研的组件,比如 gateway 组件OpenFeign
是在 Feign 的基础上,增加 SpringMVC 注解,让代码写起来像在写 Controller
使用
spring.application.name
可以给各应用起一个名字,方便应用之间互相认识,在注册中心、配置中心、路由、服务调用、限流等微服务组件中,都会用到。
# 注册中心与配置中心
注册中心和配置中心可以用不同的组件
- 注册中心:通讯录,让应用之间相互认识
- 健康检查(可以主动的让某一节点下线,常用于发布前下线)
- 路由转发(gateway 也可以根据 ip 进行路由转发,但是后期为了控制成本,会对机器做动态扩容,此时 ip 就不固定了)
- 远程调用(按名字调用)
- 配置中心:动态修改线上配置
- 开关
- 域值
- 枚举项
一般 application.yml
用来放 SpringBoot 的配置;bootstrap.yml
用来放 SpringCloud 的配置。
命名空间 -- 可用作项目隔离
集群 -- 平时生产发布时,可以对某一节点做下线处理,再重新部署;按应用名(注册中心名)来做负载均衡路由转发(相同应用名,会轮询访问)
即使 nacos 挂掉,网关一样能够根据应用名进行路由转发
# Seata 分布式事务
官网首页:Apache Seata (opens new window)
快速开始 | Spring Cloud Alibaba (spring-cloud-alibaba-group.github.io) (opens new window)
# 四种模式
- AT 模式,默认,简单,需要增加 undo_log 表,生成反向 SQL,性能高
- 回滚后,原来没数据的,现在还是没数据
- TCC 模式,try、confirm/cancel,三个阶段的代码都得自己实现,Seata 只负责调度
- 对业务代码侵入性较强,必要时可能还要修改数据库
- SAGA 模式,长事务解决方案,需要程序员自己编写两阶段代码(AT 模式不需要写第二阶段)
- 基于状态机来实现的,需要一个 JSON 文件,可异步执行
- XA 模式,XA 协议是由 X/Open 组织提出的分布式事务处理规范,基于数据库的 XA 协议来实现 2PC 又称为 XA 方案,适用于强一致性的场景,比如金融、银行等
- 需要数据库本身支持XA协议,可以跨数据库
# AT 模式
seata 的 AT 模式会自动生成反向 sql,且没有反引号
``
,所以要求表里不能有关键字。AT 模式会有个全局锁,用于防止脏读,线程 1 的事务修改了库存,但还没提交事务,线程 2 读库存时,读的还是原来的库存。
# Sentinel 降级
- Sentinel 限流降级
- Sentinel 熔断降级
一句话理解:限流是做在被调用方,熔断是做在调用方
官方文档:introduction | Sentinel (sentinelguard.io) (opens new window)
# 常见的限流算法
- 静态窗口限流
- 动态窗口限流
- 漏桶限流
- 令牌桶限流
- 令牌大闸
例如: 当前是第 2.5 秒
静态: 统计第 2 秒到现在的请求数
动态: 统计第 1.5 秒到现在的请求数
# 流控效果
- 快速失败
- 即降级
- Warm Up(预热)
- coldFactor 为 3,即请求 QPS 从(阈值/3)开始,*经多少预热时长才逐渐升至设定的QPS阈值?*比如阈值是 100,时长是 10 秒,则从 33 开始经过 10 秒上升到 100。
- 排队等待
- 适合短时高峰,可加个超时时间,慢慢消费掉请求
# 流控模式
- 直接
- 关联
- 对目标的限流是有条件的,需要关联的资源限流时,目标才会限流
- 两个关联的资源必须同时启动才能生效
- 链路
配置
入口资源
的时候,需要在资源名称前面加个/
# spring cloud alibaba
# 常见的缓存有哪些?
- 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 失效后,导致大量请求直接访问数据库。
解决方法:
- 写一个定时任务,定时刷新缓存。(可避免缓存超时失效问题,但缓存机器坏了就不能解决了)
- 针对数据库查询,增加分布式锁,100 个请求,只有一个拿到锁,去查数据库,其它 99 个请求都快速失败,告诉用户稍候重试 -- 相当于查询失败,不能让请求一直等待,会占用大量资源。(可解决缓存机器坏了的情况)
**缓存穿透:**去缓存里取数据,因数据库本身就没数据而造成缓存穿透。
解决方法:
- 给缓存设置空值,并给数据库查询加分布式锁
**缓存雪崩:**由于短时间内,大量的 key 失效导致数据库压力剧增。
# 缓存在高并发场景中的生产问题分享
**场景:**每天的会员数很多,百万级别,但每个会员一天只会有几次请求,一个会员信息在同一次请求中,会被用到多次,且会员信息较大
**问题 1:**多次调用查询会员方法,组装信息多,多次访问数据库
*解决:*使用本地缓存,1 分钟有效(请求不多且不频繁,没必要使用第三方组件(Redis),且会增加 io 消耗)
**问题 2:**fullgc(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下如何查看某个端口被谁占用 | 菜鸟教程 (runoob.com) (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
# 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.js
目录下的 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
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 个额度