欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 社会 > 许苑刷题阁笔记汇总

许苑刷题阁笔记汇总

2024/12/22 22:09:20 来源:https://blog.csdn.net/a147775/article/details/144302349  浏览:    关键词:许苑刷题阁笔记汇总

许苑刷题阁笔记汇总

  • 许苑刷题阁笔记汇总
    • 1 - 后端基础开发
      • 一、需求分析
        • 项目功能梳理
        • 需求优先级分析
      • 二、库表设计
        • 1、用户表
        • 2、题库表
        • 核心设计
        • 扩展设计
        • 3、题目表
        • 扩展设计
        • 4、题库题目关系表
        • 核心设计
      • 三、后端基础开发 - 增删改
        • 1、数据访问层代码生成
        • 2、业务逻辑代码生成
        • 3、数据模型开发
        • 4、接口开发
        • 5、服务开发
        • 6、Swagger 接口文档测试
      • 四、后端核心业务开发
        • 1、确认和完善接口
        • 2、根据题库查询列表接口实现
    • 2 - 用户功能扩展
      • 一、用户刷题记录日历
        • 需求分析:
        • 方案设计
      • 二、后端开发
      • 三、分词题目搜索
        • 需求分析
        • 方案设计
        • Elasticsearch 入门
          • **1、什么是Elasticsearch?**
          • **2、Elasticsearch 生态**
          • **3、Elasticsearch 的核心概念**
          • **4、Elasticsearch 实现全文检索的原理**
          • **5、Elasticsearch 客户端**
          • 6、ES数据同步方案
      • 四、后端开发(ES 实战)
          • **1、环境搭建**
          • **2、设计 ES 索引**
          • **3、新建 ES 索引**
          • **4、引入 ES 客户端**
          • **5、编写 ES Dao 层**
          • 6、向ES全量写入数据
          • 7、开发 ES 搜索
          • 8、数据同步
          • 扩展
    • 3 - 管理能力扩展
      • 一、题目批量处理
        • 需求分析
          • 后端设计
        • 基础后端开发
          • 1、批量向题库添加题目
      • 二、批处理操作优化
          • 一、**健壮性**
          • 二、**稳定性**
          • 三、**性能优化**
          • 四、SQL优化
          • 五、异步任务优化
          • 六、数据库连接池调优 ! !
            • **引入 Druid 连接池**
            • 使用 Druid 监控
          • 七、**数据一致性**
          • 八、**可观测性**
      • 三、自动缓存热门题库
        • 需求分析
        • 方案设计
        • hotkey入门
        • 核心组件
        • 后端开发(hotkey 实战)
          • 1、安装 Etcd
          • 2、安装 hotkey worker
          • 3、启动 hotkey 控制台
          • 4、引入 hotkey client
          • 5、了解开发模式
          • 6、配置 hotkey 规则
          • 7、项目应用 hotkey
          • 8、测试验证
        • 扩展知识
        • 扩展
    • 4 - 流量安全优化
      • 重点掌握
      • 一、网站流量控制和熔断
          • 1、流量控制
          • 2、熔断机制
          • 3、降级机制
      • 二、Sentinel 入门
          • 1、核心概念
          • 3、Sentinel 入门 Demo
          • 4、下载并启动 Sentinel 控制台
          • 5、规则管理和推送
          • 6、整合 Spring Boot
          • 7、开发模式
          • 8、其他特性
      • 三、后端开发(Sentinel 实战)
        • 1、查看题库列表接口限流熔断
      • 四、动态 IP 黑名单过滤
        • 需求分析
        • 方案设计
          • 1、 设计过程
          • 2、最终方案
        • 配置中心
          • 配置中心支持的功能
        • Nacos 入门
          • Nacos 配置管理的核心概念
          • 推送和监听
        • 后端开发
        • 扩展
    • 5 - 内容安全优化
      • 重点
      • 一、同端登录冲突检测
        • 需求分析
        • 方案设计
          • 1、账号冲突检测策略
        • 2、实现思路
      • 二、Sa-Token 入门
        • 后端开发(Sa-Token 实战)
          • 1、引入 Sa-Token
          • 2、配置 Sa-Token
          • 7、改造获取用户信息的逻辑
        • 扩展知识
          • 1、Sa-Token 登录异常捕获
          • 2、Sa-Token 集成 Redis
      • 三、分级题目反爬虫策略
        • 方案设计
          • 反爬虫的手段
          • 多级反爬虫策略
          • 统计访问频率 - 结合已有系统
          • 统计访问频率 - 基于本地计数器(单机)
          • 统计访问频率 - 基于 Redis 统计(分布式)
      • 四、后端开发
        • 1、通用计数器
        • 扩展
          • 1、配置动态化
          • 2、策略模式
          • 3、优化频率统计(只有思路但是没有很好的实现)
          • 4、提升检测爬虫的易用性
          • 2、Sa-Token 集成 Redis
      • 三、分级题目反爬虫策略
        • 方案设计
          • 反爬虫的手段
          • 多级反爬虫策略
          • 统计访问频率 - 结合已有系统
          • 统计访问频率 - 基于本地计数器(单机)
          • 统计访问频率 - 基于 Redis 统计(分布式)
      • 四、后端开发
        • 1、通用计数器
        • 扩展
          • 1、配置动态化
          • 2、策略模式
          • 3、优化频率统计(只有思路但是没有很好的实现)
          • 4、提升检测爬虫的易用性

许苑刷题阁笔记汇总

1 - 后端基础开发

一、需求分析

项目功能梳理

基础功能

  • 用户模块

    • 用户注册
    • 用户的登录
    • 【管理员】 - 增删改查
  • 题库模块

    • 查看题库列表
    • 查看题库详情
    • 【管理员】管理题库 - 增删改查
  • 题目模块

    • 题目搜索
    • 查看题目详情(进入刷题页面)
    • 【管理员】管理题目 - 增删改查(添加题目到所属题库,修改题目所属题库等)

高级功能

  • 题目批量管理

    • 【管理员】批量增删题目
  • 分词题目搜索

  • 用户刷题记录日历图

  • 自动缓存热门题目

  • 网站流量控制和熔断

  • 动态 IP 黑白名单过滤

  • 同端登录冲突检测

  • 分级题目反爬虫策略

需求优先级分析
  • P0 为核心,非做不可
  • P1 为重点功能,最好做
  • P2 为实用功能,有空就做
  • P3 可做可不做,时间充裕再做

基础功能(均为P0)

二、库表设计

1、用户表
 editTime     datetime     default CURRENT_TIMESTAMP not null comment '编辑时间',createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',

其中编辑时间需要业务处理,表中任意一个字段更新后都会触更新时间字段刷新时间

2、题库表
核心设计

题库表的核心是题库信息(标题、描述、图片),SQL 如下:

-- 题库表
create table if not exists question_bank
(id          bigint auto_increment comment 'id' primary key,title       varchar(256)                       null comment '标题',description text                               null comment '描述',picture     varchar(2048)                      null comment '图片',userId      bigint                             not null comment '创建用户 id',editTime    datetime default CURRENT_TIMESTAMP not null comment '编辑时间',createTime  datetime default CURRENT_TIMESTAMP not null comment '创建时间',updateTime  datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',isDelete    tinyint  default 0                 not null comment '是否删除',index idx_title (title)
) comment '题库' collate = utf8mb4_unicode_ci;

其中,picture 存储的是图片的 url 地址,而不是完整图片文件。通过 userId 和用户表关联,在本项目中只有管理员才能创建题库。

由于用户很可能按照标题搜索题库,所以给 title 字段增加索引。

