mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-08-01 16:28:03 +08:00
Compare commits
12 Commits
0c0efe5c3c
...
fd06ce38cf
Author | SHA1 | Date | |
---|---|---|---|
|
fd06ce38cf | ||
|
5cc5c9aff1 | ||
|
5abcaa7939 | ||
|
fb079ad8db | ||
|
5ef880c970 | ||
|
5d86986064 | ||
|
dd2adf1d90 | ||
|
0e843450d5 | ||
|
8da9f9e5ed | ||
|
2fefae797c | ||
|
05f556f412 | ||
|
1ea9254ba3 |
@ -322,6 +322,7 @@ export default sidebar({
|
|||||||
icon: "star",
|
icon: "star",
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
children: [
|
children: [
|
||||||
|
"redis-delayed-task",
|
||||||
"3-commonly-used-cache-read-and-write-strategies",
|
"3-commonly-used-cache-read-and-write-strategies",
|
||||||
"redis-data-structures-01",
|
"redis-data-structures-01",
|
||||||
"redis-data-structures-02",
|
"redis-data-structures-02",
|
||||||
|
@ -1,3 +1,19 @@
|
|||||||
$theme-color: #2980b9;
|
$theme-color: #2980b9;
|
||||||
$sidebar-width: 20rem;
|
$sidebar-width: 20rem;
|
||||||
$sidebar-mobile-width: 16rem;
|
$sidebar-mobile-width: 16rem;
|
||||||
|
$font-family:
|
||||||
|
-apple-system,
|
||||||
|
Georgia,
|
||||||
|
SF Pro Text,
|
||||||
|
SF Pro Icons,
|
||||||
|
Helvetica Neue,
|
||||||
|
PingFangTC-light,
|
||||||
|
PingFang-SC-light;
|
||||||
|
$font-family-heading:
|
||||||
|
-apple-system,
|
||||||
|
Georgia,
|
||||||
|
SF Pro Text,
|
||||||
|
SF Pro Icons,
|
||||||
|
Helvetica Neue,
|
||||||
|
PingFangTC-light,
|
||||||
|
PingFang-SC-light;
|
||||||
|
@ -65,7 +65,7 @@ ER 图由下面 3 个要素组成:
|
|||||||
- **函数依赖(functional dependency)**:若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
|
- **函数依赖(functional dependency)**:若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
|
||||||
- **部分函数依赖(partial functional dependency)**:如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号);
|
- **部分函数依赖(partial functional dependency)**:如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号);
|
||||||
- **完全函数依赖(Full functional dependency)**:在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级);
|
- **完全函数依赖(Full functional dependency)**:在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级);
|
||||||
- **传递函数依赖**:在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。。
|
- **传递函数依赖**:在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。
|
||||||
|
|
||||||
### 3NF(第三范式)
|
### 3NF(第三范式)
|
||||||
|
|
||||||
|
@ -174,9 +174,9 @@ MySQL 8.x 中实现的索引新特性:
|
|||||||
|
|
||||||
## 二级索引
|
## 二级索引
|
||||||
|
|
||||||
**二级索引(Secondary Index)又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。**
|
二级索引(Secondary Index)的叶子节点存储的数据是主键的值,也就是说,通过二级索引可以定位主键的位置,二级索引又称为辅助索引/非主键索引。
|
||||||
|
|
||||||
唯一索引,普通索引,前缀索引等索引属于二级索引。
|
唯一索引,普通索引,前缀索引等索引都属于二级索引。
|
||||||
|
|
||||||
PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。
|
PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
|
|||||||
|
|
||||||
#### 聚簇索引介绍
|
#### 聚簇索引介绍
|
||||||
|
|
||||||
**聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。**
|
聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。
|
||||||
|
|
||||||
在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
|
在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
|
|||||||
|
|
||||||
#### 非聚簇索引介绍
|
#### 非聚簇索引介绍
|
||||||
|
|
||||||
**非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。**
|
非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
|
||||||
|
|
||||||
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
|
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
|
||||||
|
|
||||||
@ -262,7 +262,9 @@ SELECT id FROM table WHERE id=1;
|
|||||||
|
|
||||||
### 覆盖索引
|
### 覆盖索引
|
||||||
|
|
||||||
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
|
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。
|
||||||
|
|
||||||
|
在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。
|
||||||
|
|
||||||
**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。**
|
**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。**
|
||||||
|
|
||||||
@ -313,7 +315,8 @@ CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据
|
|||||||
为了能够对这 100w 数据按照 `score` 进行排序,我们需要执行下面的 SQL 语句。
|
为了能够对这 100w 数据按照 `score` 进行排序,我们需要执行下面的 SQL 语句。
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;#降序排序
|
#降序排序
|
||||||
|
SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort` ,我们发现是没有用到覆盖索引的。
|
使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort` ,我们发现是没有用到覆盖索引的。
|
||||||
@ -348,13 +351,89 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
|
|||||||
|
|
||||||
### 最左前缀匹配原则
|
### 最左前缀匹配原则
|
||||||
|
|
||||||
最左前缀匹配原则指的是,在使用联合索引时,**MySQL** 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 **`>`**、**`<`** )才会停止匹配。对于 **`>=`**、**`<=`**、**`BETWEEN`**、**`like`** 前缀匹配的范围查询,并不会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
|
最左前缀匹配原则指的是在使用联合索引时,MySQL 会根据索引中的字段顺序,从左到右依次匹配查询条件中的字段。如果查询条件与索引中的最左侧字段相匹配,那么 MySQL 就会使用索引来过滤数据,这样可以提高查询效率。
|
||||||
|
|
||||||
相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ)。
|
最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配(相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ))。
|
||||||
|
|
||||||
|
假设有一个联合索引`(column1, column2, column3)`,其从左到右的所有前缀为`(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。
|
||||||
|
|
||||||
|
我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
|
||||||
|
|
||||||
|
我们这里简单演示一下最左前缀匹配的效果。
|
||||||
|
|
||||||
|
1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class`这 3 个字段。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `student` (
|
||||||
|
`id` int NOT NULL,
|
||||||
|
`name` varchar(100) DEFAULT NULL,
|
||||||
|
`class` varchar(100) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `name_class_idx` (`name`,`class`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
```
|
||||||
|
|
||||||
|
2、下面我们分别测试三条不同的 SQL 语句。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sql
|
||||||
|
# 可以命中索引
|
||||||
|
SELECT * FROM student WHERE name = 'Anne Henry';
|
||||||
|
EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk';
|
||||||
|
# 无法命中索引
|
||||||
|
SELECT * FROM student WHERE class = 'lIrm08RYVk';
|
||||||
|
```
|
||||||
|
|
||||||
|
MySQL 8.0.13 版本引入了索引跳跃扫描(Index Skip Scan,简称 ISS),它可以在某些索引查询场景下提高查询效率。在没有 ISS 之前,不满足最左前缀匹配原则的联合索引查询中会执行全表扫描。而 ISS 允许 MySQL 在某些情况下避免全表扫描,即使查询条件不符合最左前缀。不过,这个功能比较鸡肋, 和 Oracle 中的没法比,MySQL 8.0.31 还报告了一个 bug:[Bug #109145 Using index for skip scan cause incorrect result](https://bugs.mysql.com/bug.php?id=109145)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。
|
||||||
|
|
||||||
## 索引下推
|
## 索引下推
|
||||||
|
|
||||||
**索引下推(Index Condition Pushdown)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
|
**索引下推(Index Condition Pushdown,简称 ICP)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 `WHERE`字句的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率。
|
||||||
|
|
||||||
|
假设我们有一个名为 `usr` 的表,其中包含 `id`, `name`, 和 `age` 3 个字段,`name` 字段上创建了索引。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
#查询名字以"Aoki"开头且年龄为30岁的用户
|
||||||
|
EXPLAIN SELECT * FROM usr
|
||||||
|
WHERE name LIKE 'Aoki%' AND age = 30;
|
||||||
|
```
|
||||||
|
|
||||||
|
- 没有索引下推之前,即使 `name` 字段建立的索引可以帮助我们快速定位到了以“张”开头的用户,但我们仍然需要对每一个找到的以“张”开头的用户进行回表操作,获取完整的用户数据,再判断 `age` 字段是否等于 30。
|
||||||
|
- 有了索引下推之后,存储引擎会在使用 `name` 索引查找以"张"开头的记录时,同时检查 `age` 字段是否等于 30。这样,只有同时满足 `name` 和 `age` 条件的记录才会被返回,减少了回表次数。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
再来讲讲索引下推的具体原理,先看下面这张 MySQL 简要架构图。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处理查询解析、分析、优化、缓存以及与客户端的交互等操作,而存储引擎层负责数据的存储和读取,MySQL 支持 InnoDB、MyISAM、Memory 等多种存储引擎。
|
||||||
|
|
||||||
|
索引下推的**下推**其实就是指将部分上层(Server 层)负责的事情,交给了下层(存储引擎层)去处理。
|
||||||
|
|
||||||
|
我们这里结合索引下推原理再对上面提到的例子进行解释。
|
||||||
|
|
||||||
|
没有索引下推之前:
|
||||||
|
|
||||||
|
- 存储引擎层先根据 `name` 索引字段找到所有以“张”开头用户的主键 ID,然后二次回表查询,获取完整的用户数据。
|
||||||
|
- 存储引擎层把所有以“张”开头的用户数据全部交给 Server 层,Server 层根据`age` 字段是否等于 30 这一条件再进一步做筛选。
|
||||||
|
|
||||||
|
有了索引下推之后:
|
||||||
|
|
||||||
|
- 存储引擎层先根据 `name` 索引字段找到所有以“张”开头的用户,然后直接判断`age` 字段是否等于 30,筛选出符合条件的 主键 ID。
|
||||||
|
- 二次回表查询,根据符合条件的主键 ID 去获取完整的用户数据,
|
||||||
|
- 存储引擎层把符合条件的用户数据全部交给 Server 层。
|
||||||
|
|
||||||
|
可以看出,**除了可以减少回表次数之外,索引下推还可以减少存储引擎层和 Server 层的数据传输量。**
|
||||||
|
|
||||||
|
最后,总结一下索引下推应用范围:
|
||||||
|
|
||||||
|
1. 适用于 InnoDB 引擎和 MyISAM 引擎的查询。
|
||||||
|
2. 适用于执行计划是 range, ref, eq_ref, ref_or_null 的范围查询。
|
||||||
|
3. 对于 InnoDB 表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经读入 InnoDB 缓冲区。在这种情况下使用索引下推 不会减少 I/O。
|
||||||
|
4. 子查询不能使用索引下推,因为子查询通常会创建临时表来处理结果,而这些临时表是没有索引的。
|
||||||
|
5. 存储过程不能使用索引下推,因为存储引擎无法调用存储函数。
|
||||||
|
|
||||||
## 正确使用索引的一些建议
|
## 正确使用索引的一些建议
|
||||||
|
|
||||||
|
82
docs/database/redis/redis-delayed-task.md
Normal file
82
docs/database/redis/redis-delayed-task.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: 如何基于Redis实现延时任务
|
||||||
|
category: 数据库
|
||||||
|
tag:
|
||||||
|
- Redis
|
||||||
|
---
|
||||||
|
|
||||||
|
基于 Redis 实现延时任务的功能无非就下面两种方案:
|
||||||
|
|
||||||
|
1. Redis 过期事件监听
|
||||||
|
2. Redisson 内置的延时队列
|
||||||
|
|
||||||
|
面试的时候,你可以先说自己考虑了这两种方案,但最后发现 Redis 过期事件监听这种方案存在很多问题,因此你最终选择了 Redisson 内置的 DelayedQueue 这种方案。
|
||||||
|
|
||||||
|
这个时候面试官可能会追问你一些相关的问题,我们后面会提到,提前准备就好了。
|
||||||
|
|
||||||
|
另外,除了下面介绍到的这些问题之外,Redis 相关的常见问题建议你都复习一遍,不排除面试官会顺带问你一些 Redis 的其他问题。
|
||||||
|
|
||||||
|
### Redis 过期事件监听实现延时任务功能的原理?
|
||||||
|
|
||||||
|
Redis 2.0 引入了发布订阅 (pub/sub) 功能。在 pub/sub 中,引入了一个叫做 **channel(频道)** 的概念,有点类似于消息队列中的 **topic(主题)**。
|
||||||
|
|
||||||
|
pub/sub 涉及发布者(publisher)和订阅者(subscriber,也叫消费者)两个角色:
|
||||||
|
|
||||||
|
- 发布者通过 `PUBLISH` 投递消息给指定 channel。
|
||||||
|
- 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
在 pub/sub 模式下,生产者需要指定消息发送到哪个 channel 中,而消费者则订阅对应的 channel 以获取消息。
|
||||||
|
|
||||||
|
Redis 中有很多默认的 channel,这些 channel 是由 Redis 本身向它们发送消息的,而不是我们自己编写的代码。其中,`__keyevent@0__:expired` 就是一个默认的 channel,负责监听 key 的过期事件。也就是说,当一个 key 过期之后,Redis 会发布一个 key 过期的事件到`__keyevent@<db>__:expired`这个 channel 中。
|
||||||
|
|
||||||
|
我们只需要监听这个 channel,就可以拿到过期的 key 的消息,进而实现了延时任务功能。
|
||||||
|
|
||||||
|
这个功能被 Redis 官方称为 **keyspace notifications** ,作用是实时监控实时监控 Redis 键和值的变化。
|
||||||
|
|
||||||
|
### Redis 过期事件监听实现延时任务功能有什么缺陷?
|
||||||
|
|
||||||
|
**1、时效性差**
|
||||||
|
|
||||||
|
官方文档的一段介绍解释了时效性差的原因,地址:<https://redis.io/docs/manual/keyspace-notifications/#timing-of-expired-events> 。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这段话的核心是:过期事件消息是在 Redis 服务器删除 key 时发布的,而不是一个 key 过期之后就会就会直接发布。
|
||||||
|
|
||||||
|
我们知道常用的过期数据的删除策略就两个:
|
||||||
|
|
||||||
|
1. **惰性删除**:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
|
||||||
|
2. **定期删除**:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
|
||||||
|
|
||||||
|
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 **定期删除+惰性/懒汉式删除** 。
|
||||||
|
|
||||||
|
因此,就会存在我设置了 key 的过期时间,但到了指定时间 key 还未被删除,进而没有发布过期事件的情况。
|
||||||
|
|
||||||
|
**2、丢消息**
|
||||||
|
|
||||||
|
Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。
|
||||||
|
|
||||||
|
**3、多服务实例下消息重复消费**
|
||||||
|
|
||||||
|
Redis 的 pub/sub 模式目前只有广播模式,这意味着当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。
|
||||||
|
|
||||||
|
这个时候,我们需要注意多个服务实例重复处理消息的问题,这会增加代码开发量和维护难度。
|
||||||
|
|
||||||
|
### Redisson 延迟队列原理是什么?有什么优势?
|
||||||
|
|
||||||
|
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。
|
||||||
|
|
||||||
|
我们可以借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。
|
||||||
|
|
||||||
|
Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。
|
||||||
|
|
||||||
|
Redisson 使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。
|
||||||
|
|
||||||
|
相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势:
|
||||||
|
|
||||||
|
1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。
|
||||||
|
2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
|
||||||
|
|
||||||
|
跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。
|
@ -130,7 +130,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
|
|||||||
### Redis 除了做缓存,还能做什么?
|
### Redis 除了做缓存,还能做什么?
|
||||||
|
|
||||||
- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。
|
- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。
|
||||||
- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。相关阅读:[《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》](https://mp.weixin.qq.com/s/kyFAWH3mVNJvurQDt4vchA)。
|
- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
|
||||||
- **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
|
- **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
|
||||||
- **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
|
- **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
|
||||||
- **分布式 Session** :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
|
- **分布式 Session** :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
|
||||||
@ -139,11 +139,11 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
|
|||||||
|
|
||||||
### 如何基于 Redis 实现分布式锁?
|
### 如何基于 Redis 实现分布式锁?
|
||||||
|
|
||||||
关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。
|
关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) 。
|
||||||
|
|
||||||
### Redis 可以做消息队列么?
|
### Redis 可以做消息队列么?
|
||||||
|
|
||||||
> 实际项目中也没见谁使用 Redis 来做消息队列,对于这部分知识点大家了解就好了。
|
> 实际项目中使用 Redis 来做消息队列的非常少,毕竟有更成熟的消息队列中间件可以用。
|
||||||
|
|
||||||
先说结论:**可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。**
|
先说结论:**可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。**
|
||||||
|
|
||||||
@ -261,6 +261,27 @@ RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检
|
|||||||
|
|
||||||
Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。
|
Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。
|
||||||
|
|
||||||
|
### 如何基于 Redis 实现延时任务?
|
||||||
|
|
||||||
|
> 类似的问题:
|
||||||
|
>
|
||||||
|
> - 订单在 10 分钟后未支付就失效,如何用 Redis 实现?
|
||||||
|
> - 红包 24 小时未被查收自动退还,如何用 Redis 实现?
|
||||||
|
|
||||||
|
基于 Redis 实现延时任务的功能无非就下面两种方案:
|
||||||
|
|
||||||
|
1. Redis 过期事件监听
|
||||||
|
2. Redisson 内置的延时队列
|
||||||
|
|
||||||
|
Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
|
||||||
|
|
||||||
|
Redisson 内置的延时队列具备下面这些优势:
|
||||||
|
|
||||||
|
1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。
|
||||||
|
2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
|
||||||
|
|
||||||
|
关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:[如何基于 Redis 实现延时任务?](./redis-delayed-task.md)。
|
||||||
|
|
||||||
## Redis 数据类型
|
## Redis 数据类型
|
||||||
|
|
||||||
关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/) :
|
关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/) :
|
||||||
|
@ -233,7 +233,7 @@ private int randomLevel() {
|
|||||||
/**
|
/**
|
||||||
* 默认情况下的高度为1,即只有自己一个节点
|
* 默认情况下的高度为1,即只有自己一个节点
|
||||||
*/
|
*/
|
||||||
private int leveCount = 1;
|
private int levelCount = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳表最底层的节点,即头节点
|
* 跳表最底层的节点,即头节点
|
||||||
@ -271,9 +271,9 @@ public void add(int value) {
|
|||||||
maxOfMinArr[i].forwards[i] = newNode;
|
maxOfMinArr[i].forwards[i] = newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果当前newNode高度大于跳表最高高度则更新leveCount
|
//如果当前newNode高度大于跳表最高高度则更新levelCount
|
||||||
if (leveCount < level) {
|
if (levelCount < level) {
|
||||||
leveCount = level;
|
levelCount = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -299,7 +299,7 @@ public void add(int value) {
|
|||||||
public Node get(int value) {
|
public Node get(int value) {
|
||||||
Node p = h;
|
Node p = h;
|
||||||
//找到小于value的最大值
|
//找到小于value的最大值
|
||||||
for (int i = leveCount - 1; i >= 0; i--) {
|
for (int i = levelCount - 1; i >= 0; i--) {
|
||||||
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
||||||
p = p.forwards[i];
|
p = p.forwards[i];
|
||||||
}
|
}
|
||||||
@ -334,8 +334,8 @@ public Node get(int value) {
|
|||||||
public void delete(int value) {
|
public void delete(int value) {
|
||||||
Node p = h;
|
Node p = h;
|
||||||
//找到各级节点小于value的最大值
|
//找到各级节点小于value的最大值
|
||||||
Node[] updateArr = new Node[leveCount];
|
Node[] updateArr = new Node[levelCount];
|
||||||
for (int i = leveCount - 1; i >= 0; i--) {
|
for (int i = levelCount - 1; i >= 0; i--) {
|
||||||
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
||||||
p = p.forwards[i];
|
p = p.forwards[i];
|
||||||
}
|
}
|
||||||
@ -344,7 +344,7 @@ public void delete(int value) {
|
|||||||
//查看原始层节点前驱是否等于value,若等于则说明存在要删除的值
|
//查看原始层节点前驱是否等于value,若等于则说明存在要删除的值
|
||||||
if (p.forwards[0] != null && p.forwards[0].data == value) {
|
if (p.forwards[0] != null && p.forwards[0].data == value) {
|
||||||
//从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点
|
//从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点
|
||||||
for (int i = leveCount - 1; i >= 0; i--) {
|
for (int i = levelCount - 1; i >= 0; i--) {
|
||||||
if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) {
|
if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) {
|
||||||
updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i];
|
updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i];
|
||||||
}
|
}
|
||||||
@ -352,8 +352,8 @@ public void delete(int value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//从最高级开始查看是否有一级索引为空,若为空则层级减1
|
//从最高级开始查看是否有一级索引为空,若为空则层级减1
|
||||||
while (leveCount > 1 && h.forwards[leveCount] == null) {
|
while (levelCount > 1 && h.forwards[levelCount - 1] == null) {
|
||||||
leveCount--;
|
levelCount--;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -379,7 +379,7 @@ public class SkipList {
|
|||||||
/**
|
/**
|
||||||
* 默认情况下的高度为1,即只有自己一个节点
|
* 默认情况下的高度为1,即只有自己一个节点
|
||||||
*/
|
*/
|
||||||
private int leveCount = 1;
|
private int levelCount = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳表最底层的节点,即头节点
|
* 跳表最底层的节点,即头节点
|
||||||
@ -436,9 +436,9 @@ public class SkipList {
|
|||||||
maxOfMinArr[i].forwards[i] = newNode;
|
maxOfMinArr[i].forwards[i] = newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果当前newNode高度大于跳表最高高度则更新leveCount
|
//如果当前newNode高度大于跳表最高高度则更新levelCount
|
||||||
if (leveCount < level) {
|
if (levelCount < level) {
|
||||||
leveCount = level;
|
levelCount = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -463,7 +463,7 @@ public class SkipList {
|
|||||||
public Node get(int value) {
|
public Node get(int value) {
|
||||||
Node p = h;
|
Node p = h;
|
||||||
//找到小于value的最大值
|
//找到小于value的最大值
|
||||||
for (int i = leveCount - 1; i >= 0; i--) {
|
for (int i = levelCount - 1; i >= 0; i--) {
|
||||||
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
||||||
p = p.forwards[i];
|
p = p.forwards[i];
|
||||||
}
|
}
|
||||||
@ -484,8 +484,8 @@ public class SkipList {
|
|||||||
public void delete(int value) {
|
public void delete(int value) {
|
||||||
Node p = h;
|
Node p = h;
|
||||||
//找到各级节点小于value的最大值
|
//找到各级节点小于value的最大值
|
||||||
Node[] updateArr = new Node[leveCount];
|
Node[] updateArr = new Node[levelCount];
|
||||||
for (int i = leveCount - 1; i >= 0; i--) {
|
for (int i = levelCount - 1; i >= 0; i--) {
|
||||||
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
while (p.forwards[i] != null && p.forwards[i].data < value) {
|
||||||
p = p.forwards[i];
|
p = p.forwards[i];
|
||||||
}
|
}
|
||||||
@ -494,7 +494,7 @@ public class SkipList {
|
|||||||
//查看原始层节点前驱是否等于value,若等于则说明存在要删除的值
|
//查看原始层节点前驱是否等于value,若等于则说明存在要删除的值
|
||||||
if (p.forwards[0] != null && p.forwards[0].data == value) {
|
if (p.forwards[0] != null && p.forwards[0].data == value) {
|
||||||
//从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点
|
//从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点
|
||||||
for (int i = leveCount - 1; i >= 0; i--) {
|
for (int i = levelCount - 1; i >= 0; i--) {
|
||||||
if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) {
|
if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) {
|
||||||
updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i];
|
updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i];
|
||||||
}
|
}
|
||||||
@ -502,8 +502,8 @@ public class SkipList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//从最高级开始查看是否有一级索引为空,若为空则层级减1
|
//从最高级开始查看是否有一级索引为空,若为空则层级减1
|
||||||
while (leveCount > 1 && h.forwards[leveCount] == null) {
|
while (levelCount > 1 && h.forwards[levelCount - 1] == null) {
|
||||||
leveCount--;
|
levelCount--;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<mxfile host="Electron" modified="2022-10-28T04:12:19.016Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.3.0 Chrome/104.0.5112.114 Electron/20.1.3 Safari/537.36" etag="avT9uCq-Id3320v8UcYK" version="20.3.0" type="device"><diagram id="4upf38i2IvtcCPuh0ZQV" name="Page-1">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</diagram></mxfile>
|
|
@ -1 +0,0 @@
|
|||||||
<mxfile host="Electron" modified="2022-10-28T04:02:04.271Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.3.0 Chrome/104.0.5112.114 Electron/20.1.3 Safari/537.36" etag="MQtLh8jZkKju4VXrEyHk" version="20.3.0" type="device"><diagram id="BmWg7-x1DEYjv1Iut_6U" name="Page-1">7Zrbcts2EEC/hjPtQzS8iLdHSRGTdpLUidpp89SBSIjCGCRoELKkfH0XJCDxZstJ7Vp2Jc9YwAJcAtyDXWBFw5llu3ccFeuPLMHUsM1kZzhvDdu2TDuELynZ15LQdGpBykmiOh0FC/IN6yuVdEMSXLY6CsaoIEVbGLM8x7FoyRDnbNvutmK0fdcCpbgnWMSI9qV/kkSsa2lg+0f5e0zStb6z5akJL1F8nXK2ydX9DNuJvCiKgro5Q1qXmmi5RgnbNkTO3HBmnDFRl7LdDFP5bPVjq6+L7mg9jJvjXDzkgpvpl6Vd7vPZ+yj8vP32x++/5J/eKC23iG6wnoZHQd90xUAtjFrs1ZPybjZMN7wpKztOoIPlFrtjI5RS+X3FWbKJMdfaYFi1wrpZPZGDbhuGCgaHynS7JgIvChTLli0wB7K1yCjULCiisqgpWJEdTuRwCKUzRhmvFDnj2WQc+iAvBWfXuNHixQFermTLNRaxtI0pK9oqpppahDJCJcu/YjHliOQljPQjy5lqX7ANr8a2FgIQtV35FFx46vKf7FCOUsZSilFBylHMsqohLquu0arWDsWmfteeqjv0jaothLnAu4ZIGfkdZhkWHFSaqnWsgVML0nFUfdvAW4nWDbK1DKkFlR40H6GCguLqOxizBxjrmj9PJnItQy2mqCxJ3LZ69eBr12G5qt4wLSw605QGxDsi/lLXyPJXadiRq2pvd8rOVWWvCbgThxoh7RXs8wak1Lc9sdRx0vKHfYwamLgDmGgZxxQJctv2okPsqDtcMVI5FEWpa3Uo7eJXz0dd1XRrHUXeuKPI7ygSiKdY9BRVKB+m/eN0O6fpboAslzGB4PMBLTG9YiURhOXQtmRCAAng3yhJpSAG+4L7dKZU9pweYk2D+lX1aSidqGsFKwbp7blE0wzMqId9gsq1dK16QRRyGtkulfF/RFjpjwgE43KE81tMWYH//inDZQlx8ufvWKmtVXfGy+rhftd1OiCO+37XHlhQ9lP53fFpMrV14z0leVLxdiL+LiWEOPmwPAgOaP62EaAFKzlnAim4Q7Mfp1eu/BuM09Wnj079ef3x2zc7HJk2RLAuSY7bJ8nSwkdHyb04uYuTq5ycf2ZOzruQeSGz2geGHTK9ZybTv5B5IbMi0zszMoMBMus0TVmg/MeTPjMw/iY7Jn2W/JjvOaSB6ls8dRpoFcQ4jofQXgbu2L3/3P8qqAu7B+znTgOFp/1hIw2UM3mS+Dc5IF1+7BxQKRAX/WxVJY4I1cOFuajamSN1MnGk3EWdR7mnnzpvnkmCye/uB7p5oYcmmAL3eRNMeh1fNhL/941Ej8TnPnxZAz8fzV0jiIzQMuahAbEkgIJnTD0jCGQBqpOohy/MX7SdfRskFQyaMV6JejB3Sc1IktC7NhS8TmadDAJN5JzXQpPbTZwHfZq8/5SmgVR6nx3AKgyMMKpAg4L/omnSDmyhhm+9Frq8bqLogdvPp6NrIB3eZwkwmwTGxJKScG4E/SNKn66GPW1db4VA04yi13/g6P7ufNjINSzuD1jceQSLz/fO18+EF5vC/fTF5zdsnHiD7zZc9k0vc9/Uw3AA1rv3Tab/bPumQTLt+wJdYEzHxjS8bJteCExPt22C6vH1sPo4eXwHz5n/Aw==</diagram></mxfile>
|
|
@ -75,7 +75,9 @@ tag:
|
|||||||
|
|
||||||
**消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。
|
**消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。
|
||||||
|
|
||||||
消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。
|
例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消息即可。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。
|
另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ JDK、JRE、JVM、JIT 这四者的关系如下图所示。
|
|||||||
|
|
||||||
根据维基百科介绍:
|
根据维基百科介绍:
|
||||||
|
|
||||||
> 为了改善编译语言的效率而发展出的[即时编译](https://zh.wikipedia.org/wiki/即時編譯)技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成[字节码](https://zh.wikipedia.org/wiki/字节码)。到执行期时,再将字节码直译,之后执行。[Java](https://zh.wikipedia.org/wiki/Java)与[LLVM](https://zh.wikipedia.org/wiki/LLVM)是这种技术的代表产物。
|
> 为了改善解释语言的效率而发展出的[即时编译](https://zh.wikipedia.org/wiki/即時編譯)技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成[字节码](https://zh.wikipedia.org/wiki/字节码)。到执行期时,再将字节码直译,之后执行。[Java](https://zh.wikipedia.org/wiki/Java)与[LLVM](https://zh.wikipedia.org/wiki/LLVM)是这种技术的代表产物。
|
||||||
>
|
>
|
||||||
> 相关阅读:[基本功 | Java 即时编译器原理解析及实践](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html)
|
> 相关阅读:[基本功 | Java 即时编译器原理解析及实践](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html)
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ head:
|
|||||||
|
|
||||||
### Java 集合概览
|
### Java 集合概览
|
||||||
|
|
||||||
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 和 `Queue`。
|
Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 、 `Queue`、`Map`。
|
||||||
|
|
||||||
Java 集合框架如下图所示:
|
Java 集合框架如下图所示:
|
||||||
|
|
||||||
|
@ -80,6 +80,15 @@ head:
|
|||||||
|
|
||||||
实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。
|
实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。
|
||||||
|
|
||||||
|
`NavigableMap` 接口提供了丰富的方法来探索和操作键值对:
|
||||||
|
|
||||||
|
1. **定向搜索**: `ceilingEntry()`, `floorEntry()`, `higherEntry()`和 `lowerEntry()` 等方法可以用于定位大于、小于、大于等于、小于等于给定键的最接近的键值对。
|
||||||
|
2. **子集操作**: `subMap()`, `headMap()`和 `tailMap()` 方法可以高效地创建原集合的子集视图,而无需复制整个集合。
|
||||||
|
3. **逆序视图**:`descendingMap()` 方法返回一个逆序的 `NavigableMap` 视图,使得可以反向迭代整个 `TreeMap`。
|
||||||
|
4. **边界操作**: `firstEntry()`, `lastEntry()`, `pollFirstEntry()`和 `pollLastEntry()` 等方法可以方便地访问和移除元素。
|
||||||
|
|
||||||
|
这些方法都是基于红黑树数据结构的属性实现的,红黑树保持平衡状态,从而保证了搜索操作的时间复杂度为 O(log n),这让 `TreeMap` 成为了处理有序集合搜索问题的强大工具。
|
||||||
|
|
||||||
实现`SortedMap`接口让 `TreeMap` 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:
|
实现`SortedMap`接口让 `TreeMap` 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@ -138,7 +147,7 @@ TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**综上,相比于`HashMap`来说 `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。**
|
**综上,相比于`HashMap`来说, `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。**
|
||||||
|
|
||||||
### HashSet 如何检查重复?
|
### HashSet 如何检查重复?
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ icon: home
|
|||||||
title: Java 面试指南
|
title: Java 面试指南
|
||||||
heroImage: /logo.svg
|
heroImage: /logo.svg
|
||||||
heroText: JavaGuide
|
heroText: JavaGuide
|
||||||
tagline: 「Java学习 + 面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide!
|
tagline: 「Java学习 + 面试指南」涵盖 Java 程序员需要掌握的核心知识
|
||||||
actions:
|
actions:
|
||||||
- text: 开始阅读
|
- text: 开始阅读
|
||||||
link: /home.md
|
link: /home.md
|
||||||
|
Loading…
x
Reference in New Issue
Block a user