1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-25 02:27:10 +08:00

Merge branch 'Snailclimb:main' into main

This commit is contained in:
复制粘贴委员会首席技术专家, 加里敦大学著名bug架构师 2024-03-10 18:32:43 +08:00 committed by GitHub
commit 88cf8e4c9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 87 additions and 39 deletions

View File

@ -99,7 +99,7 @@ tag:
![](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E6%A0%88.png) ![](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E6%A0%88.png)
### 3.2. 栈的常见应用常见应用场景 ### 3.2. 栈的常见应用场景
当我们我们要处理的数据只涉及在一端插入和删除数据,并且满足 **后进先出LIFO, Last In First Out** 的特性时,我们就可以使用栈这个数据结构。 当我们我们要处理的数据只涉及在一端插入和删除数据,并且满足 **后进先出LIFO, Last In First Out** 的特性时,我们就可以使用栈这个数据结构。
@ -154,7 +154,12 @@ public boolean isValid(String s){
#### 3.2.4. 维护函数调用 #### 3.2.4. 维护函数调用
最后一个被调用的函数必须先完成执行,符合栈的 **后进先出LIFO, Last In First Out** 特性。 最后一个被调用的函数必须先完成执行,符合栈的 **后进先出LIFO, Last In First Out** 特性。
例如递归函数调用可以通过栈来实现,每次递归调用都会将参数和返回地址压栈。
#### 3.2.5 深度优先遍历DFS
在深度优先搜索过程中,栈被用来保存搜索路径,以便回溯到上一层。
### 3.3. 栈的实现 ### 3.3. 栈的实现
@ -316,13 +321,14 @@ myStack.pop();//报错java.lang.IllegalArgumentException: Stack is empty.
虽然优先队列的底层并非严格的线性结构,但是在我们使用的过程中,我们是感知不到**堆**的,从使用者的眼中优先队列可以被认为是一种线性的数据结构:一种会自动排序的线性队列。 虽然优先队列的底层并非严格的线性结构,但是在我们使用的过程中,我们是感知不到**堆**的,从使用者的眼中优先队列可以被认为是一种线性的数据结构:一种会自动排序的线性队列。
### 4.3. 常见应用场景 ### 4.3. 队列的常见应用场景
当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。 当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。
- **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。 - **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。
- **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如:`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。 - **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如:`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。
- 栈:双端队列天生便可以实现栈的全部功能(`push``pop``peek`),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列Deque来实现栈。 - 栈:双端队列天生便可以实现栈的全部功能(`push``pop``peek`),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列Deque来实现栈。
- 广度优先搜索BFS在图的广度优先搜索过程中队列被用于存储待访问的节点保证按照层次顺序遍历图的节点。
- Linux 内核进程队列(按优先级排队) - Linux 内核进程队列(按优先级排队)
- 现实生活中的派对,播放器上的播放列表; - 现实生活中的派对,播放器上的播放列表;
- 消息队列 - 消息队列

View File

@ -55,6 +55,6 @@ SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器
3. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。 3. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。
4. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。 4. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。
然而NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。**这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号同样是不规范的行为。但是尽管如此NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。 然而NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,<b>NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。</b>这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号同样是不规范的行为。但是尽管如此NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。
<!-- @include: @article-footer.snippet.md --> <!-- @include: @article-footer.snippet.md -->

View File

@ -402,7 +402,7 @@ Thread[线程 2,5,main]waiting get resource1
上面提到的 **破坏** 死锁产生的四个必要条件之一就可以成功 **预防系统发生死锁** ,但是会导致 **低效的进程运行****资源使用率** 。而死锁的避免相反,它的角度是允许系统中**同时存在四个必要条件** ,只要掌握并发进程中与每个进程有关的资源动态申请情况,做出 **明智和合理的选择** ,仍然可以避免死锁,因为四大条件仅仅是产生死锁的必要条件。 上面提到的 **破坏** 死锁产生的四个必要条件之一就可以成功 **预防系统发生死锁** ,但是会导致 **低效的进程运行****资源使用率** 。而死锁的避免相反,它的角度是允许系统中**同时存在四个必要条件** ,只要掌握并发进程中与每个进程有关的资源动态申请情况,做出 **明智和合理的选择** ,仍然可以避免死锁,因为四大条件仅仅是产生死锁的必要条件。
我们将系统的状态分为 **安全状态****不安全状态** ,每当在申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。 我们将系统的状态分为 **安全状态****不安全状态** ,每当在申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。
> 如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁。 > 如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁。

View File

@ -100,7 +100,7 @@ update tb_student A set A.age='19' where A.name=' 张三 ';
我们来给张三修改下年龄在实际数据库肯定不会设置年龄这个字段的不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走只不过执行更新的时候肯定要记录日志啦这就会引入日志模块了MySQL 自带的日志模块是 **binlog归档日志** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log重做日志**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: 我们来给张三修改下年龄在实际数据库肯定不会设置年龄这个字段的不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走只不过执行更新的时候肯定要记录日志啦这就会引入日志模块了MySQL 自带的日志模块是 **binlog归档日志** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log重做日志**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下:
- 先查询到张三这一条数据,如果有缓存,也是会用到缓存 - 先查询到张三这一条数据,不会走查询缓存,因为更新语句会导致与该表相关的查询缓存失效
- 然后拿到查询的语句,把 age 改为 19然后调用引擎 API 接口写入这一行数据InnoDB 引擎把数据保存在内存中,同时记录 redo log此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。 - 然后拿到查询的语句,把 age 改为 19然后调用引擎 API 接口写入这一行数据InnoDB 引擎把数据保存在内存中,同时记录 redo log此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。
- 执行器收到通知后记录 binlog然后调用引擎接口提交 redo log 为提交状态。 - 执行器收到通知后记录 binlog然后调用引擎接口提交 redo log 为提交状态。
- 更新完成。 - 更新完成。

View File

@ -182,8 +182,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
1. **唯一索引(Unique Key)**:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 1. **唯一索引(Unique Key)**:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
2. **普通索引(Index)**:普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。 2. **普通索引(Index)**:普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。
3. **前缀索引(Prefix)**:前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, 3. **前缀索引(Prefix)**:前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
因为只取前几个字符。
4. **全文索引(Full Text)**:全文索引主要是为了检索大文本数据中的关键字的信息是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引5.6 之后 InnoDB 也支持了全文索引。 4. **全文索引(Full Text)**:全文索引主要是为了检索大文本数据中的关键字的信息是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引5.6 之后 InnoDB 也支持了全文索引。
二级索引: 二级索引:

View File

@ -112,7 +112,7 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
![](https://oss.javaguide.cn/github/javaguide/10.png) ![](https://oss.javaguide.cn/github/javaguide/10.png)
在个**日志文件组**中还有两个重要的属性,分别是 `write pos、checkpoint` 个**日志文件组**中还有两个重要的属性,分别是 `write pos、checkpoint`
- **write pos** 是当前记录的位置,一边写一边后移 - **write pos** 是当前记录的位置,一边写一边后移
- **checkpoint** 是当前要擦除的位置,也是往后推移 - **checkpoint** 是当前要擦除的位置,也是往后推移

View File

@ -9,7 +9,7 @@ category: 分布式
日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。 日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。
我们现实生活中也有各种 ID比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应 我们现实生活中也有各种 ID比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应一个地址。
简单来说,**ID 就是数据的唯一标识**。 简单来说,**ID 就是数据的唯一标识**。

View File

@ -30,7 +30,7 @@ category: 分布式
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
对于单机多线程来说,在 Java 中,我们通常使用 `ReetrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。 对于单机多线程来说,在 Java 中,我们通常使用 `ReentrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
下面是我对本地锁画的一张示意图。 下面是我对本地锁画的一张示意图。

View File

@ -20,8 +20,8 @@ head:
冷热数据到底如何区分呢?有两个常见的区分方法: 冷热数据到底如何区分呢?有两个常见的区分方法:
1. **时间维度区分**:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。例如,订单系统可以将 1 年的订单数据作为冷数据1 年内的订单数据作为热数据。这种方法适用于数据的访问频率和时间有较强的相关性的场景。 1. **时间维度区分**:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。例如,订单系统可以将 1 年的订单数据作为冷数据1 年内的订单数据作为热数据。这种方法适用于数据的访问频率和时间有较强的相关性的场景。
2. **访问率区分**:将高频访问的数据视为热数据,低频访问的数据视为冷数据。例如,内容系统可以将浏览量非常低的文章作为冷数据,浏览量较高的文章作为热数据。这种方法需要记录数据的访问频率,成本较高,适合访问频率和数据本身有较强的相关性的场景。 2. **访问率区分**:将高频访问的数据视为热数据,低频访问的数据视为冷数据。例如,内容系统可以将浏览量非常低的文章作为冷数据,浏览量较高的文章作为热数据。这种方法需要记录数据的访问频率,成本较高,适合访问频率和数据本身有较强的相关性的场景。
几年前的数据并不一定都是热数据,例如一些优质文章发表几年后依然有很多人访问,大部分普通用户新发表的文章却基本没什么人访问。 几年前的数据并不一定都是热数据,例如一些优质文章发表几年后依然有很多人访问,大部分普通用户新发表的文章却基本没什么人访问。

View File

@ -196,16 +196,35 @@ MySQL 主从同步延时是指从库的数据落后于主库的数据,这种
- 数据库中的数据占用的空间越来越大,备份时间越来越长。 - 数据库中的数据占用的空间越来越大,备份时间越来越长。
- 应用的并发量太大。 - 应用的并发量太大。
不过,分库分表的成本太高,如非必要尽量不要采用。而且,并不一定是单表千万级数据量就要分表,毕竟每张表包含的字段不同,它们在不错的性能下能够存放的数据量也不同,还是要具体情况具体分析。
之前看过一篇文章分析 “[InnoDB 中高度为 3 的 B+ 树最多可以存多少数据](https://juejin.cn/post/7165689453124517896)”,写的挺不错,感兴趣的可以看看。
### 常见的分片算法有哪些? ### 常见的分片算法有哪些?
分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。 分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。
- **哈希分片**:求指定 key比如 id 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。 常见的分片算法有:
- **范围分片**按照特性的范围区间比如时间区间、ID 区间)来分配数据,比如 将 `id``1~299999` 的记录分到第一个库, `300000~599999` 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
- **哈希分片**:求指定分片键的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。哈希分片可以使每个表的数据分布相对均匀,但对动态伸缩(例如新增一个表或者库)不友好。
- **范围分片**按照特定的范围区间比如时间区间、ID 区间)来分配数据,比如 将 `id``1~299999` 的记录分到第一个表, `300000~599999` 的分到第二个表。范围分片适合需要经常进行范围查找且数据分布均匀的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
- **映射表分片**:使用一个单独的表(称为映射表)来存储分片键和分片位置的对应关系。映射表分片策略可以支持任何类型的分片算法,如哈希分片、范围分片等。映射表分片策略是可以灵活地调整分片规则,不需要修改应用程序代码或重新分布数据。不过,这种方式需要维护额外的表,还增加了查询的开销和复杂度。
- **一致性哈希分片**:将哈希空间组织成一个环形结构,将分片键和节点(数据库或表)都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上,解决了传统哈希对动态伸缩不友好的问题。
- **地理位置分片**:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。 - **地理位置分片**:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
- **融合算法**:灵活组合多种分片算法,比如将哈希分片和范围分片组合。 - **融合算法分片**:灵活组合多种分片算法,比如将哈希分片和范围分片组合。
- …… - ……
### 分片键如何选择?
分片键Sharding Key是数据分片的关键字段。分片键的选择非常重要它关系着数据的分布和查询效率。一般来说分片键应该具备以下特点
- 具有共性,即能够覆盖绝大多数的查询场景,尽量减少单次查询所涉及的分片数量,降低数据库压力;
- 具有离散性,即能够将数据均匀地分散到各个分片上,避免数据倾斜和热点问题;
- 具有稳定性,即分片键的值不会发生变化,避免数据迁移和一致性问题;
- 具有扩展性,即能够支持分片的动态增加和减少,避免数据重新分片的开销。
实际项目中,分片键很难满足上面提到的所有特点,需要权衡一下。并且,分片键可以是表中多个字段的组合,例如取用户 ID 后四位作为订单 ID 后缀。
### 分库分表会带来什么问题呢? ### 分库分表会带来什么问题呢?
记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。 记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。
@ -214,7 +233,7 @@ MySQL 主从同步延时是指从库的数据落后于主库的数据,这种
- **join 操作**:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。不过,很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。对于需要用到 join 操作的地方,可以采用多次查询业务层进行数据组装的方法。不过,这种方法需要考虑业务上多次查询的事务性的容忍度。 - **join 操作**:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。不过,很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。对于需要用到 join 操作的地方,可以采用多次查询业务层进行数据组装的方法。不过,这种方法需要考虑业务上多次查询的事务性的容忍度。
- **事务问题**:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。这个时候,我们就需要引入分布式事务了。关于分布式事务常见解决方案总结,网站上也有对应的总结:<https://javaguide.cn/distributed-system/distributed-transaction.html> - **事务问题**:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。这个时候,我们就需要引入分布式事务了。关于分布式事务常见解决方案总结,网站上也有对应的总结:<https://javaguide.cn/distributed-system/distributed-transaction.html>
- **分布式 ID**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,网站上也有对应的总结:<https://javaguide.cn/distributed-system/distributed-id.html> - **分布式 ID**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,可以看我写的这篇文章:[分布式 ID 介绍&实现方案总结](https://javaguide.cn/distributed-system/distributed-id.html)
- **跨库聚合查询问题**:分库分表会导致常规聚合查询操作,如 group byorder by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。为了实现这些操作,需要编写复杂的业务代码,或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本,以及影响查询的性能和可扩展性。 - **跨库聚合查询问题**:分库分表会导致常规聚合查询操作,如 group byorder by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。为了实现这些操作,需要编写复杂的业务代码,或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本,以及影响查询的性能和可扩展性。
- …… - ……

View File

@ -162,17 +162,23 @@ icon: jianli
- 使用 xxx 技术优化了 xxx 模块,响应时间从 2s 降低到 0.2s。 - 使用 xxx 技术优化了 xxx 模块,响应时间从 2s 降低到 0.2s。
- …… - ……
个人职责介绍示例 个人职责介绍示例(这里只是举例,不要照搬,结合自己项目经历自己去写,不然面试的时候容易被问倒)
- 基于 Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权,使用 RBAC 权限模型实现动态权限控制。 - 基于 Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权,使用 RBAC 权限模型实现动态权限控制。
- 参与项目订单模块的开发,负责订单创建、删除、查询等功能。 - 参与项目订单模块的开发,负责订单创建、删除、查询等功能,基于 Spring 状态机实现订单状态流转。
- 整合 Canal + RocketMQ 将 MySQL 增量数据(如商品、订单数据)同步到 ES。 - 商品和订单搜索场景引入 Elasticsearch并且实现了相关商品推荐以及搜索提示功能。
- 整合 Canal + RabbitMQ 将 MySQL 增量数据(如商品、订单数据)同步到 Elasticsearch。
- 利用 RabbitMQ 官方提供的延迟队列插件实现延时任务场景比如订单超时自动取消、优惠券过期提醒、退款处理。
- 消息推送系统引入 RabbitMQ 实现异步处理、削峰填谷和服务解耦,最高推送速度 10w/s单日最大消息量 2000 万。
- 使用 MAT 工具分析 dump 文件解决了广告服务新版本上线后导致大量的服务超时告警的问题。
- 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。 - 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。
- 基于 EasyExcel 实现广告投放数据的导入导出,通过 MyBatis 批处理插入数据,基于任务表实现异步。
- 负责用户统计模块的开发,使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。 - 负责用户统计模块的开发,使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。
- 使用 Sharding-JDBC 以用户 ID 后 4 位作为 Shard Key 对订单表进行分库分表,共 3 个库,每个库 2 个订单表,单表数据量保持在 500w 以下。自定义雪花算法生成订单 ID 的规则,把分片键同时作为的订单 ID 一部分,避免了额外存储订单 ID 与路由键的关系。 - 基于 Sentinel 对核心场景(如用户登入注册、收货地址查询等)进行限流、降级,保护系统,提升用户体验
- 热门数据(如首页、热门博客)使用 Redis+Caffeine 两级缓存解决了缓存击穿和穿透问题查询速度毫秒级QPS 30w+。 - 热门数据(如首页、热门博客)使用 Redis+Caffeine 两级缓存解决了缓存击穿和穿透问题查询速度毫秒级QPS 30w+。
- 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低为 0.2s。 - 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低为 0.2s。
- 搭建 EasyMock 服务,用于模拟第三方平台接口,方便了在网络隔离情况下的接口对接工作。 - 搭建 EasyMock 服务,用于模拟第三方平台接口,方便了在网络隔离情况下的接口对接工作。
- 基于 SkyWalking + Elasticsearch 搭建分布式链路追踪系统实现全链路监控。
**4、如果你觉得你的项目技术比较落后的话可以自己私下进行改进。重要的是让项目比较有亮点通过什么方式就无所谓了。** **4、如果你觉得你的项目技术比较落后的话可以自己私下进行改进。重要的是让项目比较有亮点通过什么方式就无所谓了。**

View File

@ -351,7 +351,7 @@ public class BigDecimalUtil {
} }
``` ```
相关 issue[建议对保留规则设置为 RoundingMode.HALF_EVEN,即四舍六入五成双](<[#2129](https://github.com/Snailclimb/JavaGuide/issues/2129)>) 。 相关 issue[建议对保留规则设置为 RoundingMode.HALF_EVEN,即四舍六入五成双,#2129](https://github.com/Snailclimb/JavaGuide/issues/2129) 。
![RoundingMode.HALF_EVEN](https://oss.javaguide.cn/github/javaguide/java/basis/RoundingMode.HALF_EVEN.png) ![RoundingMode.HALF_EVEN](https://oss.javaguide.cn/github/javaguide/java/basis/RoundingMode.HALF_EVEN.png)

View File

@ -88,6 +88,8 @@ JREJava Runtime Environment 是 Java 运行时环境。它是运行已编
我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 **JITJust in Time Compilation** 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 **JITJust in Time Compilation** 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言**
> 🌈 拓展:[有关 JIT 的实现细节: JVM C1、C2 编译器](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw)
![Java程序转变为机器代码的过程](https://oss.javaguide.cn/github/javaguide/java/basis/java-code-to-machine-code-with-jit.png) ![Java程序转变为机器代码的过程](https://oss.javaguide.cn/github/javaguide/java/basis/java-code-to-machine-code-with-jit.png)
> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。 > HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。

View File

@ -198,7 +198,7 @@ GitHub 地址:[https://github.com/protocolbuffers/protobuf](https://github.com
### ProtoStuff ### ProtoStuff
由于 Protobuf 的易用性,它的哥哥 Protostuff 诞生了。 由于 Protobuf 的易用性较差,它的哥哥 Protostuff 诞生了。
protostuff 基于 Google protobuf但是提供了更多的功能和更简易的用法。虽然更加易用但是不代表 ProtoStuff 性能更差。 protostuff 基于 Google protobuf但是提供了更多的功能和更简易的用法。虽然更加易用但是不代表 ProtoStuff 性能更差。

View File

@ -28,7 +28,7 @@ Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增
3. 当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。 3. 当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。
4. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。 4. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。
总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 `put``take``offfer`、`poll` 等 API 即可实现多线程之间的生产和消费。 总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 `put``take``offer`、`poll` 等 API 即可实现多线程之间的生产和消费。
这也使得阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 `workQueue` 中。 这也使得阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 `workQueue` 中。

View File

@ -111,15 +111,15 @@ public class LRUCache<K, V> extends LinkedHashMap<K, V> {
} }
``` ```
测试代码如下,笔者初始化缓存容量为 2,然后按照次序先后添加 4 个元素。 测试代码如下,笔者初始化缓存容量为 3,然后按照次序先后添加 4 个元素。
```java ```java
LRUCache < Integer, String > cache = new LRUCache < > (2); LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "one"); cache.put(1, "one");
cache.put(2, "two"); cache.put(2, "two");
cache.put(3, "three"); cache.put(3, "three");
cache.put(4, "four"); cache.put(4, "four");
for (int i = 0; i < 4; i++) { for (int i = 0; i <= 4; i++) {
System.out.println(cache.get(i)); System.out.println(cache.get(i));
} }
``` ```
@ -128,7 +128,7 @@ for (int i = 0; i < 4; i++) {
```java ```java
null null
null two
three three
four four
``` ```

View File

@ -28,7 +28,7 @@ head:
### 何为线程? ### 何为线程?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。 Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。

View File

@ -285,9 +285,9 @@ public final native boolean compareAndSwapLong(Object o, long offset, long expec
关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。
### 乐观锁存在哪些问题? ### CAS 算法存在哪些问题?
ABA 问题是乐观锁最常见的问题。 ABA 问题是 CAS 算法最常见的问题。
#### ABA 问题 #### ABA 问题
@ -317,7 +317,7 @@ CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升pause 指令有两个作用: 如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升pause 指令有两个作用:
1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。 2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
#### 只能保证一个共享变量的原子操作 #### 只能保证一个共享变量的原子操作

View File

@ -446,7 +446,7 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
> >
> IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N按道理来说WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。 > IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N按道理来说WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。
也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用! 也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用!
### 如何动态修改线程池的参数? ### 如何动态修改线程池的参数?
@ -466,7 +466,7 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-methods.png) ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-methods.png)
格外需要注意的是`corePoolSize` 程序运行期间的时候,我们调用 `setCorePoolSize`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。 格外需要注意的是`corePoolSize` 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。
另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。 另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。

View File

@ -122,9 +122,9 @@ public final native boolean compareAndSwapLong(Object o, long offset, long expec
关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。
## 乐观锁存在哪些问题? ## CAS 算法存在哪些问题?
ABA 问题是乐观锁最常见的问题。 ABA 问题是 CAS 算法最常见的问题。
### ABA 问题 ### ABA 问题
@ -165,7 +165,7 @@ CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS
- 高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。 - 高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
- 乐观锁一般会使用版本号机制或 CAS 算法实现CAS 算法相对来说更多一些,这里需要格外注意。 - 乐观锁一般会使用版本号机制或 CAS 算法实现CAS 算法相对来说更多一些,这里需要格外注意。
- CAS 的全称是 **Compare And Swap比较与交换** 用于实现乐观锁被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。 - CAS 的全称是 **Compare And Swap比较与交换** 用于实现乐观锁被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
- 乐观锁的问题ABA 问题、循环时间长开销大、只能保证一个共享变量的原子操作。 - CAS 算法的问题ABA 问题、循环时间长开销大、只能保证一个共享变量的原子操作。
## 参考 ## 参考

View File

@ -627,7 +627,7 @@ public class MapAndFlatMapExample {
.collect(Collectors.toList()); .collect(Collectors.toList());
System.out.println("Using map:"); System.out.println("Using map:");
System.out.println(mapResult); mapResult.forEach(arrays-> System.out.println(Arrays.toString(arrays)));
List<String> flatMapResult = listOfArrays.stream() List<String> flatMapResult = listOfArrays.stream()
.flatMap(array -> Arrays.stream(array).map(String::toUpperCase)) .flatMap(array -> Arrays.stream(array).map(String::toUpperCase))

View File

@ -315,7 +315,7 @@ WebSocket 的工作过程可以分为以下几个步骤:
另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。 另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。
SpringBoot 整合 Websocket先引入 Websocket 相关的工具包,和 SSE 相比额外的开发成本。 SpringBoot 整合 WebSocket先引入 WebSocket 相关的工具包,和 SSE 相比有额外的开发成本。
```xml ```xml
<!-- 引入websocket --> <!-- 引入websocket -->
@ -374,6 +374,22 @@ public class WebSocketServer {
} }
``` ```
服务端还需要注入`ServerEndpointerExporter`,这个 Bean 就会自动注册使用了`@ServerEndpoint`注解的 WebSocket 服务器。
```java
@Configuration
public class WebSocketConfiguration {
/**
* 用于注册使用了 @ServerEndpoint 注解的 WebSocket 服务器
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
```
前端初始化打开 WebSocket 连接,并监听连接状态,接收服务端数据或向服务端发送数据。 前端初始化打开 WebSocket 连接,并监听连接状态,接收服务端数据或向服务端发送数据。
```javascript ```javascript