扩展设计

1)如果要实现题库审核功能,可以对表进行如下扩展:

  1. 新增审核状态字段,用枚举值表示待审核、通过和拒绝
  2. 新增审核信息字段,用于记录未过审的原因等
  3. 新增审核人 id 字段,便于审计操作。比如出现了违规内容过审的情况,可以追责到审核人。
  4. 新增审核时间字段,也是便于审计。
3、题目表

题目表的核心是题目信息(标题、详细内容、标签),SQL 如下:

-- 题目表
create table if not exists question
(id         bigint auto_increment comment 'id' primary key,title      varchar(256)                       null comment '标题',content    text                               null comment '内容',tags       varchar(1024)                      null comment '标签列表(json 数组)',answer     text                               null comment '推荐答案',userId     bigint                             not null comment '创建用户 id',editTime   datetime default CURRENT_TIMESTAMP not null comment '编辑时间',createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',isDelete   tinyint  default 0                 not null comment '是否删除',index idx_title (title),index idx_userId (userId)
) comment '题目' collate = utf8mb4_unicode_ci;

几个重点:

  1. 题目标题 title 和题目创建人 userId 是常用的题目搜索条件,所以添加索引提升查询性能。
  2. 题目可能有多个标签,为了简化设计,没有采用关联表,而是以 JSON 数组字符串的方式存储,比如 ["Java", "Python"]
  3. 题目内容(详情)和题目答案可能很长,所以使用 text 类型。
扩展设计

题目表有很多可以扩展的方法,下面举一些例子。

1)如果要实现题目审核功能,可以参考上述题库审核功能,新增 4 个字段即可:

reviewStatus  int      default 0  not null comment '状态:0-待审核, 1-通过, 2-拒绝',
reviewMessage varchar(512)        null comment '审核信息',
reviewerId    bigint              null comment '审核人 id',
reviewTime    datetime            null comment '审核时间'

2)可能有很多评价题目的指标,比如浏览数、点赞数、收藏数,参考字段如下:

viewNum       int      default 0    not null comment '浏览量',
thumbNum      int      default 0    not null comment '点赞数',
favourNum     int      default 0    not null comment '收藏数'

3)如果要实现题目排序、精选和置顶功能,可以参考上述题库表的设计,新增整型的优先级字段,并且根据该字段排序。对应的 SQL 如下:1692192726054969345_0.5078607557302357

sql复制代码priority  int  default 0  not null comment '优先级'

4)如果题目是从其他网站或途径获取到的,担心有版权风险,可以增加题目来源字段。最简单的实现方式就是直接存来源名称:

sql复制代码source   varchar(512)  null comment '题目来源'

5)如果想设置部分题目仅会员可见,可以给题目表加上一个 “是否仅会员可见” 的字段,本质上是个布尔类型,用 1 表示仅会员可见。参考 SQL 如下:

sql复制代码needVip  tinyint  default 0  not null comment '仅会员可见(1 表示仅会员可见)'
4、题库题目关系表
核心设计

由于一个题库可以有多个题目,一个题目可以属于多个题库,所以需要关联表来实现。

三、后端基础开发 - 增删改

1、数据访问层代码生成
2、业务逻辑代码生成

​ 使用万能模板的代码生成器工具生成代码,主要包括三层模型和实现类、数据模型包装类

3、数据模型开发
  • 编写对应的数据模型包装类(DTO包的请求类和VO包的视图类 -> 封装好放回给前端的类)以及一些枚举类

💡 小技巧:可以结合具体的业务、实体类(比如 Question)、以及创建表的 DDL 语句去编写请求包装类。

4、接口开发

修改生成的 Controller 接口,不需要包含业务逻辑,处理字段不一致的问题、并且将无用的接口移除掉,能跑通就行。

生成的 Controller 接口结构都是一致的,只需要理解一个 Controller,其他的都很简单。1692192726054969345_0.5098553001075015

5、服务开发

修改生成的 Service 接口和实现类,处理字段不一致的问题,略微调整数据校验、查询条件等代码,能跑通就行。

生成的 Service 结构都是一致的,只需要理解一个,其他的都很简单。

6、Swagger 接口文档测试

运行项目,通过访问 Swagger 接口文档来测试增删改查接口能否正常执行。

地址:http://localhost:8101/api/doc.html#/home

四、后端核心业务开发

1、确认和完善接口
2、根据题库查询列表接口实现

需求:根据题库Id查询题目列表

分析:由于同一个题库内的题目不会很多,所以直接根据题库Id获取全部的题目。

2 - 用户功能扩展

一、用户刷题记录日历

需求分析:
  1. 用户首次流量题目,记录为签到。
  2. 用户同时可以在页面查看自己在某个年份的刷题签到记录
方案设计

设计一张签到表,记录用户每次的签到信息

示例表结构如下:

CREATE TABLE user_sign_in (id BIGINT AUTO_INCREMENT PRIMARY KEY,  -- 主键,自动递增userId BIGINT NOT NULL,               -- 用户ID,关联用户表signDate DATE NOT NULL,            -- 签到日期createdTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- 记录创建时间UNIQUE KEY uq_user_date (userId, signDate)  -- 用户ID和签到日期的唯一性约束
);

通过唯一索引,可以确保同一用户在同一天内只能签到一次。

优点:原理简单

缺点:随着用户量和数据量增大,对数据库的压力增大,直接查询数据库性能较差。除了单接口的响应会增加,可能整个系统都会被其拖垮。

💡 试想一下,每天 1 万个用户签到,1 个月就 30 万条数据,3 个月就接近百万的数据量了,占用硬盘空间大概 50 MB。存储 100 万个用户 365 天的签到记录,需要 17.52 GB 左右。

后端方案 - 基于缓存 Redis Set

具体示例如下,可以使用 Redis 命令行工具添加值到集合中:

SADD user:signins:123 "2024-09-01"
SADD user:signins:123 "2024-09-02"

后端方案 - Bitmap 位图

当然,我们没必要自己通过 int 等类型实现 Bitmap,JDK 自带了 BitSet 类、Redis 也支持 Bitmap 高级数据结构。考虑到项目的分布式、可扩展性,采用 Redis 的 Bitmap 实现。

Redis Key的设计:user:signins:{年份}:{userId}

设置某一个bit值的命令

设置某一个 bit 值的命令如下:

-- 表示用户在第 240 天打卡
SETBIT user:signins:2024:123 240 1
-- 表示用户在第 241 天打卡
SETBIT user:signins:2024:123 241 1

查询某一个 bit 值的命令:

GETBIT user:signins:2024:123 240

在 Java 程序中,还可以使用 Redisson 库提供的现成的 RBitSet,开发成本也很低。

这种方案的优点:内存占用极小,适合大规模用户和日期的场景

二、后端开发

需要开发两个接口:

  1. 添加刷题签到记录
  2. 查询刷题签到记录

1、集成redisson使用

1)引入依赖

2)配置客户端配置类

1)在 pom.xml 文件中引入 Redisson:

▼xml复制代码<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.21.0</version>
</dependency>

2)在 config 目录下编写 Redisson 客户端配置类,会自动读取项目中的 Redis 配置,初始化客户端 Bean。代码如下:

▼java复制代码@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {private String host;private Integer port;private Integer database;private String password;@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://" + host + ":" + port).setDatabase(database).setPassword(password);return Redisson.create(config);}
}

3)项目的 yml 配置文件中补充 Redis 配置,没有密码就可以注释掉:

▼java复制代码# Redis 配置
spring:redis:database: 0host: xxxxport: xxxtimeout: 2000password: xxx

