Skip to content

Commit 9d11263

Browse files
committed
[docs update&fix]修正快排算法&补充WebSocket面试问题&优化Redis慢查询
1 parent 2e7aa2a commit 9d11263

File tree

4 files changed

+172
-66
lines changed

4 files changed

+172
-66
lines changed

docs/cs-basics/algorithms/10-classical-sorting-algorithms.md

+51-22
Original file line numberDiff line numberDiff line change
@@ -367,31 +367,60 @@ public static int[] merge(int[] arr_1, int[] arr_2) {
367367

368368
### 代码实现
369369

370-
> 来源:[使用 Java 实现快速排序(详解)](https://segmentfault.com/a/1190000040022056)
371-
372370
```java
373-
public static int partition(int[] array, int low, int high) {
374-
int pivot = array[high];
375-
int pointer = low;
376-
for (int i = low; i < high; i++) {
377-
if (array[i] <= pivot) {
378-
int temp = array[i];
379-
array[i] = array[pointer];
380-
array[pointer] = temp;
381-
pointer++;
371+
import java.util.concurrent.ThreadLocalRandom;
372+
373+
class Solution {
374+
public int[] sortArray(int[] a) {
375+
quick(a, 0, a.length - 1);
376+
return a;
377+
}
378+
379+
// 快速排序的核心递归函数
380+
void quick(int[] a, int left, int right) {
381+
if (left >= right) { // 递归终止条件:区间只有一个或没有元素
382+
return;
382383
}
383-
System.out.println(Arrays.toString(array));
384+
int p = partition(a, left, right); // 分区操作,返回分区点索引
385+
quick(a, left, p - 1); // 对左侧子数组递归排序
386+
quick(a, p + 1, right); // 对右侧子数组递归排序
384387
}
385-
int temp = array[pointer];
386-
array[pointer] = array[high];
387-
array[high] = temp;
388-
return pointer;
389-
}
390-
public static void quickSort(int[] array, int low, int high) {
391-
if (low < high) {
392-
int position = partition(array, low, high);
393-
quickSort(array, low, position - 1);
394-
quickSort(array, position + 1, high);
388+
389+
// 分区函数:将数组分为两部分,小于基准值的在左,大于基准值的在右
390+
int partition(int[] a, int left, int right) {
391+
// 随机选择一个基准点,避免最坏情况(如数组接近有序)
392+
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
393+
swap(a, left, idx); // 将基准点放在数组的最左端
394+
int pv = a[left]; // 基准值
395+
int i = left + 1; // 左指针,指向当前需要检查的元素
396+
int j = right; // 右指针,从右往左寻找比基准值小的元素
397+
398+
while (i <= j) {
399+
// 左指针向右移动,直到找到一个大于等于基准值的元素
400+
while (i <= j && a[i] < pv) {
401+
i++;
402+
}
403+
// 右指针向左移动,直到找到一个小于等于基准值的元素
404+
while (i <= j && a[j] > pv) {
405+
j--;
406+
}
407+
// 如果左指针尚未越过右指针,交换两个不符合位置的元素
408+
if (i <= j) {
409+
swap(a, i, j);
410+
i++;
411+
j--;
412+
}
413+
}
414+
// 将基准值放到分区点位置,使得基准值左侧小于它,右侧大于它
415+
swap(a, j, left);
416+
return j;
417+
}
418+
419+
// 交换数组中两个元素的位置
420+
void swap(int[] a, int i, int j) {
421+
int t = a[i];
422+
a[i] = a[j];
423+
a[j] = t;
395424
}
396425
}
397426
```

docs/cs-basics/network/other-network-questions.md

+57-10
Original file line numberDiff line numberDiff line change
@@ -336,23 +336,70 @@ WebSocket 的工作过程可以分为以下几个步骤:
336336

337337
另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。
338338

339+
### WebSocket 与短轮询、长轮询的区别
340+
341+
这三种方式,都是为了解决“**客户端如何及时获取服务器最新数据,实现实时更新**”的问题。它们的实现方式和效率、实时性差异较大。
342+
343+
**1.短轮询(Short Polling)**
344+
345+
- **原理**:客户端每隔固定时间(如 5 秒)发起一次 HTTP 请求,询问服务器是否有新数据。服务器收到请求后立即响应。
346+
- **优点**:实现简单,兼容性好,直接用常规 HTTP 请求即可。
347+
- **缺点**
348+
- **实时性一般**:消息可能在两次轮询间到达,用户需等到下次请求才知晓。
349+
- **资源浪费大**:反复建立/关闭连接,且大多数请求收到的都是“无新消息”,极大增加服务器和网络压力。
350+
351+
**2.长轮询(Long Polling)**
352+
353+
- **原理**:客户端发起请求后,若服务器暂时无新数据,则会保持连接,直到有新数据或超时才响应。客户端收到响应后立即发起下一次请求,实现“伪实时”。
354+
- **优点**
355+
- **实时性较好**:一旦有新数据可立即推送,无需等待下次定时请求。
356+
- **空响应减少**:减少了无效的空响应,提升了效率。
357+
- **缺点**
358+
- **服务器资源占用高**:需长时间维护大量连接,消耗服务器线程/连接数。
359+
- **资源浪费大**:每次响应后仍需重新建立连接,且依然基于 HTTP 单向请求-响应机制。
360+
361+
**3. WebSocket**
362+
363+
- **原理**:客户端与服务器通过一次 HTTP Upgrade 握手后,建立一条持久的 TCP 连接。之后,双方可以随时、主动地发送数据,实现真正的全双工、低延迟通信。
364+
- **优点**
365+
- **实时性强**:数据可即时双向收发,延迟极低。
366+
- **资源效率高**:连接持续,无需反复建立/关闭,减少资源消耗。
367+
- **功能强大**:支持服务端主动推送消息、客户端主动发起通信。
368+
- **缺点**
369+
- **使用限制**:需要服务器和客户端都支持 WebSocket 协议。对连接管理有一定要求(如心跳保活、断线重连等)。
370+
- **实现麻烦**:实现起来比短轮询和长轮询要更麻烦一些。
371+
372+
![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png)
373+
339374
### SSE 与 WebSocket 有什么区别?
340375

341-
> 摘自[Web 实时消息推送详解](https://javaguide.cn/system-design/web-real-time-message-push.html)
376+
SSE (Server-Sent Events) 和 WebSocket 都是用来实现服务器向浏览器实时推送消息的技术,让网页内容能自动更新,而不需要用户手动刷新。虽然目标相似,但它们在工作方式和适用场景上有几个关键区别:
377+
378+
1. **通信方式:**
379+
- **SSE:** **单向通信**。只有服务器能向客户端(浏览器)发送数据。客户端不能通过同一个连接向服务器发送数据(需要发起新的 HTTP 请求)。
380+
- **WebSocket:** **双向通信 (全双工)**。客户端和服务器可以随时互相发送消息,实现真正的实时交互。
381+
2. **底层协议:**
382+
- **SSE:** 基于**标准的 HTTP/HTTPS 协议**。它本质上是一个“长连接”的 HTTP 请求,服务器保持连接打开并持续发送事件流。不需要特殊的服务器或协议支持,现有的 HTTP 基础设施就能用。
383+
- **WebSocket:** 使用**独立的 ws:// 或 wss:// 协议**。它需要通过一个特定的 HTTP "Upgrade" 请求来建立连接,并且服务器需要明确支持 WebSocket 协议来处理连接和消息帧。
384+
3. **实现复杂度和成本:**
385+
- **SSE:** **实现相对简单**,主要在服务器端处理。浏览器端有标准的 EventSource API,使用方便。开发和维护成本较低。
386+
- **WebSocket:** **稍微复杂一些**。需要服务器端专门处理 WebSocket 连接和协议,客户端也需要使用 WebSocket API。如果需要考虑兼容性、心跳、重连等,开发成本会更高。
387+
4. **断线重连:**
388+
- **SSE:** **浏览器原生支持**。EventSource API 提供了自动断线重连的机制。
389+
- **WebSocket:** **需要手动实现**。开发者需要自己编写逻辑来检测断线并进行重连尝试。
390+
5. **数据类型:**
391+
- **SSE:** **主要设计用来传输文本** (UTF-8 编码)。如果需要传输二进制数据,需要先进行 Base64 等编码转换成文本。
392+
- **WebSocket:** **原生支持传输文本和二进制数据**,无需额外编码。
342393

343-
SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:
394+
为了提供更好的用户体验和利用其简单、高效、基于标准 HTTP 的特性,**Server-Sent Events (SSE) 是目前大型语言模型 API(如 OpenAI、DeepSeek 等)实现流式响应的常用甚至可以说是标准的技木选择**
344395

345-
- SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。
346-
- SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
347-
- SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。
348-
- SSE 默认支持断线重连;WebSocket 则需要自己实现。
349-
- SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。
396+
这里以 DeepSeek 为例,我们发送一个请求并打开浏览器控制台验证一下:
350397

351-
**SSE 与 WebSocket 该如何选择?**
398+
![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse.png)
352399

353-
SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。
400+
![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse-eventstream.png)
354401

355-
但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SSE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力
402+
可以看到,响应头应里包含了 `text/event-stream`,说明使用的确实是SSE。并且,响应数据也确实是持续分块传输
356403

357404
## PING
358405

docs/database/redis/redis-questions-02.md

+25-11
Original file line numberDiff line numberDiff line change
@@ -525,11 +525,13 @@ Redis 中的大部分命令都是 O(1) 时间复杂度,但也有少部分 O(n)
525525
526526
#### 如何找到慢查询命令?
527527
528+
Redis 提供了一个内置的**慢查询日志 (Slow Log)** 功能,专门用来记录执行时间超过指定阈值的命令。这对于排查性能瓶颈、找出导致 Redis 阻塞的“慢”操作非常有帮助,原理和 MySQL 的慢查询日志类似。
529+
528530
`redis.conf` 文件中,我们可以使用 `slowlog-log-slower-than` 参数设置耗时命令的阈值,并使用 `slowlog-max-len` 参数设置耗时命令的最大记录条数。
529531
530532
当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than` 阈值的命令时,就会将该命令记录在慢查询日志(slow log)中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。
531533
532-
️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。
534+
️ 注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。
533535
534536
`slowlog-log-slower-than``slowlog-max-len` 的默认配置如下(可以自行修改):
535537
@@ -569,12 +571,12 @@ CONFIG SET slowlog-max-len 128
569571

570572
慢查询日志中的每个条目都由以下六个值组成:
571573

572-
1. 唯一渐进的日志标识符
573-
2. 处理记录命令的 Unix 时间戳。
574-
3. 执行所需的时间量,以微秒为单位
575-
4. 组成命令参数的数组
576-
5. 客户端 IP 地址和端口
577-
6. 客户端名称。
574+
1. **唯一 ID**: 日志条目的唯一标识符
575+
2. **时间戳 (Timestamp)**: 命令执行完成时的 Unix 时间戳。
576+
3. **耗时 (Duration)**: 命令执行所花费的时间,单位是**微秒**
577+
4. **命令及参数 (Command)**: 执行的具体命令及其参数数组
578+
5. **客户端信息 (Client IP:Port)**: 执行命令的客户端地址和端口
579+
6. **客户端名称 (Client Name)**: 如果客户端设置了名称 (CLIENT SETNAME)
578580

579581
`SLOWLOG GET` 命令默认返回最近 10 条的的慢查询命令,你也自己可以指定返回的慢查询命令的数量 `SLOWLOG GET N`
580582

@@ -731,15 +733,27 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
731733

732734
### 如何保证缓存和数据库数据的一致性?
733735

734-
细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。
736+
缓存和数据库一致性是个挺常见的技术挑战。引入缓存主要是为了提升性能、减轻数据库压力,但确实会带来数据不一致的风险。绝对的一致性往往意味着更高的系统复杂度和性能开销,所以实践中我们通常会根据业务场景选择合适的策略,在性能和一致性之间找到一个平衡点。
737+
738+
下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。这是非常常用的一种缓存读写策略,它的读写逻辑是这样的:
739+
740+
- **读操作**
741+
1. 先尝试从缓存读取数据。
742+
2. 如果缓存命中,直接返回数据。
743+
3. 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。
744+
- **写操作**
745+
1. 先更新数据库。
746+
2. 再直接删除缓存中对应的数据。
747+
748+
图解如下:
735749

736-
下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。
750+
![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-write.png)
737751

738-
Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存。
752+
![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-read.png)
739753

740754
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案:
741755

742-
1. **缓存失效时间变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
756+
1. **缓存失效时间(TTL - Time To Live)变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
743757
2. **增加缓存更新重试机制**(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。
744758

745759
相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd)

0 commit comments

Comments
 (0)