然后尝试启动项目。如果用的是编程导航的万用后端模板,记得取消启动类对 Redis 的移除

实现优化功能

三、分词题目搜索

需求分析

用户能够更灵活地搜索出题目,分词搜索

方案设计
Elasticsearch 入门

使用Elasticsearch实现题目数据的存储和分词搜索,需要将数据库的数据同步到Elasticsearch

1、什么是Elasticsearch?

Elasticsearch 是一个分布式的开源搜索引擎和分析引擎,它主要用于解决搜索难题、数据分析以及实时文档检索,广泛用于全文检索、日志分析、监控数据分析等场景。

2、Elasticsearch 生态

Elasticsearch 生态系统非常丰富,包含了一系列工具和功能,帮助用户处理、分析和可视化数据,Elastic Stack 是其核心组成部分。

Elastic Stack(也称为 ELK Stack)由以下几部分组成:

  • Elasticsearch:核心搜索引擎,负责存储、索引和搜索数据。
  • Kibana:可视化平台,用于查询、分析和展示 Elasticsearch 中的数据。
  • Logstash:数据处理管道,负责数据收集、过滤、增强和传输到 Elasticsearch。
  • Beats:轻量级的数据传输工具,收集和发送数据到 Logstash 或 Elasticsearch。

Kibana 是 Elastic Stack 的可视化组件,允许用户通过图表、地图和仪表盘来展示存储在 Elasticsearch 中的数据。它提供了简单的查询接口、数据分析和实时监控功能。

3、Elasticsearch 的核心概念
  • 索引(Index):类似于数据库中的表。

  • 文档(Document):索引中的每条记录,类似于数据库中的行。文档以JSON格式存储。

  • 字段(Field):文档中的每个键值对,类似于数据库中的列。

  • 映射(Mapping):用于定义文档字段的数据类型以及其处理方式,类似于表结构。

  • 集群(Cluster):多个节点组成的群集,用于存储数据并提供搜索功能。集群中的每个节点都可以处理数据。

  • 分片(Shard):为了实现横向扩展,ES 将索引拆分成多个分片,每个分片可以分布在不同节点上。

  • 副本(Replica):分片的复制品,用于提高可用性和容错性。

    和数据库类比:

    Elasticsearch 概念关系型数据库类比
    IndexTable
    DocumentRow
    FieldColumn
    MappingSchema
    ShardPartition
    ReplicaBackup
4、Elasticsearch 实现全文检索的原理
  1. 分词
  2. 倒排索引(根据分词去词表查询文档Id,然后再根据文档Id去查询文档)

正向和倒排索引的区别:

  • 正向是根据文档找词条,倒排是根据词条找文档。
5、Elasticsearch 客户端
6、ES数据同步方案

一般情况下,如果做查询搜索功能,使用 ES 来模糊搜索,但是数据是存放在数据库 MySQL 里的,所以说我们需要把 MySQL 中的数据和 ES 进行同步,保证数据一致(以 MySQL 为主)。

数据流向:MySQL => ES (单向)

数据同步一般有 2 个过程:全量同步(首次)+ 增量同步(新数据)

总共有 4 种主流方案:


1)定时任务

比如 1 分钟 1 次,找到 MySQL 中过去几分钟内(至少是定时周期的 2 倍)发生改变的数据,然后更新到 ES。

优点:

  • 简单易懂,开发、部署、维护相对容易。
  • 占用资源少,不需要引入复杂的第三方中间件。
  • 不用处理复杂的并发和实时性问题。

缺点:

  • 有时间差:无法做到实时同步,数据存在滞后。
  • 数据频繁变化时,无法确保数据完全同步,容易出现错过更新的情况。
  • 对大数据量的更新处理不够高效,可能会引入重复更新逻辑。

应用场景:

  • 数据实时性要求不高:适合数据短时间内不同步不会带来重大影响的场景。
  • 数据基本不发生修改:适合数据几乎不修改、修改不频繁的场景。
  • 数据容忍丢失

2)双写

3)用 Logstash 数据同步管道

4)监听 MySQL Binlog

四、后端开发(ES 实战)

1、环境搭建
2、设计 ES 索引
3、新建 ES 索引

可以通过如下命令创建索引,在 Kibana 开发者工具中执行、或者用 CURL 调用 Elasticsearch 执行均可:

PUT /question_v1
{"mappings": {"properties": {...}}
}

但是有一点要注意,推荐在创建索引时添加 alias(别名) ,因为它提供了灵活性和简化索引管理的能力。具体原因如下:

  1. 零停机切换索引:在更新索引或重新索引数据时,你可以创建一个新索引并使用 alias 切换到新索引,而不需要修改客户端查询代码,避免停机或中断服务。
  2. 简化查询:通过 alias,可以使用一个统一的名称进行查询,而不需要记住具体的索引名称(尤其当索引有版本号或时间戳时)。
  3. 索引分组:alias 可以指向多个索引,方便对多个索引进行联合查询,例如用于跨时间段的日志查询或数据归档。
4、引入 ES 客户端

在 Spring Boot 项目中,可以通过 Starter 快速引入 Elasticsearch,非常方便:

1)在 pom.xml 中引入:

▼xml复制代码<!-- elasticsearch-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

2)修改项目 yml 配置:

spring:elasticsearch:uris: http://xxx:9200# 没有username password不需要填写username: elasticpassword: coder_yupi_swag

3)使用 Spring Data Elasticsearch 提供的 Bean 即可操作 Elasticsearch,可以直接通过 @Resource 注解引入:

@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;

了解 Spring Data Elasticsearch 操作 ES 的方法:

  1. 构造一个 Query 对象,比如插入数据使用 IndexQuery,更新数据使用 UpdateQuery
  2. 调用 elasticsearchRestTemplate 的增删改查方法,传入 Query 对象和要操作的索引作为参数
  3. 对返回值进行处理

示例代码如下:

▼java复制代码Map<String, Object> updates = new HashMap<>();
updates.put("title", "Updated Elasticsearch Title");
updates.put("updateTime", "2023-09-01 10:30:00");UpdateQuery updateQuery = UpdateQuery.builder(documentId).withDocument(Document.from(updates)).build();elasticsearchRestTemplate.update(updateQuery, IndexCoordinates.of(INDEX_NAME));Map<String, Object> updatedDocument = elasticsearchRestTemplate.get(documentId, Map.class, IndexCoordinates.of(INDEX_NAME));

但是有个问题,我们上述代码都是用 Map 来传递数据。记得之前使用 MyBatis 操作数据库的时候,都要定义一个数据库实体类,然后把参数传给这个实体类的对象就可以了,会更方便和规范。

没错,Spring Data Elasticsearch 也是支持这种标准 Dao 层开发方式的,下面就来使用一下

5、编写 ES Dao 层

2)定义 Dao 层

可以在 esdao 包中统一存放对 Elasticsearch 的操作,只需要继承 ElasticsearchRepository 类即可。

代码如下:

▼java复制代码/*** 题目 ES 操作*/
public interface QuestionEsDao extends ElasticsearchRepository<QuestionEsDTO, Long> {}

ElasticsearchRepository类为我们提供了大量现成的CRUD操作

目前我们学到了 2 种 Spring Data Elasticsearch 的使用方法,应该如何选择呢?

  • 第 1 种方式:Spring 默认给我们提供的操作 es 的客户端对象 ElasticsearchRestTemplate,也提供了增删改查,它的增删改查更灵活,适用于更复杂的操作,返回结果更完整,但需要自己解析。
  • 第 2 种方式:ElasticsearchRepository<Entity, IdType>,默认提供了更简单易用的增删改查,返回结果也更直接。适用于可预期的、相对简单的操作
6、向ES全量写入数据

可以通过编写单次执行的任务,将 MySQL 中题目表的数据,全量写入到 Elasticsearch。

可以通过实现 CommandLineRunner 接口定义单次任务,也可以编写一个仅管理员可用的接口,根据需要选择就好。

同时可以使用注解方法实现@Poutcat实现初始化bean时候只加载一次

filter 更侧重于高效的过滤,而不影响评分。

7、开发 ES 搜索
8、数据同步

根据之前

扩展

1、ES接口支持降级

需求:ES挂了、或则未搭建ES环境时,照样能把项目跑起来。

思路:ES如果查询报错,该位数据库查询

3 - 管理能力扩展

一、题目批量处理

需求分析
  • 管理员批量向题库增删题目和移除题目
后端设计

通过循环依次调用数据库完成操作。注意,由于是批量操作,需要事务支持,有任何失败都会抛出异常并回滚。而且需要避免长事务

基础后端开发
1、批量向题库添加题目

添加前校验题目和题库是否存在,只添加合法题目:

二、批处理操作优化

一般情况下,外卖可以从以下多个角度对批处理任务进行优化。

  • 健壮性
  • 稳定性
  • 性能
  • 数据一致性
  • 可观测性
一、健壮性
  1. 参数提前校验
  2. 异常处理
二、稳定性
  1. 避免长事务问题
  2. 重试
  3. 中断恢复
三、性能优化

批量操作,防止多次交互导致性能下降

四、SQL优化

并发编程:利用并发包中的 CompletableFuture + 线程池 来并发处理多个任务。

五、异步任务优化
六、数据库连接池调优 ! !

数据连接池是用于管理与数据库之间连接的资源池,他能够 复用 现有的数据库连接,而不是每次请求都新建和销毁连接,从而提升系统的性能和响应速度。

引入 Druid 连接池

可以参考 官方文档 引入(虽然也没什么好参考的)。

1)通过 Maven 引入 Druid,并且排除默认引入的 HikariCP:1692192726054969345_0.07771421202239193

▼xml复制代码<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.23</version>
</dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version><exclusions><!-- 排除默认的 HikariCP --><exclusion><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId></exclusion></exclusions>
</dependency>

2)修改 application.yml 文件配置。

由于参数较多,建议直接拷贝以下配置即可,部分参数可以根据注释自行调整:1692192726054969345_0.7653976684104309

▼yaml复制代码spring:# 数据源配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mianshiyausername: rootpassword: 123456# 指定数据源类型type: com.alibaba.druid.pool.DruidDataSource# Druid 配置druid:# 配置初始化大小、最小、最大initial-size: 10minIdle: 10max-active: 10# 配置获取连接等待超时的时间(单位:毫秒)max-wait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒time-between-eviction-runs-millis: 2000# 配置一个连接在池中最小生存的时间,单位是毫秒min-evictable-idle-time-millis: 600000max-evictable-idle-time-millis: 900000# 用来测试连接是否可用的SQL语句,默认值每种数据库都不相同,这是mysqlvalidationQuery: select 1# 应用向连接池申请连接,并且testOnBorrow为false时,连接池将会判断连接是否处于空闲状态,如果是,则验证这条连接是否可用testWhileIdle: true# 如果为true,默认是false,应用向连接池申请连接时,连接池会判断这条连接是否是可用的testOnBorrow: false# 如果为true(默认false),当应用使用完连接,连接池回收连接的时候会判断该连接是否还可用testOnReturn: false# 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oraclepoolPreparedStatements: true# 要启用PSCache,必须配置大于0,当大于0时, poolPreparedStatements自动触发修改为true,# 在Druid中,不会存在Oracle下PSCache占用内存过多的问题,# 可以把这个数值配置大一些,比如说100maxOpenPreparedStatements: 20# 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作keepAlive: true# Spring 监控,利用aop 对指定接口的执行时间,jdbc数进行记录aop-patterns: "com.springboot.template.dao.*"########### 启用内置过滤器(第一个 stat 必须,否则监控不到SQL)##########filters: stat,wall,log4j2# 自己配置监控统计拦截的filterfilter:# 开启druiddatasource的状态监控stat:enabled: truedb-type: mysql# 开启慢sql监控,超过2s 就认为是慢sql,记录到日志中log-slow-sql: trueslow-sql-millis: 2000# 日志监控,使用slf4j 进行日志输出slf4j:enabled: truestatement-log-error-enabled: truestatement-create-after-log-enabled: falsestatement-close-after-log-enabled: falseresult-set-open-after-log-enabled: falseresult-set-close-after-log-enabled: false########## 配置WebStatFilter,用于采集web关联监控的数据 ##########web-stat-filter:enabled: true                   # 启动 StatFilterurl-pattern: /* # 过滤所有urlexclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 排除一些不必要的urlsession-stat-enable: true       # 开启session统计功能session-stat-max-count: 1000 # session的最大个数,默认100########## 配置StatViewServlet(监控页面),用于展示Druid的统计信息 ##########stat-view-servlet:enabled: true                   # 启用StatViewServleturl-pattern: /druid/* # 访问内置监控页面的路径,内置监控页面的首页是/druid/index.htmlreset-enable: false              # 不允许清空统计数据,重新计算login-username: root # 配置监控页面访问密码login-password: 123allow: 127.0.0.1 # 允许访问的地址,如果allow没有配置或者为空,则允许所有访问deny: # 拒绝访问的地址,deny优先于allow,如果在deny列表中,就算在allow列表中,也会被拒绝

3)启动后访问监控面板:http://localhost:8101/api/druid/index.html

输入上述配置中的用户名和密码登录:1692192726054969345_0.7162386619813503

在这里插入图片描述

💡扩展知识:想去除底部广告,可以在项目中添加下面的代码:

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.servlet.*;
import java.io.IOException;@Configuration
@ConditionalOnWebApplication
@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled",havingValue = "true", matchIfMissing = true)
public class RemoveDruidAdConfig {/*** 方法名: removeDruidAdFilterRegistrationBean* 方法描述 除去页面底部的广告* @param properties com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties* @return org.springframework.boot.web.servlet.FilterRegistrationBean*/@Beanpublic FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) {// 获取web监控页面的参数DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();// 提取common.js的配置路径String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");final String filePath = "support/http/resources/js/common.js";//创建filter进行过滤Filter filter = new Filter() {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {chain.doFilter(request, response);// 重置缓冲区,响应头不会被重置response.resetBuffer();// 获取common.jsString text = Utils.readFromResource(filePath);// 正则替换banner, 除去底部的广告信息text = text.replaceAll("<a.*?banner\"></a><br/>", "");text = text.replaceAll("powered.*?shrek.wang</a>", "");response.getWriter().write(text);}@Overridepublic void destroy() {}};FilterRegistrationBean registrationBean = new FilterRegistrationBean();registrationBean.setFilter(filter);registrationBean.addUrlPatterns(commonJsPattern);return registrationBean;}
}
使用 Druid 监控

下面我们测试一下因为数据库连接池不足导致的性能问题,并借此带大家熟悉 Druid 监控的使用。

1)将配置中的连接池大小改为 1,且获取连接等待时间超时为 2s:

druid:# 配置初始化大小、最小、最大initial-size: 1minIdle: 1max-active: 1# 配置获取连接等待超时的时间(单位:毫秒)max-wait: 2000

然后任意执行一次对数据库的批量操作,比如插入 20 条数据,每批 2 条,一共 10 批,会随机报错:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查看 Druid 监控,可以看到最大并发为 1,因为连接池的连接数量只有 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

除了 SQL 的监控,还有 URI 的监控,可以看到是哪个接口调用了数据库,执行了多少时间。以后出现线上数据库卡死的问题时,很快就能定位到是哪个接口、哪个 SQL 出现了问题(或者访问频率过高)。1692192726054969345_0.7465633142458248

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

💡 Druid 的 URI 监控是怎么实现的?

核心实现方法如下:1692192726054969345_0.21558958193359

  1. 通过基于 Servlet 的过滤器 WebStatFilter 来拦截请求,该过滤器会收集关于请求的相关信息,比如请求的 URI、执行时长、请求期间执行的 SQL 语句数等。
  2. 统计 URI 和 SQL 执行情况是怎么关联起来的呢? 每次执行 SQL 时,Druid 会在内部统计该 SQL 的执行情况,而 WebStatFilter 会把 SQL 执行信息与当前的 HTTP 请求 URI 关联起来。

2)将配置中的连接池大小改为 10,且获取连接等待时间超时为 2s:

▼yaml复制代码druid:# 配置初始化大小、最小、最大initial-size: 10minIdle: 10max-active: 10# 配置获取连接等待超时的时间(单位:毫秒)max-wait: 2000

可以看到,SQL 的并发直接变成了 10:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

URI 接口调用的耗时,直接缩小的 10 倍,符合我们提升了 10 倍并发的优化情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有的时候,即使我们服务器 JVM 的内存和 CPU 占用都非常低,其他的中间件比如 MySQL 和 Redis 的占用也非常低,但系统依然会出现响应慢、卡死的情况。这可能就是因为一些配置错误,所以了解这些知识还是非常有必要的。

七、数据一致性
八、可观测性

三、自动缓存热门题库

需求分析

对于某个数据的时候可能会被大量访问,大家可以直接点链接进入特定的内容,这个内容就会成为 热点数据 。我们如果能够预判到热点数据,可以提前人工给数据加缓存,也就是 缓存预热

但是有些时候,我们无法预料到哪些数据是热点。如果某个数据没来得及缓存,突然被大量访问,系统不就故障了么?

这里我们就要引出一个新的概念:热点问题

很多企业级项目都会有热点问题,例如微博,一个明星出了某些花边新闻,那么这条微博就会成为热点,此时系统需要 自动发现 这个热点,将其做多级缓存来顶住大流量访问的压力。

为什么需要 自动发现 热点?

因为热点的瞬时流量大,需要及时发现与缓存。如果靠人为来手动设置,可能刚打开后台页面,系统就已经崩溃了,一般要求秒级发现热点且自动缓存。


需求:希望自动为频繁的题库增加缓存

具体规则就是,如果 5 秒内访问 >= 10 次,就要使用本地缓存将题库详情缓存 10 分钟,之后都从本地缓存读取(或则缓存到Redis中去)。

方案设计

自动缓存题库需要以下五个步骤:

1、 记录访问:用户每访问一次题库,统计次数 + 1

2、访问统计:统计一段时间内题库的访问次数。也就是本节内容重点。

3、阈值判断:访问频率超过一定的阈值,变为热点数据。

4、缓存数据:缓存热点数据

5、获取数据:后续访问时刻,从缓存中获取数据

hotkey入门

这是一个真正经历过实战的高性能热点 key 探测框架,整体架构如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

核心组件

它的主要核心组件如下:

1)Etcd 集群

Etcd 作为一个高性能的配置中心,可以以极小的资源占用,提供高效的监听订阅服务。主要用于存放规则配置,各 worker 的 ip 地址,以及探测出的热 key、手工添加的热 key 等。Etcd 常用于配置中心和注册中心

2)client 端 jar 包

就是在服务中添加的引用 jar,引入后,就可以便捷地去判断某 key 是否热 key。同时,该 jar 完成了 key 上报、监听 Etcd 里的 rule 变化、worker 信息变化、热 key 变化,对热 key 进行本地 Caffeine 缓存等。

3)worker 端集群

worker 端是一个独立部署的 Java 程序,启动后会连接 Etcd,并定期上报自己的 ip 信息,供 client 端获取地址并进行长连接。之后,主要就是对各个 client 发来的待测 key 进行 累加计算,当达到 Etcd 里设定的 rule 阈值后,将热 key 推送到各个 client

4)dashboard 控制台

控制台是一个带可视化界面的 Java 程序,也是连接到 Etcd,之后在控制台设置各个 APP 的 key 规则,譬如 2 秒出现 20 次算热 key。然后当 worker 探测出来热 key 后,会将 key 发往 etcd,dashboard 也会监听热 key 信息,进行入库保存记录。同时,dashboard 也可以手工添加、删除热 key,供各个 client 端监听。

后端开发(hotkey 实战)
1、安装 Etcd
2、安装 hotkey worker
3、启动 hotkey 控制台
4、引入 hotkey client

有 2 种引入 hotkey client 的方式:

  1. 手动源码打包
  2. 通过 Maven 远程仓库 引入

由于 Maven 远程仓库的包引用量过少,而且不具备官方权威性,所以更推荐通过 hotkey 源码手动打包。(经过踩坑测试,果然引入远程仓库的包后出现了客户端和 worker 之间心跳失败的问题)

所以选择方式 1,手动将 hotkey 源码中的 client 模块通过 Maven 打成 jar 包:

5、了解开发模式

1)boolean isHotKey(String key)

用于判断改key是否是热key,如果是返回true,如果不是返回false,并且会将key上报到探测集群进行数量计算。

2)Object get(String key)

用于判断是key后,再去获取本地缓存的value值。

3)void smartSet(String key, Object value)

方法个热key赋值value,如果是热key,该方法才会赋值,非热key,什么也不做。

4)Object getValue(String key)

该方法是一个整合方法,相当于 isHotKey 和 get 两个方法的整合,该方法直接返回本地缓存的 value。 如果是热 key,则存在两种情况

  1. 是返回 value
  2. 是返回 null
6、配置 hotkey 规则

判断 bank_detail_ 开头的 key,如果 5 秒访问 10 次,就会被推送到 jvm 内存中,将这个热 key 缓存 10 分钟。

对应的规则配置如下:

[{"duration": 600, // 缓存 10 分钟"key": "bank_detail_", // 判断这个开头的key"prefix": true,"interval": 5, // 间隔"threshold": 10,// 阈值"desc": "热门题库缓存"}
]
7、项目应用 hotkey
8、测试验证

debug 程序,第 11 次请求的时候虽然会判断为 hotkey,但还是不会走缓存,本次请求会设置缓存。后续就能查询缓存了。

扩展知识

1、 如何更新本地缓存

2、能否能够和redis分布式缓存结合。

扩展

1)热点题目增加自动缓存

思路:跟本教程演示的热点题库完全一致。

2)编写一个注解,自动对该方法进行热 key 探测,并将返回值作为本地缓存

思路:参考 Spring Data Redis 提供的 @Cacheable 注解,可以利用 AOP 扫描注解实现。

4 - 流量安全优化

重点掌握

流量安全优化

  • 网站流量控制和熔断(基于Sentinel)
  • 动态 IP 黑白名单过滤(基于Nacos)

一、网站流量控制和熔断

流量安全优化的目标可以简单概括为:确保数据在传输过程中的机密性、完整性和可用性,防止未经授权的访问、纂改、泄露和攻击,同时提升网络传输效率与性能。

1、流量控制

1)请求频率限制:限制单位时间内单用户、单 IP 的请求数(如每秒最多 100 次请求)。

2)带宽限制:控制访问系统时消耗的带宽量或者下载速度。

3)总流量限制:限制用户或系统整体的数据传输量。

4)细粒度控制:根据接口、用户等特定维度进行组合限流。比如限制访问特定接口时,每个用户每分钟只能访问 60 次。

2、熔断机制

熔断机制目的:避免当下游服务器发生异常时整个系统继续耗费资源重复发起失败请求,从而防止连锁故障

这类似于电路中的断路器,当检测到异常情况时,熔断器会自动切断对故障服务的调用,防止问题扩大

工作机制:

  1. 监控服务健康状态:系统户实时监控服务的调用情况,例如请求成功率,响应时间根据这些来判断服务的健康状态。
  2. 进入熔断状态:监控服务健康状态出现异常,激活 熔断器 ,暂停对改服务的调用。
  3. 快速失败。
  4. 熔断恢复机制:熔断器会进入半开状态,允许少量请求测试服务,如果服务监控正常,熔断器将关闭,恢复正常服务调用,如果仍有问题,则继续保持熔断。

熔断流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3、降级机制

降级的目的是在某个服务的响应能力下降、或该服务不可用时,提供简化版的功能或返回默认值作为 兜底,保持系统的部分功能可用,确保用户体验的连续性,避免系统频繁报错。

降级可以是手动配置,也可以根据系统负载自动触发。系统可能由于多种原因(如高负载、外部依赖不可用等)触发降级,返回简化的响应或默认值。

降级机制的好处:

  1. 优雅地处理故障:在降级状态下,系统不会直接返回错误信息,而是提供一个替代方案。例如,某个数据查询服务不可用时,系统可以返回缓存数据,确保用户看到的是有效信息,而非错误页面。
  2. 降低服务压力:降级有助于减轻系统对非核心服务的依赖,确保核心功能的稳定运行。例如,当推荐系统或广告服务出现故障时,降级可以减少对这些服务的调用,保护系统的整体稳定性。

举个例子,在一个电商网站上,如果商品推荐系统由于外部服务故障无法正常运行,可以触发降级机制,显示一组静态的推荐商品列表。这确保用户仍然能够顺利浏览商品页面,而不是直接看到错误信息。

是不是有点 try…catch… 的感觉?但降级这个概念显然比异常处理。

二、Sentinel 入门

1、核心概念

1)资源:表示要保护的业务逻辑或代码块。我们说的资源,可以是任何东西,服务、服务里的方法、甚至是一段代码。

使用 Sentinel 来进行资源保护,主要分为几个步骤:

  1. 定义资源
  2. 定义规则
  3. 校验规则是否有效
3、Sentinel 入门 Demo
4、下载并启动 Sentinel 控制台

可以 参考官方文档 进行安装。

1)下载控制台 jar 包并在本地启动,可以访问从 github 上下载 release的 jar 包。

本教程为大家提供了软件包:https://pan.baidu.com/s/1u73-Nlolrs8Rzb1_b6X6HA ,提取码:c2sd1692192726054969345_0.9878708215363858

2)直接在命令行窗口启动 Sentinel 控制台:

注意:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。

命令

java -Dserver.port=8131 -jar sentinel-dashboard-1.8.6.jar

4)客户端接入控制台

引入 Maven 依赖,用于和 Sentinel 控制台通讯:

▼xml复制代码<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-transport-simple-http</artifactId><version>1.8.6</version>
</dependency>

程序启动时需要加入 JVM 参数 -Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制台地址和端口。若启动多个应用,则需要通过 -Dcsp.sentinel.api.port=xxxx 指定客户端监控 API 的端口(默认是 8719)。

Sentinel 非常贴心,提供了很多框架整合的依赖,便于开发,比如 Spring Web 项目支持将所有的接口自动识别为资源:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以根据使用的框架引入适配依赖,参考官方文档。

此处直接运行 Main 方法来演示效果,JVM 参数为:-Dcsp.sentinel.dashboard.server=localhost:8131

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

还有更多的配置,比如更改日志目录等,可以看 官方文档 了解。

5、规则管理和推送

问题:Sentinel的规则存储在哪里?又是如何通过控制台修改规则之后,将规则同步给客户端进行限流熔断的呢?

官方文档 有详细地介绍:Sentinel 控制台同时提供简单的规则管理以及推送的功能。规则推送分为 3 种模式,包括 “原始模式”、“Pull 模式” 和 “Push 模式”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

目前控制台的规则推送也是通过 规则查询更改 HTTP API 来更改规则。这也意味着这些规则仅在内存态生效,应用重启之后,该规则会丢失。

更多规则管理和推送规则可以阅读:在生产环境使用 Sentinel。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6、整合 Spring Boot

基于 Spring Boot Starter + 注解模式开发 + 原始规则推送模式开发1692192726054969345_0.13361055357278895

Spring Boot 项目可以轻松和 Sentinel 集成,直接引入一个 starter,使用 Spring Cloud Alibaba Sentinel 即可。

在引入整合依赖时,一定要注意版本号!

建议 参考官方文档选择版本。由于 Spring Boot 3.0,Spring Boot 2.7~2.4 和 2.4 以下版本之间变化较大,目前企业级客户老项目相关 Spring Boot 版本仍停留在 Spring Boot 2.4 以下,为了同时满足存量用户和新用户不同需求,社区以 Spring Boot 3.0 和 2.4 分别为分界线,同时维护 2022.x、2021.x、2.2.x 三个分支迭代。1692192726054969345_0.8921685825905006

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

本项目 Spring Boot 用的是 2.7,因此使用 Sentinel Starter 的版本 2021.0.5.0。在项目中引入依赖:

<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId><version>2021.0.5.0</version>
</dependency>

可以看到,该依赖自动整合了 Sentinel 的 core 包、客户端通讯包、注解开发包、webmvc 适配包、热点参数限流包等:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

整合包支持自动将所有的接口根据 url 路径识别为资源。启动项目后,通过接口文档测试就能看到监控效果:1692192726054969345_0.4077353096514593

7、开发模式

1)**定义资源:**支持通过代码、引入框架适配、注解方式定义资源。

推荐开发模式:优先使用适配包来自动识别资源,然后能运用注解尽量运用注解,最后再选择主动编码定义资源。

2)定义规则:支持通过代码、控制台**(推荐)**、配置文件来定义规则。

8、其他特性

三、后端开发(Sentinel 实战)

1、查看题库列表接口限流熔断

资源:listQuestionBankVOByPage 接口

目的:对于耗时较长的、经常访问的接口的请求频率,防止过多请求导致系统过载。

限流规则:

  • 策略:整个接口每秒钟不超过10次
  • 阻塞操左:提示“系统压力过大,请耐心等待”

熔断规则:

  • 熔断规则:如果接口异常率超过10%,或则慢掉用(响应时长 > 3 秒)的比例大于20%,触发60秒熔断。
  • 熔断操做: 直接返回本地数据(缓存或空数据)

开发模式:用注解定义资源 + 基于控制台定义规则

1)定义资源。给需要限流的接口添加 @SentinelResource 注解:

@PostMapping("/list/page/vo")
@SentinelResource(value = "listQuestionBankVOByPage",blockHandler = "handleanBlockHandler",fallback = "handleFallback")
public BaseResponse<Page<QuestionBankVO>> listQuestionBankVOByPage(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,HttpServletRequest request) {
}

2)实现限流阻塞和熔断降级方法

3)通过控制台定义规则

熔断规则:新增两条熔断规则,注意设置最小请求数、统计时长

4)测试

注意,只有业务异常(比如请求参数错误、或者数据库操作失败等问题),才会算到熔断条件中,限流熔断本身的异常 BlockException 是不计算的。

测试发现,如果 blockHandlerfallbackHandler 同时配置,当熔断器打开后,仍然会进入 blockHandler 进行处理,因此需要在该方法中处理因为熔断触发的降级逻辑:1692192726054969345_0.17912640063145813

▼java复制代码/*** listQuestionBankVOByPage 流控操作* 限流:提示“系统压力过大,请耐心等待”* 熔断:执行降级操作*/
public BaseResponse<Page<QuestionBankVO>> handleBlockException(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,HttpServletRequest request, BlockException ex) {// 降级操作if (ex instanceof DegradeException) {return handleFallback(questionBankQueryRequest, request, ex);}// 限流操作return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统压力过大,请耐心等待");
}

总结一下:

  • blockHandler 处理 Sentinel 流量控制异常,如 BlockException
  • fallback 处理业务逻辑中的异常,比如我们自己的 BusinessException

四、动态 IP 黑名单过滤

需求分析

一些恶意用户肯频繁请求服务器资源,导致资源占用过高。因此我们需要一定的手段实时阻止可疑或恶意的用户,减少攻击风险。

通过 IP 封禁,可以有效拉黑攻击者,防止资源被滥用,保障合法用户的正常访问。

对于我们的需求,不让拉近黑名单的IP访问任何接口。

方案设计
1、 设计过程
2、最终方案

1)使用Nacos 配置中心存储和管理 IP 黑名单

2)后端服务利用

3)后端服务利用布隆骡驴其过滤IP黑名单

Bloom Filter 是一种高效的、基于概率的数据结构,用于判断一个元素是否存在于集合中。

原理是利用多个哈希函数将元素映射到固定的点位上(位数组中),因此面对海量数据它占据的空间也非常小。1692192726054969345_0.0015558538338711347

例如某个 key 通过 hash-1 和 hash-2 两个哈希函数,定位到数组中的值都为 1,则说明它存在。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果布隆过滤器判断一个元素不存在集合中,那么这个元素一定不在集合中,如果判断元素存在集合中则不一定是真的,因为哈希可能会存在冲突。因此布隆过滤器 有误判的概率

配置中心
配置中心支持的功能
  1. 集中化配置管理:所有服务的配置可以在一个地方集中挂历,运维人员和开发人员可以通过统一的接口修改和获取配置,避免了在每个实例中重复的配置。
  2. 动态配置:配置中心允许在不启动应用的情况下动态更新配置。
Nacos 入门
Nacos 配置管理的核心概念

实际上,Nacos 不仅支持配置管理,它还支持服务发现(作为注册中心),以下是官网总结的 Nacos 地图:

1、Namespace(命名空间)

例如,开发环境的配置和生产环境的配置完全隔离,可以通过不同的命名空间来管理。

2、Group(组)

例如,一个系统中的“支付服务”和“订单服务”可能需要用不同的组来存储各自的配置。

3、Data ID

Data ID 是一个唯一的配置标识符,通常与具体的应用程序相关。

4、Config Listener(配置监听器)

推送和监听

推送方法:

  1. Nacos控制台(推荐)
  2. 应用程序 SDK。Nacos 支持和 Spring Boot 快速整合,可以参考 官方文档
  3. Open API](https://nacos.io/zh-cn/docs/open-api.html)

监听方法:使用SDK配置Config Listener,参考官方文档。实例代码如下:

后端开发
扩展

1)基于 DFA 实现黑名单过滤

思路:可以自主了解 DFA 算法,通过 Hutool 工具类等现成的库轻松实现,不建议自己写算法。由于有多种黑名单过滤算法,还可以基于策略模式优化代码。1692192726054969345_0.6415266086656102

2)配置降级

思路:验证如果从 Nacos 获取配置失败后,应用程序会有什么表现?思考如何防止配置拉取失败的情况,提升应用的稳定性。

5 - 内容安全优化

重点

内容安全优化

  • 同端登录冲突检测(基于 Sa-Token)
  • 分级题目反爬虫策略(基于 Redis)

一、同端登录冲突检测

需求分析

针对本刷题万盏,若会员账户被不法分子售卖或多人共享,将会损害公司利益。

为防止这些情况,我们的系统需要能够实时监控和检测同一个账户在多个设备上的登录情况,在检测到冲突时候,及时通知用户并采取相应的安全措施。

方案设计
1、账号冲突检测策略

1)单点登录模式

2)多设备登录限制

3)同设备类型限制

允许同一个账户在不同设备类型上同时dengl,单同一个类型设备只能登录一个,为了兼顾我们的网站,因此我们采用第3种策略 同设备类型限制**(同端互斥登录)**

2、实现思路

如何实现同端互斥登录呢?

  1. 用户登录时获取当前设备信息(通过 User-Agent 获取)
  2. 用户登录时判断同设备是否已经登录(本地或缓存中是否已存在
  3. 将用户登录信息与设备信息一起保存(本地或三方缓存中)
  4. 如果检测到冲突,可以直接顶号(将前一个设备下线,也就是移除登录态)

禁言不足之下,建议使用一些成熟的第三方框架,比如Sa-Token内置了同端互斥登录功能。

二、Sa-Token 入门

要理解 Sa-Token 的 Session 模型,区别于 Servlet 的 HttpSession,Sa-Token 维护了自己的 Session,共有 3 种 Session 创建时机:

  • Account-Session: 指的是框架为每个 账号 id 分配的 Session。是分配给账号id的,而不是分配给指定客户端的,也就是说在 PC、APP 上登录的同一账号所得到的 Session 也是同一个,所以两端可以非常轻松的同步数据。
  • Token-Session: 指的是框架为每个 token 分配的 Session。不同的设备端,哪怕登录了同一账号,只要它们得到的 token 不一致,它们对应的 Token-Session 就不一致,这就为我们不同端的独立数据读写提供了支持。比如实现“指定客户端超过两小时无操作就自动下线,如果两小时内有操作,就再续期两小时,直到新的两小时无操作”。
  • Custom-Session: 指的是以一个特定的值作为 SessionId,来分配的 Session。不依赖特定的账号id 或者 token,当成一个 Map 去使用即可,比如可以为一个团队的用户指定相同的 SessionId,让一个团队最多 N 个用户同时在线等。
后端开发(Sa-Token 实战)
1、引入 Sa-Token

在项目中添加下列依赖:

<!-- Sa-Token 权限认证 -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.39.0</version>
</dependency>
2、配置 Sa-Token

在项目的 application.yml 中添加以下配置。重点关注 is-concurrent,需要设置为 false,这样才能实现同端冲突下线。

# Sa-Token 配置
sa-token:# token 名称(同时也是 cookie 名称)token-name: mianshiya# token 有效期(单位:秒) 默认30天,-1 代表永久有效timeout: 2592000# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结active-timeout: -1# 是否允许同一账号多地同时登录(为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)is-concurrent: false# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)is-share: true# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)token-style: uuid# 是否输出操作日志 is-log: true
7、改造获取用户信息的逻辑

UserServiceImpl#getLoginUser 修改为如下,不再从 request.getSession() 中获取登录用户的 id,改为从 Sa-Token 中获取。

@Override
public User getLoginUser(HttpServletRequest request) {// 先判断是否已登录Object loginUserId = StpUtil.getLoginIdDefaultNull();if (loginUserId == null) {throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}// 从数据库查询(追求性能的话可以注释,直接走缓存)User currentUser = this.getById((String) loginUserId);if (currentUser == null) {throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}return currentUser;
}

如果用户信息几乎很少修改,可以不查数据库,直接从 Sa-Token 的 Session 中获取之前保存的用户登录态:

@Override
public User getLoginUser(HttpServletRequest request) {// 先判断是否已登录Object loginId = StpUtil.getLoginIdDefaultNull();if (Objects.isNull(loginId)) {throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}return (User) StpUtil.getSessionByLoginId(loginId).get(USER_LOGIN_STATE);
}

注意,所有原本从 servlet Session 中取登录态的代码都要修改! 比如还有 UserServiceImpl#isAdmin 方法:

public boolean isAdmin(HttpServletRequest request) {// 仅管理员可查询// 基于 Sa-Token 改造Object userObj = StpUtil.getSession().get(USER_LOGIN_STATE);// Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);User user = (User) userObj;return isAdmin(user);
}
扩展知识
1、Sa-Token 登录异常捕获

通过测试发现,如果不做任何处理,接口返回的无权限和未登录的报错如下:

▼json复制代码{"code": 50000,"data": null,"message": "系统错误"
}

这是因为没有处理 Sa-Token 自己的未登录异常(获取不到 Token 的异常)。

可以在 GlobalExceptionHandler 文件中,全局错误拦截 Sa-Token 的 NotRoleException 和 NotLoginException 异常:

@ExceptionHandler(NotRoleException.class)
public BaseResponse<?> notRoleExceptionHandler(RuntimeException e) {log.error("NotRoleException", e);return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限");
}@ExceptionHandler(NotLoginException.class)
public BaseResponse<?> notLoginExceptionHandler(RuntimeException e) {log.error("NotLoginException", e);return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "未登录");
}

之后,用户无访问权限和未登录的报错就更清晰了:

{"code": 40101,"data": null,"message": "无权限"
}{"code": 40100,"data": null,"message": "未登录"
}
2、Sa-Token 集成 Redis

三、分级题目反爬虫策略

方案设计
反爬虫的手段

可能会想到“限流”,限流确实可以减缓爬虫的请求频率,通过限制每个用户或则IP在一定实际内的请求数量来降低对系统的压力。然而,限流的核心目的是保护系统的可用性和性能,防止系统因瞬时过载而奔溃。它更像一种流量控制措施,而不是专门的防爬策略。

多级反爬虫策略

建议采用分级反爬虫策略,先告警、再采取强制措施,可以有效减少误封的风险:

  • 如果每分钟超过 10 道题目,则给管理员发送告警,比如发送邮件或者短信。
  • 如果每分钟超过 20 道题目,则直接将账号踢下线,且进行封号操作。(或者限制一段时间无法访问)

那么如何统计用户访问题目的频率呢?有下面 3 种方案:

统计访问频率 - 结合已有系统

前几节中,我们学习了Hotkey热key探测系统和Sentinel流控系统,这些系统实现固安捷都在于如何统计一段时间内的调用频率。

统计访问频率 - 基于本地计数器(单机)
统计访问频率 - 基于 Redis 统计(分布式)

1)设计键值对

要能区分出用户和时间窗,示例key为:user:access:{userId}:{分钟级时间戳}

表示:每一分钟对应一个不同的key

每个 key 的 value,就是该用户在这分钟内的访问次数。

四、后端开发

1、通用计数器
扩展
1、配置动态化

可以将以下 2 个配置放到 Nacos 中,并通过 @Value 注解获取,支持动态调整反爬虫的配置:

@NacosValue(value = "${warn.count:10}", autoRefreshed = true)
private Integer warnCount;@NacosValue(value = "${ban.count:20}", autoRefreshed = true)
private Integer banCount;

包括 CounterManager 中的 timeInterval、timeUnit 这两个参数,也可以抽取为配置,放置到 Nacos 中。

2、策略模式

可以用策略模式实现多种计数器,比如基于 LongAdder 的本地计数器,开发者可以自主选择使用何种计数器。

而且还可以将本地计数器作为 Redis 计数器统计异常的降级,这样即使项目没有配置 Redis 也可以使用。

3、优化频率统计(只有思路但是没有很好的实现)

目前每次统计都要请求 Redis,频率非常高的情况下,可能会对 Redis 造成压力。可以先本地聚合统计结果,每隔一段时间再统一发送给 Redis。这也是之前讲过的 Hotkey 的统计原理,在高并发点赞系统中,也可以运用类似的设计思路。

当然,也可以直接尝试结合 Hotkey 或 Sentinel 优化频率统计的精准度(采用滑动窗口统计)。

4、提升检测爬虫的易用性

“未登录”);
}


之后,用户无访问权限和未登录的报错就更清晰了:```json
{"code": 40101,"data": null,"message": "无权限"
}{"code": 40100,"data": null,"message": "未登录"
}
2、Sa-Token 集成 Redis

三、分级题目反爬虫策略

方案设计
反爬虫的手段

可能会想到“限流”,限流确实可以减缓爬虫的请求频率,通过限制每个用户或则IP在一定实际内的请求数量来降低对系统的压力。然而,限流的核心目的是保护系统的可用性和性能,防止系统因瞬时过载而奔溃。它更像一种流量控制措施,而不是专门的防爬策略。

多级反爬虫策略

建议采用分级反爬虫策略,先告警、再采取强制措施,可以有效减少误封的风险:

  • 如果每分钟超过 10 道题目,则给管理员发送告警,比如发送邮件或者短信。
  • 如果每分钟超过 20 道题目,则直接将账号踢下线,且进行封号操作。(或者限制一段时间无法访问)

那么如何统计用户访问题目的频率呢?有下面 3 种方案:

统计访问频率 - 结合已有系统

前几节中,我们学习了Hotkey热key探测系统和Sentinel流控系统,这些系统实现固安捷都在于如何统计一段时间内的调用频率。

统计访问频率 - 基于本地计数器(单机)
统计访问频率 - 基于 Redis 统计(分布式)

1)设计键值对

要能区分出用户和时间窗,示例key为:user:access:{userId}:{分钟级时间戳}

表示:每一分钟对应一个不同的key

每个 key 的 value,就是该用户在这分钟内的访问次数。

四、后端开发

1、通用计数器
扩展
1、配置动态化

可以将以下 2 个配置放到 Nacos 中,并通过 @Value 注解获取,支持动态调整反爬虫的配置:

@NacosValue(value = "${warn.count:10}", autoRefreshed = true)
private Integer warnCount;@NacosValue(value = "${ban.count:20}", autoRefreshed = true)
private Integer banCount;

包括 CounterManager 中的 timeInterval、timeUnit 这两个参数,也可以抽取为配置,放置到 Nacos 中。

2、策略模式

可以用策略模式实现多种计数器,比如基于 LongAdder 的本地计数器,开发者可以自主选择使用何种计数器。

而且还可以将本地计数器作为 Redis 计数器统计异常的降级,这样即使项目没有配置 Redis 也可以使用。

3、优化频率统计(只有思路但是没有很好的实现)

目前每次统计都要请求 Redis,频率非常高的情况下,可能会对 Redis 造成压力。可以先本地聚合统计结果,每隔一段时间再统一发送给 Redis。这也是之前讲过的 Hotkey 的统计原理,在高并发点赞系统中,也可以运用类似的设计思路。

当然,也可以直接尝试结合 Hotkey 或 Sentinel 优化频率统计的精准度(采用滑动窗口统计)。

4、提升检测爬虫的易用性

如果有更多接口需要检测爬虫,可以考虑封装一个检测爬虫的注解,并通过 AOP 进行扫描;使用时直接给要反爬的接口添加注解即可

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com