diff --git a/notes/Docker.md b/notes/Docker.md
index 45872cae5..cb08f9614 100644
--- a/notes/Docker.md
+++ b/notes/Docker.md
@@ -328,14 +328,14 @@ readinessProbe 是就绪探针:Pod 启动后要通过检查才会加入 Servic
### Pod 的创建过程
-K8s 的各组件不直接互相调用,而是都通过 API Server 协作:每个组件监听(watch)自己关心的对象,发现实际状态与期望不一致就采取行动,再把结果写回 API Server。以订单服务中被调度到节点 d1-agent 的一个 Pod 为例:
+K8s 的各组件不直接互相调用,而是都通过 API Server 协作:每个组件监听(watch)自己关心的对象,发现实际状态与期望不一致就采取行动,再把结果写回 API Server。以订单服务中被调度到节点 k3s-agent 的一个 Pod 为例:
1. kubectl 把 Deployment 发给 API Server,API Server 完成认证、鉴权和准入检查后写入 etcd;
2. 控制器管理器中的 Deployment 控制器发现新的 Deployment,创建 ReplicaSet;ReplicaSet 控制器发现副本数不足,创建 3 个 Pod 对象,这时的 Pod 还没有指定节点;
-3. 调度器发现未调度的 Pod,先过滤掉不满足条件的节点(资源不足、有不能容忍的污点等),再给剩下的节点打分,选出 d1-agent 写入 Pod 的 nodeName;
-4. d1-agent 上的 kubelet 发现分配给自己的 Pod,通过容器运行时接口(Container Runtime Interface, CRI)让 containerd 创建 Pod 沙箱:先启动一个 pause 容器持有网络命名空间,再调用 CNI 插件为它配置网卡、分配 IP 10.42.1.5;Pod 声明了存储卷时,还会通过 CSI 插件把卷挂载到节点上;
+3. 调度器发现未调度的 Pod,先过滤掉不满足条件的节点(资源不足、有不能容忍的污点等),再给剩下的节点打分,选出 k3s-agent 写入 Pod 的 nodeName;
+4. k3s-agent 上的 kubelet 发现分配给自己的 Pod,通过容器运行时接口(Container Runtime Interface, CRI)让 containerd 创建 Pod 沙箱:先启动一个 pause 容器持有网络命名空间,再调用 CNI 插件为它配置网卡、分配 IP 10.42.1.5;Pod 声明了存储卷时,还会通过 CSI 插件把卷挂载到节点上;
5. kubelet 拉取镜像(这里镜像已在节点上),创建并启动容器;
6. kubelet 执行探针,就绪探针通过后把 Pod 的状态更新为 Ready;
7. EndpointSlice 控制器把 10.42.1.5:8080 加入 order-service 的后端,各节点的 kube-proxy 随之更新转发规则。
@@ -389,13 +389,13 @@ Ingress 只是规则,真正处理请求的是 Ingress 控制器,例如 Traef
需要注意的是,Ingress 控制器通常不经过 Service 的虚拟 IP,而是从 EndpointSlice 取得 Pod 的地址后直接转发给 Pod(Traefik 的默认行为),Service 在这里只用来找到后端。Ingress API 已经冻结,不再增加新功能,新功能加在它的后继 Gateway API 中。
-Pod 之间的通信由 CNI 插件实现。K8s 的网络模型要求每个 Pod 有集群内唯一的 IP,任意两个 Pod 不经过 NAT 就能直接通信。k3s 默认的 Flannel 给每个节点分配一个网段(实验中 d1-server 上的 Pod 是 10.42.0.x,d1-agent 上的是 10.42.1.x),跨节点的包用 VXLAN 封装后经节点之间的网络发送。
+Pod 之间的通信由 CNI 插件实现。K8s 的网络模型要求每个 Pod 有集群内唯一的 IP,任意两个 Pod 不经过 NAT 就能直接通信。k3s 默认的 Flannel 给每个节点分配一个网段(实验中 k3s-server 上的 Pod 是 10.42.0.x,k3s-agent 上的是 10.42.1.x),跨节点的包用 VXLAN 封装后经节点之间的网络发送。
### Pod 异常状态的排查
Pod 的阶段(phase)只有 Pending、Running、Succeeded、Failed、Unknown 五种,kubectl get pods 的 STATUS 一栏显示得更细。常见的异常状态(k3s 实测,见[附录 A8](#a8-pod-的异常状态)):
-- Pending:Pod 还没有调度到节点上。describe 的事件中有 FailedScheduling 时是调度失败,例如申请 20 个 CPU 时为 0/2 nodes are available: 2 Insufficient cpu;两个节点都有污点 dedicated=db:NoSchedule、Pod 没有对应的容忍时为 2 node(s) had untolerated taint;Pod 使用的 PVC 没有绑定到存储卷时也会停在这里;
+- Pending:Pod 已被集群接受,但还有容器没有创建并运行起来,包括等待调度和拉取镜像的时间。describe 的事件中有 FailedScheduling 时是调度失败,例如申请 20 个 CPU 时为 0/2 nodes are available: 2 Insufficient cpu;两个节点都有污点 dedicated=db:NoSchedule、Pod 没有对应的容忍时为 2 node(s) had untolerated taint;Pod 使用的 PVC 没有绑定到存储卷时也会停在这里;
- ImagePullBackOff:拉取镜像失败,常见原因是镜像名或标签写错、私有仓库没有配置凭证(imagePullSecrets)、节点访问不了镜像仓库,kubelet 在 ErrImagePull 和 ImagePullBackOff 之间反复重试;
- CrashLoopBackOff:容器启动后反复退出,kubelet 每次重启前的等待时间按 10 秒、20 秒、40 秒翻倍,最长 5 分钟。kubectl logs --previous 查看上一次退出前的日志,例如把 server.port 写成 abc 时,日志中是 APPLICATION FAILED TO START 和具体的配置错误,退出码 1;
- OOMKilled:容器超过内存上限被内核杀掉,退出码 137,之后同样进入 CrashLoopBackOff。例如内存上限 256 MiB,JVM 堆却设为 300 MB 并在启动时全部占用,Java 应用的堆和堆外内存加起来要小于 limits;
@@ -403,7 +403,7 @@ Pod 的阶段(phase)只有 Pending、Running、Succeeded、Failed、Unknown
排查的顺序通常是:kubectl get pods -o wide 看状态、重启次数和所在节点;kubectl describe pod 看事件;kubectl logs 看应用日志,容器重启过的加 --previous;kubectl exec 进入容器检查配置和网络;kubectl top pod 看资源占用(需要 metrics-server)。
-节点 NotReady 指控制平面一段时间没有收到节点上 kubelet 的心跳,这个时间由 node-monitor-grace-period 决定(v1.31 默认 40 秒,v1.32 起 50 秒)。停掉工作节点 d1-agent 后 32 秒,它变为 NotReady(kubelet 约每 10 秒续约一次心跳,宽限期从最后一次心跳算起,所以比 40 秒短),Conditions 中是 Kubelet stopped posting node status,并被加上 node.kubernetes.io/unreachable 污点;上面的 Pod 同时被标记为未就绪,从 Service 的后端中摘除。Pod 默认容忍这个污点 300 秒,又过了约 5 分钟才被驱逐,Deployment 在 d1-server 上补足了副本(见[附录 A9](#a9-节点-notready))。
+节点 NotReady 指控制平面一段时间没有收到节点上 kubelet 的心跳,这个时间由 node-monitor-grace-period 决定(v1.31 默认 40 秒,v1.32 起 50 秒)。停掉工作节点 k3s-agent 后 32 秒,它变为 NotReady(kubelet 约每 10 秒续约一次心跳,宽限期从最后一次心跳算起,所以比 40 秒短),Conditions 中是 Kubelet stopped posting node status,并被加上 node.kubernetes.io/unreachable 污点;上面的 Pod 同时被标记为未就绪,从 Service 的后端中摘除。Pod 默认容忍这个污点 300 秒,又过了约 5 分钟才被驱逐,Deployment 在 k3s-server 上补足了副本(见[附录 A9](#a9-节点-notready))。
排查 NotReady 时先用 kubectl describe node 看 Conditions:
@@ -601,16 +601,16 @@ $ docker inspect -f '{{.Name}} started {{.State.StartedAt}}' shop-mysql-1 shop-o
### A5 部署、自愈与滚动升级
-两个节点的 k3s v1.31.4:server 节点 d1-server(控制平面兼工作节点)和 agent 节点 d1-agent 各运行在一个 Docker 特权容器中,容器内不能再用 OverlayFS,containerd 使用 native 快照(--snapshotter native)。订单服务的 1.0、1.1 两个镜像用 docker save 导出后导入两个节点的 containerd。
+两个节点的 k3s v1.31.4:server 节点 k3s-server(控制平面兼工作节点)和 agent 节点 k3s-agent 各运行在一个 Docker 特权容器中,容器内不能再用 OverlayFS,containerd 使用 native 快照(--snapshotter native)。订单服务的 1.0、1.1 两个镜像用 docker save 导出后导入两个节点的 containerd。
```text
$ kubectl apply -f order.yaml
all ready after 21.1 s
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
-order-service-5ccc589fd8-b76g2 1/1 Running 0 20s 10.42.1.5 d1-agent
-order-service-5ccc589fd8-vb2lx 1/1 Running 0 20s 10.42.1.4 d1-agent
-order-service-5ccc589fd8-vz9hj 1/1 Running 0 20s 10.42.0.8 d1-server
+order-service-5ccc589fd8-b76g2 1/1 Running 0 20s 10.42.1.5 k3s-agent
+order-service-5ccc589fd8-vb2lx 1/1 Running 0 20s 10.42.1.4 k3s-agent
+order-service-5ccc589fd8-vz9hj 1/1 Running 0 20s 10.42.0.8 k3s-server
$ kubectl get svc order-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
order-service ClusterIP 10.43.210.38 80/TCP 21s
@@ -628,7 +628,7 @@ $ kubectl delete pod/order-service-5ccc589fd8-b76g2
new Pod object created after 0.34 s
3/3 ready again after 11.2 s
NAME READY STATUS RESTARTS AGE IP NODE
-order-service-5ccc589fd8-494s9 1/1 Running 0 11s 10.42.0.9 d1-server
+order-service-5ccc589fd8-494s9 1/1 Running 0 11s 10.42.0.9 k3s-server
...
NAME ADDRESSTYPE PORTS ENDPOINTS
order-service-m2pjv IPv4 8080 10.42.1.4,10.42.0.8,10.42.0.9
@@ -651,7 +651,7 @@ $ kubectl get events(Deployment 与 ReplicaSet 的事件,节选)
2026-10-28T10:21:40Z ReplicaSet SuccessfulCreate replicaset-controller Created pod: order-service-5ccc589fd8-b76g2
$ kubectl get events --field-selector involvedObject.name=order-service-5ccc589fd8-b76g2
TIME REASON FROM MESSAGE
- Scheduled Successfully assigned default/order-service-5ccc589fd8-b76g2 to d1-agent
+ Scheduled Successfully assigned default/order-service-5ccc589fd8-b76g2 to k3s-agent
2026-10-28T10:21:40Z Pulled kubelet Container image "order-service:1.0" already present on machine
2026-10-28T10:21:51Z Created kubelet Created container app
2026-10-28T10:21:51Z Started kubelet Started container app
@@ -687,7 +687,7 @@ mnt mnt:[4026534203] mnt mnt:[4026534206]
### A7 Service 与 Ingress
-在 d1-agent 上查看 kube-proxy 写入的规则(节选):
+在 k3s-agent 上查看 kube-proxy 写入的规则(节选):
```text
$ iptables-save -t nat | grep default/order-service
@@ -698,7 +698,7 @@ $ iptables-save -t nat | grep default/order-service
-A KUBE-SEP-CRY6GKLRKVOMXZX4 -p tcp -m comment --comment "default/order-service" -m tcp -j DNAT --to-destination 10.42.0.8:8080
```
-正文中的 Ingress,k3s 自带的 Traefik 2.11 处理,在 d1-server 上访问节点 IP 的 80 端口:
+正文中的 Ingress,k3s 自带的 Traefik 2.11 处理,在 k3s-server 上访问节点 IP 的 80 端口:
```text
$ kubectl get ingress shop
@@ -718,7 +718,7 @@ $ wget --header "Host: other.example.com" http://172.19.0.2/orders/1024
$ kubectl patch svc order-service -p '{"spec":{"type":"NodePort"}}'
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
order-service NodePort 10.43.210.38 80:31887/TCP 103s
-$ wget -qO- http://172.19.0.3:31887/orders/1024 (在 d1-server 上访问 d1-agent 的 NodePort)
+$ wget -qO- http://172.19.0.3:31887/orders/1024 (在 k3s-server 上访问 k3s-agent 的 NodePort)
{"status":"SHIPPED","id":1024,"host":"order-service-5ccc589fd8-b76g2"}
```
@@ -770,39 +770,39 @@ Failed to convert to type java.lang.Integer
oom-demo 在第一次被杀之前 kubectl get pods 显示的状态是 OOMKilled。给两个节点都加上污点后创建 Pod:
```text
-$ kubectl taint nodes d1-server d1-agent dedicated=db:NoSchedule
+$ kubectl taint nodes k3s-server k3s-agent dedicated=db:NoSchedule
$ kubectl run -n lab taint-demo --image=order-service:1.0
Warning FailedScheduling 5s default-scheduler 0/2 nodes are available: 2 node(s) had untolerated taint {dedicated: db}. ...
-$ kubectl taint nodes d1-server d1-agent dedicated=db:NoSchedule-
-(8 秒后 taint-demo 在 d1-server 上 Running)
+$ kubectl taint nodes k3s-server k3s-agent dedicated=db:NoSchedule-
+(8 秒后 taint-demo 在 k3s-server 上 Running)
```
### A9 节点 NotReady
-用 docker stop 停掉 d1-agent 所在的容器,相当于节点宕机:
+用 docker stop 停掉 k3s-agent 所在的容器,相当于节点宕机:
```text
-10:28:55 $ docker stop d1-k3s-agent
-10:29:28 d1-agent NotReady after 32 s
+10:28:55 $ docker stop k3s-agent
+10:29:28 k3s-agent NotReady after 32 s
Type Status LastTransitionTime Reason Message
MemoryPressure Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
DiskPressure Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
PIDPressure Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
Ready Unknown Wed, 28 Oct 2026 18:29:27 +0800 NodeStatusUnknown Kubelet stopped posting node status.
Taints: node.kubernetes.io/unreachable:NoSchedule
-10:34:33 pods on d1-agent marked for deletion after 337 s
+10:34:33 pods on k3s-agent marked for deletion after 337 s
NAME READY STATUS IP NODE
-order-service-5ccc589fd8-24sqr 1/1 Terminating 10.42.1.8 d1-agent
-order-service-5ccc589fd8-2f56l 1/1 Terminating 10.42.1.9 d1-agent
-order-service-5ccc589fd8-2vrx4 1/1 Running 10.42.0.11 d1-server
-order-service-5ccc589fd8-4gzps 1/1 Running 10.42.0.17 d1-server
-order-service-5ccc589fd8-gzsbv 1/1 Running 10.42.0.16 d1-server
+order-service-5ccc589fd8-24sqr 1/1 Terminating 10.42.1.8 k3s-agent
+order-service-5ccc589fd8-2f56l 1/1 Terminating 10.42.1.9 k3s-agent
+order-service-5ccc589fd8-2vrx4 1/1 Running 10.42.0.11 k3s-server
+order-service-5ccc589fd8-4gzps 1/1 Running 10.42.0.17 k3s-server
+order-service-5ccc589fd8-gzsbv 1/1 Running 10.42.0.16 k3s-server
$ kubectl get pod -o jsonpath='{.items[0].spec.tolerations}'
[{"effect":"NoExecute","key":"node.kubernetes.io/not-ready","operator":"Exists","tolerationSeconds":300},
{"effect":"NoExecute","key":"node.kubernetes.io/unreachable","operator":"Exists","tolerationSeconds":300}]
```
-节点失联后,Pod 无法确认已经停止,一直显示 Terminating。恢复 d1-agent、重新部署后再停一次,每 4 秒查看 EndpointSlice 中各地址的 ready:
+节点失联后,Pod 无法确认已经停止,一直显示 Terminating。恢复 k3s-agent、重新部署后再停一次,每 4 秒查看 EndpointSlice 中各地址的 ready:
```text
+33s node=Ready endpoints: 10.42.0.19=true 10.42.1.14=true 10.42.1.13=true
diff --git a/notes/Git.md b/notes/Git.md
index ee772c7a6..5fb8b0764 100644
--- a/notes/Git.md
+++ b/notes/Git.md
@@ -152,7 +152,7 @@ target/
*.local.yml
```
-需要注意的是,.gitignore 只对未跟踪的文件生效。已经提交过的文件即使后来写进 .gitignore,修改仍会出现在 git status 中,要先用 git rm --cached 把它移出版本库(工作区的文件保留)。
+需要注意的是,.gitignore 只对未跟踪的文件生效。已经提交过的文件即使后来写进 .gitignore,修改仍会出现在 git status 中,要先用 git rm --cached \ 把它移出版本库(工作区的文件保留)。
不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
@@ -160,22 +160,22 @@ target/
| 用途 | 命令 |
| --- | --- |
-| 克隆仓库 | git clone |
+| 克隆仓库 | git clone \ |
| 查看状态 | git status |
-| 加入暂存区 | git add |
+| 加入暂存区 | git add \ |
| 提交 | git commit -m "说明" |
| 查看历史 | git log --oneline --graph |
| 查看改动 | git diff(工作区与暂存区),git diff --staged(暂存区与最新提交) |
-| 撤销工作区的修改 | git restore |
-| 撤销 git add | git restore --staged |
+| 撤销工作区的修改 | git restore \ |
+| 撤销 git add | git restore --staged \ |
| 新建并切换分支 | git switch -c dev |
| 切换分支 | git switch master |
| 合并分支 | git merge dev |
| 变基 | git rebase master |
| 临时保存修改 | git stash,之后用 git stash pop 恢复 |
| 撤销最近一次提交,保留修改 | git reset --soft HEAD~1 |
-| 回到某个提交,丢弃之后的修改 | git reset --hard |
-| 用一个新提交抵消某次提交 | git revert |
+| 回到某个提交,丢弃之后的修改 | git reset --hard \ |
+| 用一个新提交抵消某次提交 | git revert \ |
| 下载远程分支的新提交,不合并 | git fetch |
| 拉取并合并远程分支 | git pull |
| 推送 | git push origin dev |
diff --git a/notes/HTTP.md b/notes/HTTP.md
index 97ddc5cf0..b08c4feab 100644
--- a/notes/HTTP.md
+++ b/notes/HTTP.md
@@ -530,8 +530,8 @@ JWT 只是令牌的格式,而 OAuth 2.0 是授权框架,规定第三方应
多个系统在同一个父域名下时(a.example.com、b.example.com),把会话的 Cookie 设置为 Domain=example.com 就能共享。域名不同时(shop.com、pay.com),Cookie 无法跨域,需要一个统一的认证中心 sso.example.com:
-1. 用户访问 pay.com,pay.com 发现没有会话,重定向到 sso.example.com/login?redirect=https://pay.com/;
-2. 用户之前在 shop.com 登录时已经在认证中心登录过,认证中心的域名下有它自己的 Cookie,于是不需要输入密码,直接生成一个一次性的票据,重定向回 https://pay.com/?ticket=ST-1024;
+1. 用户访问 pay.com,pay.com 发现没有会话,重定向到 `sso.example.com/login?redirect=https://pay.com/`;
+2. 用户之前在 shop.com 登录时已经在认证中心登录过,认证中心的域名下有它自己的 Cookie,于是不需要输入密码,直接生成一个一次性的票据,重定向回 `https://pay.com/?ticket=ST-1024`;
3. pay.com 的后端拿票据向认证中心验证,得到用户信息后建立 pay.com 自己的会话。
票据经过浏览器的地址栏,可能被记录或截获,所以只能使用一次、有效期很短,并且必须由后端去认证中心验证,而不是直接信任。
@@ -659,7 +659,9 @@ ETag: "82e22293907ce725faf67773957acd12"
If-None-Match: "82e22293907ce725faf67773957acd12"
```
-Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器:时间只精确到秒,同一秒内的多次修改区分不出来;文件被重新生成而内容没变时,修改时间也会变,导致不必要的重新下载。所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有实体主体的 304 Not Modified 响应报文。
+Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器:时间只精确到秒,同一秒内的多次修改区分不出来;文件被重新生成而内容没变时,修改时间也会变,导致不必要的重新下载。所以它通常作为 ETag 的备用方案。
+
+如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有实体主体的 304 Not Modified 响应报文。
```http
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
@@ -886,7 +888,7 @@ SSE 的局限:
### 跨域 🔥
-同源指的是协议、域名和端口都相同。浏览器的同源策略限制网页中的脚本读取其它源的响应,例如 https://shop.example.com 页面中的脚本请求 https://api.example.com/orders,请求能够发出去,但浏览器默认不把响应交给脚本。
+同源指的是协议、域名和端口都相同。浏览器的同源策略限制网页中的脚本读取其它源的响应,例如 `https://shop.example.com` 页面中的脚本请求 `https://api.example.com/orders`,请求能够发出去,但浏览器默认不把响应交给脚本。
同源策略保护的是用户浏览器中的身份和网络位置:没有它,用户打开一个恶意网页,网页中的脚本就能以用户的身份(带着 Cookie)请求其它网站,或者访问用户所在内网中的服务,并读取响应的内容。
@@ -976,7 +978,7 @@ HTTPS 只防网络中的窃听、冒充服务器和篡改。SQL 注入、XSS、C
- 优点:公开密钥可以公开传输,被截获也无法用来解密;
- 缺点:运算速度慢。
-例如本机 RSA-2048 用私钥解密每秒约 1600 次,每次只能处理两百多字节,合计不到 0.4 MB/s;对称加密 AES-128-GCM 每秒能加密约 6 GB(OpenSSL 3.6 实测,见[附录 A14](#a14-对称加密与非对称加密的速度))。所以非对称加密只适合处理密钥、签名这类很短的数据。
+例如在 Apple M1 Pro 上,RSA-2048 用私钥解密每秒约 1600 次,每次只能处理两百多字节,合计不到 0.4 MB/s;对称加密 AES-128-GCM 每秒能加密约 6 GB(OpenSSL 3.6 实测,见[附录 A14](#a14-对称加密与非对称加密的速度))。所以非对称加密只适合处理密钥、签名这类很短的数据。
@@ -1014,7 +1016,7 @@ TLS 1.3 的套件名称只包含后两项,例如访问 www.jd.com 时为 TLS_A
证书中的签名是 CA 用自己的私钥对证书内容(域名、公钥、有效期、签发者等)的哈希值计算出来的。客户端用上一级证书中的公钥验证签名:证书内容只要被改动一个字节,哈希值就会变化,签名验证就会失败。例如把 baidu.com 证书中的 CN 改成 baidu.con,再用中间证书验证,结果为 certificate signature failure(OpenSSL 3.0 实测,见[附录 A9](#a9-证书验证))。
-根证书是信任的起点,它是自签名的,无法再向上验证,因此通常不在握手中发送,而是预装在操作系统或浏览器的信任列表中。客户端沿证书链逐级验证,链的最上端必须是信任列表中的根证书。除了签名,客户端还要检查证书是否在有效期内、是否已被吊销,以及访问的域名是否在证书的 Subject Alternative Name 中,例如 baidu.com 的证书列出了 53 个域名,其中的 *.baidu.com 匹配 www.baidu.com。
+根证书是信任的起点,它是自签名的,无法再向上验证,因此通常不在握手中发送,而是预装在操作系统或浏览器的信任列表中。客户端沿证书链逐级验证,链的最上端必须是信任列表中的根证书。除了签名,客户端还要检查证书是否在有效期内、是否已被吊销,以及访问的域名是否在证书的 Subject Alternative Name 中,例如 baidu.com 的证书列出了 53 个域名,其中的 *.baidu.com 匹配 `www.baidu.com`。
证书在有效期内也可能被提前吊销,例如私钥泄露。查询方式有两种,证书中可以写明相应的地址(baidu.com 的证书两者都有,见[附录 A9](#a9-证书验证)):证书吊销列表(Certificate Revocation List, CRL)是 CA 定期发布的全部被吊销证书的序列号;在线证书状态协议(Online Certificate Status Protocol, OCSP)只查询这一张证书。每次连接都去问 CA 既慢又暴露用户的访问记录,所以服务器可以在握手时附带 CA 签过名的 OCSP 响应,称为 OCSP Stapling;浏览器也大多改为定期下载厂商汇总的吊销列表。
@@ -1030,7 +1032,7 @@ TLS 握手完成三件事:协商加密算法、验证服务器的证书、协
-TLS 1.2 要 2 个 RTT 之后才能发送 HTTP 请求。TLS 1.3 把 ECDHE 公钥直接放在 ClientHello 中,服务器回复 ServerHello 时双方就能算出密钥,之后的握手报文(图中花括号内)都已加密,1 个 RTT 后就能发送请求;TLS 1.3 还去掉了 RSA 密钥交换等不安全的算法。
+TLS 1.2 要 2 个 RTT 之后才能发送 HTTP 请求。TLS 1.3 把 ECDHE 公钥直接放在 ClientHello 中,服务器回复 ServerHello 时双方就能算出密钥,之后的握手报文(图中花括号内)都已加密,1 个 RTT 后就能发送请求。如果客户端猜错了服务器支持的曲线,服务器会回复 HelloRetryRequest 要求重发 ClientHello,多花 1 个 RTT。TLS 1.3 还去掉了 RSA 密钥交换等不安全的算法。
#### 1. 握手报文的内容
@@ -1069,7 +1071,11 @@ RSA 密钥交换没有前向安全:攻击者录下通信,日后拿到服务
Finished 报文的内容是用主密钥对此前全部握手报文的哈希值计算出的校验值(TLS 1.2 中为 12 字节)。握手的前几个报文是明文,攻击者可以篡改,例如删掉 ClientHello 中安全的密码套件,迫使双方使用弱算法,称为降级攻击。双方各自根据自己收发的报文计算校验值,报文被改过,两边的结果就对不上,握手失败。
-完整的握手要验证证书和签名,开销较大。客户端再次连接同一个服务器时可以恢复会话:服务器在握手后发给客户端一个会话票据(即上面 TLS 1.2 握手中的 NewSessionTicket),客户端下次连接时带上它,双方直接由之前协商的密钥派生出新的会话密钥,不再验证证书。TLS 1.3 恢复会话时,客户端还可以在第一个报文中就带上请求,称为 0-RTT。需要注意的是,0-RTT 的数据可以被攻击者录下后重放,通常只有 GET 这类安全的请求才放在 0-RTT 中发送。
+完整的握手要验证证书和签名,开销较大。客户端再次连接同一个服务器时可以恢复会话:服务器在握手后发给客户端一个会话票据(即上面 TLS 1.2 握手中的 NewSessionTicket),客户端下次连接时带上它,双方直接由之前协商的密钥派生出新的会话密钥,不再验证证书。
+
+TLS 1.3 恢复会话时通常还会再做一次 ECDHE,即使票据对应的密钥泄露,握手之后的应用数据密钥也算不出来;只用票据派生密钥则没有前向安全。
+
+TLS 1.3 恢复会话时,客户端还可以在第一个报文中就带上请求,称为 0-RTT。0-RTT 的数据只用票据派生的密钥加密,不享有上面的前向安全保证。需要注意的是,0-RTT 的数据可以被攻击者录下后重放,通常只有 GET 这类安全的请求才放在 0-RTT 中发送。TLS 只要求每个服务器实例对同一次握手的 0-RTT 数据最多接受一次(例如每张票据只能用一次),客户端重试造成的重复请求仍要由应用处理;服务器不想冒险时可以返回 425 Too Early,让客户端在握手完成后重发。
#### 4. 一次 HTTPS 请求的过程
@@ -1095,7 +1101,7 @@ HTTPS 的报文摘要功能之所以安全,是因为计算摘要时用到了
- 因为需要进行加密解密等过程,因此速度会更慢;
- 需要申请和维护证书,不过 Let's Encrypt 等机构可以免费签发证书。
-速度上的主要开销是建立连接时的 TLS 握手。例如直连 www.baidu.com(TLS 1.2)时,TCP 建连约 12 ms,TLS 握手约 34 ms;www.jd.com(TLS 1.3)的 TLS 握手约 15 ms,与 TCP 建连的 13 ms 相当(各 5 次的中位数,macOS 实测,见[附录 A11](#a11-https-各阶段的耗时))。对称加密本身很快,现代 CPU 有 AES 指令加速;长连接和会话恢复可以避免每次请求都完整握手。
+速度上的主要开销是建立连接时的 TLS 握手。例如直连 `www.baidu.com`(TLS 1.2)时,TCP 建连约 12 ms,TLS 握手约 34 ms;`www.jd.com`(TLS 1.3)的 TLS 握手约 15 ms,与 TCP 建连的 13 ms 相当(各 5 次的中位数,macOS 实测,见[附录 A11](#a11-https-各阶段的耗时))。对称加密本身很快,现代 CPU 有 AES 指令加速;长连接和会话恢复可以避免每次请求都完整握手。
## 七、HTTP/1.1 新特性
@@ -1491,7 +1497,7 @@ $ curl --resolve shop.example:9443:127.0.0.1 https://shop.example:9443/
curl: (60) SSL certificate problem: self-signed certificate
```
-证书中的吊销查询地址(2026-10-30,LibreSSL 3.3.6 的 openssl s_client -status 连接 www.baidu.com,再用 openssl x509 -text 查看服务器证书):
+证书中的吊销查询地址(2026-10-30,LibreSSL 3.3.6 的 openssl s_client -status 连接 `www.baidu.com`,再用 openssl x509 -text 查看服务器证书):
```text
OCSP response: no response sent
@@ -1716,7 +1722,8 @@ AES-128-GCM 6280411.97k
- [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/)
- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences)
- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2)
-- [RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3](https://www.rfc-editor.org/rfc/rfc8446)
+- [RFC 9846: The Transport Layer Security (TLS) Protocol Version 1.3](https://www.rfc-editor.org/rfc/rfc9846)
+- [RFC 8470: Using Early Data in HTTP](https://www.rfc-editor.org/rfc/rfc8470)
- [RFC 7541: HPACK: Header Compression for HTTP/2](https://www.rfc-editor.org/rfc/rfc7541)
- [RFC 9114: HTTP/3](https://www.rfc-editor.org/rfc/rfc9114)
- [RFC 9000: QUIC](https://www.rfc-editor.org/rfc/rfc9000)
diff --git a/notes/Java IO.md b/notes/Java IO.md
index 5f468e909..bf9c354f2 100644
--- a/notes/Java IO.md
+++ b/notes/Java IO.md
@@ -599,12 +599,12 @@ NIO 与普通 I/O 的区别主要有以下两点:
- [The Java Tutorials: Basic I/O](https://docs.oracle.com/javase/tutorial/essential/io/)
- [JEP 353: Reimplement the Legacy Socket API](https://openjdk.org/jeps/353)
- [Java SE 21 API: MappedByteBuffer](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/MappedByteBuffer.html)
-- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
+- NIO 入门(IBM developerWorks 中文站,已下线)
- [Java NIO Tutorial](https://jenkov.com/tutorials/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/2016/11/04/nio.html)
-- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
-- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
-- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
+- 深入分析 Java I/O 的工作机制(IBM developerWorks 中文站,已下线)
+- 深入分析 Java 中的中文编码问题(IBM developerWorks 中文站,已下线)
+- Java 序列化的高级认识(IBM developerWorks 中文站,已下线)
- [NIO 与传统 IO 的区别](https://web.archive.org/web/20250917122905/https://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
diff --git "a/notes/Java \345\237\272\347\241\200.md" "b/notes/Java \345\237\272\347\241\200.md"
index 0ce7cb0cd..417b954bd 100644
--- "a/notes/Java \345\237\272\347\241\200.md"
+++ "b/notes/Java \345\237\272\347\241\200.md"
@@ -1993,13 +1993,13 @@ Blank.java:1: 错误: 可能尚未初始化变量x
- [StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?](https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting)
- [StackOverflow : Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
- [Abstract Methods and Classes](https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html)
-- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
+- 深入理解 abstract class 和 interface(IBM developerWorks 中文站,已下线)
- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
-- [Java 9 Private Methods in Interfaces](https://www.journaldev.com/12850/java-9-private-methods-interfaces)
+- [Java 9 Features with Examples](https://www.digitalocean.com/community/tutorials/java-9-features-with-examples)
- [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
- [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
- [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/)
-- [Java Exception Interview Questions and Answers](https://www.journaldev.com/2167/java-exception-interview-questions-and-answers)
+- [Java Exception Interview Questions and Answers](https://www.digitalocean.com/community/tutorials/java-exception-interview-questions-and-answers)
- [Java提高篇——Java 异常处理](https://www.cnblogs.com/Qian123/p/5715402.html)
- [Java 泛型详解](https://www.cnblogs.com/Blue-Keroro/p/8875898.html)
- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
diff --git "a/notes/Java \345\256\271\345\231\250.md" "b/notes/Java \345\256\271\345\231\250.md"
index 20c40b01b..c3b40664b 100644
--- "a/notes/Java \345\256\271\345\231\250.md"
+++ "b/notes/Java \345\256\271\345\231\250.md"
@@ -1032,7 +1032,7 @@ capacity 32: [5] 5 37 [21] 21 53
### A6 JDK 1.7 HashMap 扩容成环的模拟
-本机没有 JDK 7,这里在 JDK 21 上按 JDK 7 HashMap.transfer() 的逻辑模拟两个线程的执行顺序:A、B 的哈希值为 3 和 11,容量从 4 扩到 8 时都在 3 号桶。线程 1 执行完 next = e.next 后暂停,线程 2 完成整个扩容,线程 1 再继续:
+下面在 JDK 21 上按 JDK 7 HashMap.transfer() 的逻辑模拟两个线程的执行顺序:A、B 的哈希值为 3 和 11,容量从 4 扩到 8 时都在 3 号桶。线程 1 执行完 next = e.next 后暂停,线程 2 完成整个扩容,线程 1 再继续:
```java
Entry e = old[3], next = e.next; // 线程 1 读到 e、next 后暂停
diff --git "a/notes/Java \345\271\266\345\217\221.md" "b/notes/Java \345\271\266\345\217\221.md"
index 065ed3d1f..a51d91a95 100644
--- "a/notes/Java \345\271\266\345\217\221.md"
+++ "b/notes/Java \345\271\266\345\217\221.md"
@@ -383,7 +383,9 @@ public class SimplePool {
try {
task = queue.poll(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
- return;
+ // 上一个任务可能留下了中断标记,抛出异常时标记已被清除;
+ // 直接退出会少一个工作线程,是否退出只由 shutdown 决定
+ continue;
}
if (task == null) {
continue;
@@ -417,6 +419,8 @@ public class SimplePool {
例如 2 个线程执行 4 个任务,其中任务 2 抛出异常,其余 3 个任务仍由这两个线程执行完;关闭之后再提交会被拒绝(JDK 21 实测,见[附录 A6](#a6-简化版线程池))。
+工作线程在 poll() 处被中断时不能直接退出:某个任务执行完后线程上留着中断标记,下一次 poll() 就会抛出 InterruptedException,直接退出的话,只有 1 个线程时队列中剩下的任务就再也没人执行(见附录 A6 的第二个例子)。
+
ThreadPoolExecutor 的结构与它相同,多做了以下几件事:
- 线程不是一开始就全部创建,而是按前面的提交顺序按需创建(prestartAllCoreThreads() 可以提前创建核心线程);
@@ -1146,7 +1150,7 @@ t1.join(); t2.join();
-用双向链表是因为 next 用于释放时找到要唤醒的后继,prev 用于判断前驱是不是头节点、取消时跳过已取消的前驱。
+用双向链表是因为 next 用于释放时找到要唤醒的后继,prev 用于判断前驱是不是头节点、取消时跳过已取消的前驱。这是 JDK 14 改写 AQS 之后的实现。JDK 8 的资料中常见的 waitStatus(在前驱节点上记 SIGNAL,表示后继需要唤醒)和释放时从 tail 向前查找后继,在 JDK 21 中改为节点自己的 WAITING 状态位和 signalNext(head):释放时直接唤醒 head 的后继,已取消的节点由 cleanQueue() 摘除。
ReentrantLock 的每个 Condition 还有自己的条件队列:await() 释放锁并进入条件队列,signal() 把节点移到等待队列,被移过去的线程要重新获得锁才能从 await() 返回(JDK 21 实测,见[附录 A15](#a15-aqs-等待队列的获取顺序)):
@@ -1457,6 +1461,8 @@ public static void main(String[] args) {
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
```
+需要注意的是,打印语句在 put()、take() 返回之后才执行,两者之间线程可能被切换,所以输出中 consume.. 可能出现在对应的 produce.. 之前,看起来像先消费后生产。这只是打印的顺序:take() 只有在队列中有元素时才会返回,5 个生产者和 5 个消费者最终各打印 5 次。
+
### ForkJoin
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
@@ -1748,6 +1754,8 @@ volatile 变量单次的读、写是原子的(包括 long、double),但它
+表中 ARM 的指令是 C2 编译后的结果,C1 编译的代码在 ARM 上仍用普通的读写加 dmb 屏障指令。
+
x86 上 volatile 写之后的 lock addl 要等写缓冲中的写入进入缓存,后面的读才能执行;其它核心中这一行的旧副本由 MESI 等协议置为失效,随后读到的就是新值。
volatile 与 synchronized 的区别:volatile 只修饰变量,保证可见性和有序性,不保证复合操作的原子性,线程不会阻塞;synchronized 修饰方法和代码块,同一时刻只有一个线程执行,同时保证原子性、可见性和有序性,代价是竞争时线程会阻塞。所以状态标志、发布数据用 volatile,读-改-写操作和多个变量一起修改用锁或原子类。
@@ -1820,7 +1828,7 @@ JDK 中线程安全的类有不可变类、StringBuffer 和 Vector 这类每个
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。CAS 失败时不阻塞线程,只返回 false,由调用方决定重试还是放弃。
-CAS 由处理器指令完成,比较和写入之间不会被其它核心插入:x86 上 HotSpot 生成带 lock 前缀的 cmpxchg 指令,lock 前缀保证执行期间其它核心不能访问这个内存位置;ARM 上使用 ARMv8.1 的 CAS 指令,不支持时用独占加载、存储指令 LDXR、STXR 循环实现。Java 代码通过 Unsafe 的 compareAndSetInt() 等方法使用 CAS,JIT 把它们直接编译成这些指令。
+CAS 由处理器指令完成,比较和写入之间不会被其它核心插入:x86 上 HotSpot 生成带 lock 前缀的 cmpxchg 指令,lock 前缀保证执行期间其它核心不能访问这个内存位置;ARM 上使用 ARMv8.1 的 CASAL 指令(带获取和释放语义的 CAS),不支持时用带同样语义的独占加载、存储指令 LDAXR、STLXR 循环实现。Java 代码通过 Unsafe 的 compareAndSetInt() 等方法使用 CAS,JIT 把它们直接编译成这些指令。
#### 2. AtomicInteger
@@ -2142,7 +2150,7 @@ public static String concatString(String s1, String s2, String s3) {
JDK 1.6 引入了偏向锁,加上此前已有的轻量级锁,锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasable)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
-以下是 64 位 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。末尾两位的锁标志和倒数第 3 位的偏向标志区分了五种状态:上面的四种,以及 GC 时使用的标记状态。
+以下是 JDK 8 中 64 位 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。末尾两位的锁标志和倒数第 3 位的偏向标志区分了五种状态:上面的四种,以及 GC 时使用的标记状态。偏向锁的实现在 JDK 18 中删除,JDK 21 的 Mark Word 中倒数第 3 位不再使用,只靠末尾两位区分无锁、轻量级锁、重量级锁和 GC 标记。
@@ -2158,7 +2166,7 @@ JDK 1.6 引入了偏向锁,加上此前已有的轻量级锁,锁拥有了四
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
-以上是 JDK 21 默认的实现(LockingMode 为 1)。JDK 23 起默认改为新的轻量级锁实现(LM_LIGHTWEIGHT),不再把 Mark Word 复制到栈上的 Lock Record 中,而是在线程中维护一个持有的锁对象列表。
+以上是 JDK 21 默认的实现(LockingMode 为 1)。JDK 23 起默认改为新的轻量级锁实现(LM_LIGHTWEIGHT),不再把 Mark Word 复制到栈上的 Lock Record 中,而是在线程中维护一个持有的锁对象列表。JDK 26 删除了旧的实现,-XX:LockingMode 不再生效,JDK 27 中使用这个参数会因无法识别而启动失败。
### 偏向锁
@@ -2176,7 +2184,7 @@ synchronized 设计锁升级,是因为重量级锁的代价在阻塞和唤醒
1. 无锁:新创建的对象,Mark Word 末尾为 01;
2. 轻量级锁:一个线程进入 synchronized 时,用 CAS 把 Mark Word 换成指向自己栈中 Lock Record 的指针,末尾变为 00;同一个线程重入时只在栈中多放一个 Lock Record,退出时再用 CAS 把 Mark Word 换回来;
-3. 重量级锁:其它线程持有锁时 CAS 失败,锁膨胀为 ObjectMonitor(结构见[锁的实现](#1-锁的实现)),原持有者成为它的 _owner,Mark Word 改为指向它,末尾变为 10。抢锁的线程先自适应自旋若干次,仍然抢不到再进入队列阻塞,原持有者退出时唤醒它们。
+3. 重量级锁:其它线程持有锁时 CAS 失败,锁膨胀为 ObjectMonitor(结构见[锁的实现](#1-锁的实现)),原持有者成为它的 _owner,Mark Word 改为指向它,末尾变为 10。JDK 27 起默认不再改写 Mark Word 中的地址,而是把对象和 ObjectMonitor 的对应关系记在一张全局表中,按对象的哈希码查找,Mark Word 保留哈希码,只把末尾改为 10。抢锁的线程先自适应自旋若干次,仍然抢不到再进入队列阻塞,原持有者退出时唤醒它们。
轻量级锁一节中锁状态变化的图就是这个过程(JDK 21 实测,见[附录 A10](#a10-锁状态与监视器的回收))。JDK 15 之前,无锁和轻量级锁之间还有偏向锁,其它线程来加锁时要先在安全点[撤销偏向](#偏向锁),代价很高。偏向锁的收益主要来自 Vector、Hashtable 这类每个方法都加锁、实际只被一个线程使用的旧代码,又让 JVM 的同步代码很复杂,所以 JDK 15 起被默认禁用(JEP 374),之后被移除。
@@ -2257,7 +2265,7 @@ fair=false count=1600000 55 ms
fair=true count=1600000 3019 ms
```
-锁内再记下上一次的持有者,统计 160 万次加锁中换了多少次线程(JDK 21.0.13,Apple M1 Pro,2026-10-30 重跑;这次机器负载较高,耗时比上面长,只列切换次数):
+锁内再记下上一次的持有者,统计 160 万次加锁中换了多少次线程(JDK 21.0.13,Apple M1 Pro;运行时机器负载较高,耗时不与上面比较,只列切换次数):
```java
l.lock();
@@ -2397,6 +2405,19 @@ java.lang.IllegalStateException: 任务 2 出错
异常的堆栈只保留了第一行。任务执行的先后和线程的分配每次不同,但每次都是 3 个任务完成、关闭后的提交被拒绝。ThreadPoolExecutor 的 ctl、getTask() 中的 poll/take 见 JDK 21 源码 java/util/concurrent/ThreadPoolExecutor.java。
+任务留下中断标记的情况:只有 1 个工作线程,任务 1 执行完后调用 Thread.currentThread().interrupt() 留下中断标记,任务 2、3 已在队列中,500 毫秒后打印存活的工作线程数:
+
+```text
+== 工作线程被中断时直接 return
+任务 1 执行完,留下了中断标记
+存活的工作线程:0
+== 改为 continue(正文中的写法)
+任务 1 执行完,留下了中断标记
+任务 2 执行
+任务 3 执行
+存活的工作线程:1
+```
+
### A7 线程池的异常与参数调整
线程工厂把线程依次命名为 worker-1、worker-2……,每次提交后等待 100 ms:
@@ -2509,7 +2530,7 @@ AtomicStampedReference CAS(A -> C, 版本 0):false,当前值 A,版本 2
值为 1000 时 CAS(1000 -> 500):false
```
-正文中无锁栈的例子用 AtomicReference 实现的栈,按正文的步骤手动交错执行两个线程的操作:
+正文中无锁栈的例子用 AtomicReference\ 实现的栈,按正文的步骤手动交错执行两个线程的操作:
```text
初始:A B C
@@ -2518,7 +2539,7 @@ AtomicStampedReference CAS(A -> C, 版本 0):false,当前值 A,版本 2
结果:B C
```
-把栈顶换成 AtomicStampedReference,同样的步骤:
+把栈顶换成 AtomicStampedReference\,同样的步骤:
```text
线程 2 操作后:A D C,版本 4
@@ -2932,7 +2953,7 @@ submit: ClassCastException
### A22 start() 与操作系统线程
-JDK 21.0.13,macOS 15.8(Apple M1 Pro)。osThreads() 用 ps -M -p 统计本进程的操作系统线程数(第一次启动子进程时 JDK 会多创建一个 process reaper 线程,所以先调用一次预热),线程在 run() 中等待 done 以便统计:
+JDK 21.0.13,macOS 15.8(Apple M1 Pro)。osThreads() 用 ps -M -p \ 统计本进程的操作系统线程数(第一次启动子进程时 JDK 会多创建一个 process reaper 线程,所以先调用一次预热),线程在 run() 中等待 done 以便统计:
```java
CountDownLatch done = new CountDownLatch(1);
@@ -3105,7 +3126,7 @@ void writer() { data = 1; ready = 1; }
int reader() { int r = ready; return r + data; }
```
-用 java -XX:+UnlockDiagnosticVMOptions -XX:-TieredCompilation -XX:CompileCommand=print,VolAsm::writer -XX:CompileCommand=print,VolAsm::reader VolAsm 输出机器码。本机没有反汇编插件,输出是十六进制,按 AArch64 的指令编码解出其中的读写指令:
+用 java -XX:+UnlockDiagnosticVMOptions -XX:-TieredCompilation -XX:CompileCommand=print,VolAsm::writer -XX:CompileCommand=print,VolAsm::reader VolAsm 输出机器码。未加载反汇编插件时,机器码以十六进制输出,下面按 AArch64 的指令编码解出其中的读写指令:
```text
writer:str w11, [x1, #12] // data = 1
diff --git "a/notes/Java \350\231\232\346\213\237\346\234\272.md" "b/notes/Java \350\231\232\346\213\237\346\234\272.md"
index 37250a2b0..ccfba06fb 100644
--- "a/notes/Java \350\231\232\346\213\237\346\234\272.md"
+++ "b/notes/Java \350\231\232\346\213\237\346\234\272.md"
@@ -58,6 +58,7 @@
* [A20 线程栈中的 CPU 时间](#a20-线程栈中的-cpu-时间)
* [A21 堆以外的内存](#a21-堆以外的内存)
* [A22 容器中的默认堆](#a22-容器中的默认堆)
+ * [A23 等待连接池时的线程栈](#a23-等待连接池时的线程栈)
* [参考资料](#参考资料)
@@ -80,7 +81,7 @@
### Java 虚拟机栈
-每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
+每个 Java 方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法返回地址等信息。栈帧中保存着指向当前方法所属类的运行时常量池的引用,用于支持动态链接:在运行时把常量池中方法和字段的符号引用解析为实际的方法入口和字段位置。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
@@ -202,7 +203,9 @@ Instance size: 32 bytes
- 实例数据:字段不按声明的顺序排列,虚拟机会重排以减少空隙,例如 userId 被放进了对象头后面 4 字节的空位;
- 对齐填充:对象的大小必须是 8 字节的整数倍,new Object() 只有 12 字节的对象头,要补齐到 16 字节。
-关闭压缩指针(-XX:-UseCompressedOops -XX:-UseCompressedClassPointers)后,类型指针和 note 引用都变成 8 字节,Order 从 32 字节变成 40 字节。压缩指针用 32 位存放对象地址,因为对象按 8 字节对齐,地址的低 3 位总是 0,可以省略,所以最多能表示 232 × 8 = 32 GB 的堆。最大堆达到 32 GB 时,对象引用的压缩自动关闭,所有引用都变成 8 字节(JDK 21 中 -Xmx31g 时 UseCompressedOops 为 true,-Xmx32g 时为 false,类型指针的压缩不受影响),这是堆的大小通常小于 32 GB 的原因。
+以上是 JDK 21 的布局。JDK 27 起默认使用紧凑对象头(JEP 534):压缩后的类型指针放进 Mark Word 的高 22 位,对象头只有 8 字节,new Object() 也只占 8 字节,可以用 -XX:-UseCompactObjectHeaders 关闭。
+
+在 JDK 21 中关闭压缩指针(-XX:-UseCompressedOops -XX:-UseCompressedClassPointers)后,类型指针和 note 引用都变成 8 字节,Order 从 32 字节变成 40 字节。压缩指针用 32 位存放对象地址,因为对象按 8 字节对齐,地址的低 3 位总是 0,可以省略,所以最多能表示 232 × 8 = 32 GB 的堆。最大堆接近 32 GB 时,对象引用的压缩自动关闭,所有引用都变成 8 字节(JDK 21 中 -Xmx32736m 时 UseCompressedOops 为 true,-Xmx32737m 时为 false,类型指针的压缩不受影响),这是堆的大小通常小于 32 GB 的原因。JDK 27 起类型指针总是压缩,-XX:-UseCompressedClassPointers 会被忽略。
## 二、垃圾收集 🔥🔥
@@ -416,7 +419,7 @@ STW 无法完全消除,但可以缩短:CMS、G1 把耗时最长的标记与
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
-它是非服务器级机器(例如只有 1 个处理器)上的默认收集器,见 [G1 收集器](#7-g1-收集器) 中默认收集器的选择,这类机器的内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
+在 JDK 26 及以前,它是非服务器级机器(例如只有 1 个处理器)上的默认收集器,见 [G1 收集器](#7-g1-收集器) 中默认收集器的选择,这类机器的内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
#### 2. ParNew 收集器
@@ -923,7 +926,7 @@ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundE
需要打破双亲委派的常见场景:
- SPI:JDBC 的 DriverManager 由平台类加载器加载(JDK 8 中为启动类加载器),而 MySQL 驱动等实现类在 classpath 上,由应用类加载器加载,父加载器看不到子加载器中的类。DriverManager 通过 ServiceLoader 查找驱动,ServiceLoader 使用线程上下文类加载器(Thread.getContextClassLoader(),默认就是应用类加载器)加载实现类,相当于父加载器委托子加载器加载类;
-- Tomcat:每个 Web 应用有自己的 WebappClassLoader,先在本应用的 /WEB-INF/classes 和 /WEB-INF/lib 中查找,找不到再交给父加载器,这样同一个 Tomcat 中的两个应用可以使用同一个库的不同版本;JDK 的核心类和 Servlet 等规范接口仍然先交给父加载器;
+- Tomcat:每个 Web 应用有自己的 WebappClassLoader,先在本应用的 /WEB-INF/classes 和 /WEB-INF/lib 中查找,找不到再交给父加载器,这样同一个 Tomcat 中的两个应用可以使用同一个库的不同版本;JDK 自带的类先交给加载 JDK 类的加载器(JDK 9 起为平台类加载器,JDK 8 为扩展类加载器),Servlet 等规范接口和 Tomcat 自身的类先交给父加载器,Web 应用无法用自己的版本覆盖它们;
- 热部署:同一个类加载器不能重复定义同一个类,修改后的类要用新的类加载器加载,旧的加载器连同它加载的类一起丢弃,OSGi 等模块系统也为每个模块使用单独的加载器。
例如两个上面这样的类加载器分别从 v1、v2 目录加载同名的 Greeter 类,在同一个 JVM 中得到两个不同的 Class 对象,分别输出 Greeter v1 和 Greeter v2。打破双亲委派也不能替换 JDK 的核心类,defineClass() 拒绝定义 java. 开头的包中的类,抛出 SecurityException: Prohibited package name: java.lang(JDK 21 实测,见[附录 A7](#a7-打破双亲委派))。
@@ -1009,7 +1012,7 @@ Dumping heap to java_pid24607.hprof ...
Heap dump file created [19154891 bytes in 0.007 secs]
```
-线上的泄漏通常很慢,还没有发生内存溢出,只是每次 GC 后堆的占用越来越高。这时可以每隔一段时间用 jcmd GC.class_histogram 统计一次各个类的实例数和字节数,持续增长的类就是线索;它会先进行一次 Full GC,只统计存活的对象,对线上服务有影响。例如一个用 HashMap 做缓存、从不删除的程序,相隔 20 秒的两次统计中,Session 对象从约 8.2 万个增长到约 25.0 万个,HashMap$Node 同步增长,说明某个 HashMap 只增不减,再导出堆转储找到持有它的对象(JDK 21 实测,见[附录 A11](#a11-堆直方图))。
+线上的泄漏通常很慢,还没有发生内存溢出,只是每次 GC 后堆的占用越来越高。这时可以每隔一段时间用 jcmd \ GC.class_histogram 统计一次各个类的实例数和字节数,持续增长的类就是线索;它会先进行一次 Full GC,只统计存活的对象,对线上服务有影响。例如一个用 HashMap 做缓存、从不删除的程序,相隔 20 秒的两次统计中,Session 对象从约 8.2 万个增长到约 25.0 万个,HashMap$Node 同步增长,说明某个 HashMap 只增不减,再导出堆转储找到持有它的对象(JDK 21 实测,见[附录 A11](#a11-堆直方图))。
判断是不是泄漏,要看每次 GC 之后堆的占用。例如把附录 A11 的程序和一个只保留最近 1 万个 Session 的版本都用 128 MB 的堆运行:前者 GC 后的占用总体持续上升,最后几次 Full GC 后仍有 115~127 MB,28 秒时抛出 OutOfMemoryError;后者每次 GC 后都回到约 8 MB(JDK 21 实测,见[附录 A12](#a12-泄漏时-gc-之后的占用))。
@@ -1048,7 +1051,7 @@ Heap dump file created [19154891 bytes in 0.007 secs]
常见做法是不写 -Xmx,用 MaxRAMPercentage 按比例设堆,但比例不能太高。例如限制 1 GiB、比例 75% 时堆最大 768 MiB,堆外只剩 256 MiB,要放下元空间、线程栈、代码缓存、GC 数据结构和直接内存;直接内存没有设上限时可以和堆一样大。例如堆用到 700 MiB、ByteBuffer.allocateDirect 申请的直接内存 300 MiB,再加上约 100 MiB 的其它堆外内存,合计约 1.1 GiB,堆还没到上限,进程已经超过了限制。这时 JVM 不会抛出 OutOfMemoryError,而是被内核的 OOM Killer 杀掉(退出码 137,见 [内存溢出与内存泄漏](#内存溢出与内存泄漏))。所以要用 -XX:MaxDirectMemorySize 给 NIO 的直接缓冲区设上限,压测时加 -XX:NativeMemoryTracking=summary,用 `jcmd VM.native_memory summary` 看各部分的占用(输出见[附录 A21](#a21-堆以外的内存)),再定堆的比例。
-判断 GC 是不是瓶颈,看停顿时间占运行时间的比例和最长停顿。jstat -gcutil 5000 每 5 秒输出一次,GCT 列是累计的停顿秒数,两次之差除以间隔就是这段时间的占比。-Xlog:gc 日志中以 Pause 开头的行记录一次停顿,例如 Pause Young (Normal) (G1 Evacuation Pause) 24M->1M(64M) 0.542ms 依次是回收的类型、原因、回收前后堆的占用、堆的大小和停顿时长;Concurrent 开头的行是并发执行的耗时,不计入停顿。例如 64 MB 的堆中老年代已用 92%,5 秒内新生代回收了 1403 次,20% 的时间都在停顿(JDK 21 实测,见[附录 A19](#a19-gc-时间的占比))。
+判断 GC 是不是瓶颈,看停顿时间占运行时间的比例和最长停顿。jstat -gcutil \ 5000 每 5 秒输出一次,GCT 列是累计的停顿秒数,两次之差除以间隔就是这段时间的占比。-Xlog:gc 日志中以 Pause 开头的行记录一次停顿,例如 Pause Young (Normal) (G1 Evacuation Pause) 24M->1M(64M) 0.542ms 依次是回收的类型、原因、回收前后堆的占用、堆的大小和停顿时长;Concurrent 开头的行是并发执行的耗时,不计入停顿。例如 64 MB 的堆中老年代已用 92%,5 秒内新生代回收了 1403 次,20% 的时间都在停顿(JDK 21 实测,见[附录 A19](#a19-gc-时间的占比))。
线上频繁 Full GC 的排查思路:
@@ -1061,8 +1064,8 @@ Heap dump file created [19154891 bytes in 0.007 secs]
### CPU 占用过高 🔥
1. 用 top 找到 CPU 占用高的 Java 进程;
-2. 用 top -H -p 查看这个进程中每个线程的 CPU 占用,记下占用最高的线程 ID(输出示例见 [Linux - CPU 占用高](Linux.md#cpu-占用高));
-3. 用 jstack 或者 jcmd Thread.print 导出所有线程的调用栈,按线程 ID 找到对应的线程;
+2. 用 top -H -p \ 查看这个进程中每个线程的 CPU 占用,记下占用最高的线程 ID(输出示例见 [Linux - CPU 占用高](Linux.md#cpu-占用高));
+3. 用 jstack \ 或者 jcmd \ Thread.print 导出所有线程的调用栈,按线程 ID 找到对应的线程;
4. 查看这个线程正在执行的代码,隔几秒再导出两三次,确认它一直停在同一段代码中。
需要重启恢复服务时,先导出线程栈再重启,重启后现场就丢失了。
@@ -1077,16 +1080,22 @@ Heap dump file created [19154891 bytes in 0.007 secs]
要按线程 ID 查找,不能只看状态:阻塞在 socket 读取上等待数据的线程也是 RUNNABLE,但 cpu= 几乎不增长(JDK 21 实测,见[附录 A20](#a20-线程栈中的-cpu-时间))。
-jstack 和 Thread.print 不输出虚拟线程的栈,要用 jcmd Thread.dump_to_file <文件> 导出(JDK 21 实测)。
+jstack 和 Thread.print 不输出虚拟线程的栈,要用 jcmd \ Thread.dump_to_file <文件> 导出(JDK 21 实测)。
常见的原因有:
- 死循环,例如空转等待、出错后立即重试,或者多线程同时扩容 JDK 1.7 的 HashMap 使链表成环,之后的 get() 在环上不停遍历(见 [HashMap 的线程安全](Java%20容器.md#10-线程安全));
-- 频繁 GC,这时 CPU 占用高的线程中有多个 GC 线程(名为 GC Thread#0、G1 Conc#0 等)。用 jstat -gcutil 1000 看哪一列增长得快:FGC 增长时按 [JVM 调优](#jvm-调优) 中频繁 Full GC 的思路排查;只有 YGC 增长时,多半是对象分配太快或堆太小。
+- 频繁 GC,这时 CPU 占用高的线程中有多个 GC 线程(名为 GC Thread#0、G1 Conc#0 等)。用 jstat -gcutil \ 1000 看哪一列增长得快:FGC 增长时按 [JVM 调优](#jvm-调优) 中频繁 Full GC 的思路排查;只有 YGC 增长时,多半是对象分配太快或堆太小。
需要注意的是,Linux 的平均负载(load average,top 和 uptime 显示的三个数)统计的不只是正在运行和等待 CPU 的任务(R 状态),还包括不可中断睡眠的任务(D 状态,通常在等待磁盘 I/O)。所以 CPU 利用率低而负载很高时,通常是大量线程阻塞在 I/O 上,例如磁盘读写过慢、NFS 挂载无响应。这时 top 中的 wa(等待 I/O 的时间占比)和 vmstat 中的 b 列(阻塞在 I/O 上的进程数)偏高,可以用 ps -eLo stat,pid,tid,comm 找出状态为 D 的线程。
-CPU 和负载都不高、接口却变慢时,线程多半在等待,多导出几次线程栈,看大量线程是 BLOCKED 在同一把锁上,还是在 socket 读取上等待下游的响应。
+CPU 和负载都不高、接口却变慢时,线程多半在等待。多导出几次线程栈,看大量线程停在哪里:
+
+- BLOCKED,停在同一把 synchronized 锁上:找到持有这把锁的线程在做什么;
+- TIMED_WAITING (parking),停在 HikariPool.getConnection 这类获取连接的方法中:连接池已经耗尽,要找的是拿着连接不还或执行太慢的线程,原因在数据库一侧时扩大连接池没有用;
+- RUNNABLE,停在 socket 读取上(例如 MySQL 驱动的 ReadAheadInputStream.read):在等数据库或下游服务返回,状态是 RUNNABLE 却不占 CPU。
+
+连接池耗尽时常常是后两种同时出现:少数线程拿着连接、在数据库中等待行锁,其余线程在等连接(JDK 21、HikariCP 5.1 实测,见[附录 A23](#a23-等待连接池时的线程栈))。数据库一侧找到持有锁的源头事务的方法见[慢查询的定位与优化](MySQL.md#2-分析原因)。
### 死锁
@@ -1360,7 +1369,7 @@ $ docker run --pids-limit 500 ... java -Xmx256m Threads
### A11 堆直方图
-用 HashMap 做缓存,每次放入一个带 512 字节数组的 Session,从不删除,每放入 100 个休眠 10 ms;运行 10 秒和 30 秒时各执行一次 jcmd GC.class_histogram:
+用 HashMap 做缓存,每次放入一个带 512 字节数组的 Session,从不删除,每放入 100 个休眠 10 ms;运行 10 秒和 30 秒时各执行一次 jcmd \ GC.class_histogram:
```java
record Session(String id, byte[] payload) {}
@@ -1703,6 +1712,29 @@ $ java -XX:MaxRAM=1g -XX:+PrintFlagsFinal -version | grep ' MaxHeapSize '
256 MiB 和 512 MiB 都是 128 MiB:内存的一半不小于默认的最小值(约 125 MiB)时,最大堆取 25% 与这个最小值中较大的一个。
+### A23 等待连接池时的线程栈
+
+JDK 21.0.13、HikariCP 5.1.0、MySQL Connector/J 9.4.0、MySQL 8.0.44,Apple M1 Pro。先用一个单独的连接执行 UPDATE account SET balance = balance - 1 WHERE id = 1 且不提交;再建一个最大连接数为 2 的连接池,3 个线程各取一个连接执行 UPDATE account SET balance = balance + 1 WHERE id = 1。6 秒后用 jstack 导出线程栈(节选):
+
+```text
+"http-nio-8080-exec-1" ... runnable
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.Net.poll(java.base@21.0.13/Native Method)
+ ...
+ at com.mysql.cj.protocol.ReadAheadInputStream.fill(ReadAheadInputStream.java:91)
+ at com.mysql.cj.protocol.ReadAheadInputStream.read(ReadAheadInputStream.java:157)
+
+"http-nio-8080-exec-3" ... waiting on condition
+ java.lang.Thread.State: TIMED_WAITING (parking)
+ at jdk.internal.misc.Unsafe.park(java.base@21.0.13/Native Method)
+ ...
+ at java.util.concurrent.SynchronousQueue.poll(java.base@21.0.13/SynchronousQueue.java:338)
+ at com.zaxxer.hikari.util.ConcurrentBag.borrow(ConcurrentBag.java:151)
+ at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
+```
+
+exec-1、exec-2 拿到了连接,在 socket 上等数据库返回,实际是在数据库中等待行锁;exec-3 等连接,30 秒后抛出 SQLTransientConnectionException: Connection is not available, request timed out after 30012ms (total=2, active=2, idle=0, waiting=0)。同一时刻数据库一侧的查询结果见 [MySQL 附录 A14](MySQL.md#a14-找到持有锁的源头事务)。
+
## 参考资料
- [Troubleshooting Memory Leaks (Java SE 21)](https://docs.oracle.com/en/java/javase/21/troubleshoot/troubleshooting-memory-leaks.html)
@@ -1719,7 +1751,7 @@ $ java -XX:MaxRAM=1g -XX:+PrintFlagsFinal -version | grep ' MaxHeapSize '
- [深入理解 JVM(2)——GC 算法与内存分配策略](https://web.archive.org/web/20201120232823/https://crowhawk.github.io/2017/08/10/jvm_2/)
- [深入理解 JVM(3)——7 种垃圾收集器](https://web.archive.org/web/20201120232823/https://crowhawk.github.io/2017/08/15/jvm_3/)
- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
-- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
+- 深入探讨 Java 类加载器(IBM developerWorks 中文站,已下线)
- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
- [Garbage-First (G1) Garbage Collector | Java SE 17 GC Tuning Guide](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \344\272\214\345\210\206\346\237\245\346\211\276.md" "b/notes/Leetcode \351\242\230\350\247\243 - \344\272\214\345\210\206\346\237\245\346\211\276.md"
index bcf3e7709..4e5565d78 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \344\272\214\345\210\206\346\237\245\346\211\276.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \344\272\214\345\210\206\346\237\245\346\211\276.md"
@@ -102,7 +102,7 @@ l m h
69\. Sqrt(x) (Easy)
-[Leetcode](https://leetcode.com/problems/sqrtx/description/) / [力扣](https://leetcode-cn.com/problems/sqrtx/description/)
+[Leetcode](https://leetcode.com/problems/sqrtx/description/) / [力扣](https://leetcode.cn/problems/sqrtx/description/)
```text
Input: 4
@@ -142,7 +142,7 @@ public int mySqrt(int x) {
744\. Find Smallest Letter Greater Than Target (Easy)
-[Leetcode](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/) / [力扣](https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target/description/)
+[Leetcode](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/) / [力扣](https://leetcode.cn/problems/find-smallest-letter-greater-than-target/description/)
```text
Input:
@@ -180,7 +180,7 @@ public char nextGreatestLetter(char[] letters, char target) {
540\. Single Element in a Sorted Array (Medium)
-[Leetcode](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) / [力扣](https://leetcode-cn.com/problems/single-element-in-a-sorted-array/description/)
+[Leetcode](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) / [力扣](https://leetcode.cn/problems/single-element-in-a-sorted-array/description/)
```text
Input: [1, 1, 2, 3, 3, 4, 4, 8, 8]
@@ -221,7 +221,7 @@ public int singleNonDuplicate(int[] nums) {
278\. First Bad Version (Easy)
-[Leetcode](https://leetcode.com/problems/first-bad-version/description/) / [力扣](https://leetcode-cn.com/problems/first-bad-version/description/)
+[Leetcode](https://leetcode.com/problems/first-bad-version/description/) / [力扣](https://leetcode.cn/problems/first-bad-version/description/)
题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,在第 x 位置开始出现错误版本,导致后面的版本都错误。可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。
@@ -248,7 +248,7 @@ public int firstBadVersion(int n) {
153\. Find Minimum in Rotated Sorted Array (Medium)
-[Leetcode](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) / [力扣](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/description/)
+[Leetcode](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) / [力扣](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/)
```text
Input: [3,4,5,1,2],
@@ -280,7 +280,7 @@ public int findMin(int[] nums) {
34\. Find First and Last Position of Element in Sorted Array (Medium)
-[Leetcode](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/) / [力扣](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)
+[Leetcode](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/) / [力扣](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/)
```text
Input: nums = [5,7,7,8,8,10], target = 8
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \344\275\215\350\277\220\347\256\227.md" "b/notes/Leetcode \351\242\230\350\247\243 - \344\275\215\350\277\220\347\256\227.md"
index f6dc375a6..719ac4650 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \344\275\215\350\277\220\347\256\227.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \344\275\215\350\277\220\347\256\227.md"
@@ -125,7 +125,7 @@ static String Integer.toBinaryString(int i); // 转换为二进制表示的字
461\. Hamming Distance (Easy)
-[Leetcode](https://leetcode.com/problems/hamming-distance/) / [力扣](https://leetcode-cn.com/problems/hamming-distance/)
+[Leetcode](https://leetcode.com/problems/hamming-distance/) / [力扣](https://leetcode.cn/problems/hamming-distance/)
```text
Input: x = 1, y = 4
@@ -180,7 +180,7 @@ public int hammingDistance(int x, int y) {
136\. Single Number (Easy)
-[Leetcode](https://leetcode.com/problems/single-number/description/) / [力扣](https://leetcode-cn.com/problems/single-number/description/)
+[Leetcode](https://leetcode.com/problems/single-number/description/) / [力扣](https://leetcode.cn/problems/single-number/description/)
```text
Input: [4,1,2,1,2]
@@ -201,7 +201,7 @@ public int singleNumber(int[] nums) {
268\. Missing Number (Easy)
-[Leetcode](https://leetcode.com/problems/missing-number/description/) / [力扣](https://leetcode-cn.com/problems/missing-number/description/)
+[Leetcode](https://leetcode.com/problems/missing-number/description/) / [力扣](https://leetcode.cn/problems/missing-number/description/)
```text
Input: [3,0,1]
@@ -226,7 +226,7 @@ public int missingNumber(int[] nums) {
260\. Single Number III (Medium)
-[Leetcode](https://leetcode.com/problems/single-number-iii/description/) / [力扣](https://leetcode-cn.com/problems/single-number-iii/description/)
+[Leetcode](https://leetcode.com/problems/single-number-iii/description/) / [力扣](https://leetcode.cn/problems/single-number-iii/description/)
题目描述:数组中只有两个元素出现一次,其余元素都出现两次,找出这两个元素。
@@ -252,7 +252,7 @@ public int[] singleNumber(int[] nums) {
190\. Reverse Bits (Easy)
-[Leetcode](https://leetcode.com/problems/reverse-bits/description/) / [力扣](https://leetcode-cn.com/problems/reverse-bits/description/)
+[Leetcode](https://leetcode.com/problems/reverse-bits/description/) / [力扣](https://leetcode.cn/problems/reverse-bits/description/)
```java
public int reverseBits(int n) {
@@ -311,7 +311,7 @@ a = a ^ b;
231\. Power of Two (Easy)
-[Leetcode](https://leetcode.com/problems/power-of-two/description/) / [力扣](https://leetcode-cn.com/problems/power-of-two/description/)
+[Leetcode](https://leetcode.com/problems/power-of-two/description/) / [力扣](https://leetcode.cn/problems/power-of-two/description/)
二进制表示只有一个 1 存在。
@@ -333,7 +333,7 @@ public boolean isPowerOfTwo(int n) {
342\. Power of Four (Easy)
-[Leetcode](https://leetcode.com/problems/power-of-four/) / [力扣](https://leetcode-cn.com/problems/power-of-four/)
+[Leetcode](https://leetcode.com/problems/power-of-four/) / [力扣](https://leetcode.cn/problems/power-of-four/)
这种数在二进制表示中有且只有一个 1,并且位于从右往左数的第 1、3、5…… 位,例如 16(10000)。代码中的掩码正好在这些位上为 1。
@@ -355,7 +355,7 @@ public boolean isPowerOfFour(int num) {
693\. Binary Number with Alternating Bits (Easy)
-[Leetcode](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) / [力扣](https://leetcode-cn.com/problems/binary-number-with-alternating-bits/description/)
+[Leetcode](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) / [力扣](https://leetcode.cn/problems/binary-number-with-alternating-bits/description/)
```text
Input: 10
@@ -382,7 +382,7 @@ public boolean hasAlternatingBits(int n) {
476\. Number Complement (Easy)
-[Leetcode](https://leetcode.com/problems/number-complement/description/) / [力扣](https://leetcode-cn.com/problems/number-complement/description/)
+[Leetcode](https://leetcode.com/problems/number-complement/description/) / [力扣](https://leetcode.cn/problems/number-complement/description/)
```text
Input: 5
@@ -439,7 +439,7 @@ public int findComplement(int num) {
371\. Sum of Two Integers (Medium)
-[Leetcode](https://leetcode.com/problems/sum-of-two-integers/description/) / [力扣](https://leetcode-cn.com/problems/sum-of-two-integers/description/)
+[Leetcode](https://leetcode.com/problems/sum-of-two-integers/description/) / [力扣](https://leetcode.cn/problems/sum-of-two-integers/description/)
a ^ b 表示没有考虑进位的情况下两数的和,(a & b) \<\< 1 就是进位。两者再相加又可能产生进位,重复到进位为 0 为止。例如 5 + 3:
@@ -463,7 +463,7 @@ public int getSum(int a, int b) {
318\. Maximum Product of Word Lengths (Medium)
-[Leetcode](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) / [力扣](https://leetcode-cn.com/problems/maximum-product-of-word-lengths/description/)
+[Leetcode](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) / [力扣](https://leetcode.cn/problems/maximum-product-of-word-lengths/description/)
```text
Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"]
@@ -500,7 +500,7 @@ public int maxProduct(String[] words) {
338\. Counting Bits (Easy)
-[Leetcode](https://leetcode.com/problems/counting-bits/description/) / [力扣](https://leetcode-cn.com/problems/counting-bits/description/)
+[Leetcode](https://leetcode.com/problems/counting-bits/description/) / [力扣](https://leetcode.cn/problems/counting-bits/description/)
i&(i-1) 去掉 i 最低的那一位 1,结果比 i 小、已经算过,因此 dp[i] = dp[i&(i-1)] + 1。例如 dp[6] = dp[4] + 1 = 2(110 去掉最低的 1 得到 100)。
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \345\210\206\346\262\273.md" "b/notes/Leetcode \351\242\230\350\247\243 - \345\210\206\346\262\273.md"
index 5ab72b422..d3d149605 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \345\210\206\346\262\273.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \345\210\206\346\262\273.md"
@@ -11,7 +11,7 @@
241\. Different Ways to Add Parentheses (Medium)
-[Leetcode](https://leetcode.com/problems/different-ways-to-add-parentheses/description/) / [力扣](https://leetcode-cn.com/problems/different-ways-to-add-parentheses/description/)
+[Leetcode](https://leetcode.com/problems/different-ways-to-add-parentheses/description/) / [力扣](https://leetcode.cn/problems/different-ways-to-add-parentheses/description/)
```text
Input: "2-1-1".
@@ -60,7 +60,7 @@ public List diffWaysToCompute(String input) {
95\. Unique Binary Search Trees II (Medium)
-[Leetcode](https://leetcode.com/problems/unique-binary-search-trees-ii/description/) / [力扣](https://leetcode-cn.com/problems/unique-binary-search-trees-ii/description/)
+[Leetcode](https://leetcode.com/problems/unique-binary-search-trees-ii/description/) / [力扣](https://leetcode.cn/problems/unique-binary-search-trees-ii/description/)
给定一个数字 n,要求生成所有值为 1...n 的二叉搜索树。
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \345\212\250\346\200\201\350\247\204\345\210\222.md" "b/notes/Leetcode \351\242\230\350\247\243 - \345\212\250\346\200\201\350\247\204\345\210\222.md"
index a48f15a1c..094d1759b 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \345\212\250\346\200\201\350\247\204\345\210\222.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \345\212\250\346\200\201\350\247\204\345\210\222.md"
@@ -15,6 +15,7 @@
* [1. 数组区间和](#1-数组区间和)
* [2. 数组中等差递增子区间的个数](#2-数组中等差递增子区间的个数)
* [3. 最长有效括号](#3-最长有效括号)
+ * [4. 乘积最大子数组](#4-乘积最大子数组)
* [分割整数](#分割整数)
* [1. 分割整数的最大乘积](#1-分割整数的最大乘积)
* [2. 按平方数来分割整数](#2-按平方数来分割整数)
@@ -55,7 +56,7 @@
70\. Climbing Stairs (Easy)
-[Leetcode](https://leetcode.com/problems/climbing-stairs/description/) / [力扣](https://leetcode-cn.com/problems/climbing-stairs/description/)
+[Leetcode](https://leetcode.com/problems/climbing-stairs/description/) / [力扣](https://leetcode.cn/problems/climbing-stairs/description/)
题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
@@ -86,7 +87,7 @@ public int climbStairs(int n) {
198\. House Robber (Medium)
-[Leetcode](https://leetcode.com/problems/house-robber/description/) / [力扣](https://leetcode-cn.com/problems/house-robber/description/)
+[Leetcode](https://leetcode.com/problems/house-robber/description/) / [力扣](https://leetcode.cn/problems/house-robber/description/)
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
@@ -112,7 +113,7 @@ public int rob(int[] nums) {
213\. House Robber II (Medium)
-[Leetcode](https://leetcode.com/problems/house-robber-ii/description/) / [力扣](https://leetcode-cn.com/problems/house-robber-ii/description/)
+[Leetcode](https://leetcode.com/problems/house-robber-ii/description/) / [力扣](https://leetcode.cn/problems/house-robber-ii/description/)
题目描述:住户围成一圈,首尾两户相邻。
@@ -170,7 +171,7 @@ dp[i] 表示 i 封信都装错的方式数量。第 i 封信装进了第 j 个
64\. Minimum Path Sum (Medium)
-[Leetcode](https://leetcode.com/problems/minimum-path-sum/description/) / [力扣](https://leetcode-cn.com/problems/minimum-path-sum/description/)
+[Leetcode](https://leetcode.com/problems/minimum-path-sum/description/) / [力扣](https://leetcode.cn/problems/minimum-path-sum/description/)
```text
[[1,3,1],
@@ -210,7 +211,7 @@ public int minPathSum(int[][] grid) {
62\. Unique Paths (Medium)
-[Leetcode](https://leetcode.com/problems/unique-paths/description/) / [力扣](https://leetcode-cn.com/problems/unique-paths/description/)
+[Leetcode](https://leetcode.com/problems/unique-paths/description/) / [力扣](https://leetcode.cn/problems/unique-paths/description/)
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。
@@ -283,7 +284,7 @@ public int maximalSquare(char[][] matrix) {
303\. Range Sum Query - Immutable (Easy)
-[Leetcode](https://leetcode.com/problems/range-sum-query-immutable/description/) / [力扣](https://leetcode-cn.com/problems/range-sum-query-immutable/description/)
+[Leetcode](https://leetcode.com/problems/range-sum-query-immutable/description/) / [力扣](https://leetcode.cn/problems/range-sum-query-immutable/description/)
```text
Given nums = [-2, 0, 3, -5, 2, -1]
@@ -317,7 +318,7 @@ class NumArray {
413\. Arithmetic Slices (Medium)
-[Leetcode](https://leetcode.com/problems/arithmetic-slices/description/) / [力扣](https://leetcode-cn.com/problems/arithmetic-slices/description/)
+[Leetcode](https://leetcode.com/problems/arithmetic-slices/description/) / [力扣](https://leetcode.cn/problems/arithmetic-slices/description/)
```text
A = [0, 1, 2, 3, 4]
@@ -441,13 +442,53 @@ public int longestValidParentheses(String s) {
}
```
+### 4. 乘积最大子数组
+
+152\. Maximum Product Subarray (Medium)
+
+[Leetcode](https://leetcode.com/problems/maximum-product-subarray/) / [力扣](https://leetcode.cn/problems/maximum-product-subarray/)
+
+```text
+Input: [3, -1, 2, -4]
+Output: 24,子数组为整个数组
+```
+
+与 [子数组最大的和](Leetcode%20题解%20-%20贪心思想.md#10-子数组最大的和) 类似,都看以当前元素结尾的子数组,但乘积不能只保存最大值:负数乘上最小的负积会变成最大的正积。所以同时维护以 nums[i] 结尾的最大积 max 和最小积 min,三个候选是 nums[i] 本身、max × nums[i] 和 min × nums[i]。
+
+以 [3, -1, 2, -4] 为例:
+
+```text
+i nums[i] max min 结果
+0 3 3 3 3
+1 -1 -1 -3 3
+2 2 2 -6 3
+3 -4 24 -8 24
+```
+
+走到 2 时最大积只有 2,-6 看起来没用,但它乘上 -4 就得到 24。只保存最大积的话,最后只能得到 3。
+
+```java
+public int maxProduct(int[] nums) {
+ int max = nums[0], min = nums[0], ret = nums[0];
+ for (int i = 1; i < nums.length; i++) {
+ int a = max * nums[i], b = min * nums[i];
+ max = Math.max(nums[i], Math.max(a, b));
+ min = Math.min(nums[i], Math.min(a, b));
+ ret = Math.max(ret, max);
+ }
+ return ret;
+}
+```
+
+只遍历一遍数组,时间复杂度 O(N);只保存三个变量,空间复杂度 O(1)。
+
## 分割整数
### 1. 分割整数的最大乘积
343\. Integer Break (Medium)
-[Leetcode](https://leetcode.com/problems/integer-break/description/) / [力扣](https://leetcode-cn.com/problems/integer-break/description/)
+[Leetcode](https://leetcode.com/problems/integer-break/description/) / [力扣](https://leetcode.cn/problems/integer-break/description/)
题目描述:把正整数 n 拆成至少两个正整数的和,求这些整数乘积的最大值。For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
@@ -470,7 +511,7 @@ public int integerBreak(int n) {
279\. Perfect Squares (Medium)
-[Leetcode](https://leetcode.com/problems/perfect-squares/description/) / [力扣](https://leetcode-cn.com/problems/perfect-squares/description/)
+[Leetcode](https://leetcode.com/problems/perfect-squares/description/) / [力扣](https://leetcode.cn/problems/perfect-squares/description/)
题目描述:把 n 表示成若干个完全平方数的和,求最少需要几个。For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
@@ -510,7 +551,7 @@ private List generateSquareList(int n) {
91\. Decode Ways (Medium)
-[Leetcode](https://leetcode.com/problems/decode-ways/description/) / [力扣](https://leetcode-cn.com/problems/decode-ways/description/)
+[Leetcode](https://leetcode.com/problems/decode-ways/description/) / [力扣](https://leetcode.cn/problems/decode-ways/description/)
题目描述:字母 A \~ Z 分别编码为 1 \~ 26,求一个数字串有多少种解码方法。Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).
@@ -560,7 +601,7 @@ public int numDecodings(String s) {
300\. Longest Increasing Subsequence (Medium)
-[Leetcode](https://leetcode.com/problems/longest-increasing-subsequence/description/) / [力扣](https://leetcode-cn.com/problems/longest-increasing-subsequence/description/)
+[Leetcode](https://leetcode.com/problems/longest-increasing-subsequence/description/) / [力扣](https://leetcode.cn/problems/longest-increasing-subsequence/description/)
```java
public int lengthOfLIS(int[] nums) {
@@ -629,7 +670,7 @@ private int binarySearch(int[] tails, int len, int key) {
646\. Maximum Length of Pair Chain (Medium)
-[Leetcode](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) / [力扣](https://leetcode-cn.com/problems/maximum-length-of-pair-chain/description/)
+[Leetcode](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) / [力扣](https://leetcode.cn/problems/maximum-length-of-pair-chain/description/)
```text
Input: [[1,2], [2,3], [3,4]]
@@ -665,7 +706,7 @@ public int findLongestChain(int[][] pairs) {
376\. Wiggle Subsequence (Medium)
-[Leetcode](https://leetcode.com/problems/wiggle-subsequence/description/) / [力扣](https://leetcode-cn.com/problems/wiggle-subsequence/description/)
+[Leetcode](https://leetcode.com/problems/wiggle-subsequence/description/) / [力扣](https://leetcode.cn/problems/wiggle-subsequence/description/)
```text
Input: [1,7,4,9,2,5]
@@ -726,7 +767,7 @@ public int wiggleMaxLength(int[] nums) {
1143\. Longest Common Subsequence
-[Leetcode](https://leetcode.com/problems/longest-common-subsequence/) / [力扣](https://leetcode-cn.com/problems/longest-common-subsequence/)
+[Leetcode](https://leetcode.com/problems/longest-common-subsequence/) / [力扣](https://leetcode.cn/problems/longest-common-subsequence/)
```java
public int longestCommonSubsequence(String text1, String text2) {
@@ -850,7 +891,7 @@ public int knapsack(int W, int N, int[] weights, int[] values) {
416\. Partition Equal Subset Sum (Medium)
-[Leetcode](https://leetcode.com/problems/partition-equal-subset-sum/description/) / [力扣](https://leetcode-cn.com/problems/partition-equal-subset-sum/description/)
+[Leetcode](https://leetcode.com/problems/partition-equal-subset-sum/description/) / [力扣](https://leetcode.cn/problems/partition-equal-subset-sum/description/)
```text
Input: [1, 5, 11, 5]
@@ -892,7 +933,7 @@ private int computeArraySum(int[] nums) {
494\. Target Sum (Medium)
-[Leetcode](https://leetcode.com/problems/target-sum/description/) / [力扣](https://leetcode-cn.com/problems/target-sum/description/)
+[Leetcode](https://leetcode.com/problems/target-sum/description/) / [力扣](https://leetcode.cn/problems/target-sum/description/)
```text
Input: nums is [1, 1, 1, 1, 1], S is 3.
@@ -966,7 +1007,7 @@ private int findTargetSumWays(int[] nums, int start, int S) {
474\. Ones and Zeroes (Medium)
-[Leetcode](https://leetcode.com/problems/ones-and-zeroes/description/) / [力扣](https://leetcode-cn.com/problems/ones-and-zeroes/description/)
+[Leetcode](https://leetcode.com/problems/ones-and-zeroes/description/) / [力扣](https://leetcode.cn/problems/ones-and-zeroes/description/)
```text
Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
@@ -1006,7 +1047,7 @@ public int findMaxForm(String[] strs, int m, int n) {
322\. Coin Change (Medium)
-[Leetcode](https://leetcode.com/problems/coin-change/description/) / [力扣](https://leetcode-cn.com/problems/coin-change/description/)
+[Leetcode](https://leetcode.com/problems/coin-change/description/) / [力扣](https://leetcode.cn/problems/coin-change/description/)
```text
Example 1:
@@ -1046,7 +1087,7 @@ public int coinChange(int[] coins, int amount) {
518\. Coin Change II (Medium)
-[Leetcode](https://leetcode.com/problems/coin-change-ii/description/) / [力扣](https://leetcode-cn.com/problems/coin-change-ii/description/)
+[Leetcode](https://leetcode.com/problems/coin-change-ii/description/) / [力扣](https://leetcode.cn/problems/coin-change-ii/description/)
```text
Input: amount = 5, coins = [1, 2, 5]
@@ -1080,7 +1121,7 @@ public int change(int amount, int[] coins) {
139\. Word Break (Medium)
-[Leetcode](https://leetcode.com/problems/word-break/description/) / [力扣](https://leetcode-cn.com/problems/word-break/description/)
+[Leetcode](https://leetcode.com/problems/word-break/description/) / [力扣](https://leetcode.cn/problems/word-break/description/)
```text
s = "leetcode",
@@ -1113,7 +1154,7 @@ public boolean wordBreak(String s, List wordDict) {
377\. Combination Sum IV (Medium)
-[Leetcode](https://leetcode.com/problems/combination-sum-iv/description/) / [力扣](https://leetcode-cn.com/problems/combination-sum-iv/description/)
+[Leetcode](https://leetcode.com/problems/combination-sum-iv/description/) / [力扣](https://leetcode.cn/problems/combination-sum-iv/description/)
```text
nums = [1, 2, 3]
@@ -1163,7 +1204,7 @@ public int combinationSum4(int[] nums, int target) {
309\. Best Time to Buy and Sell Stock with Cooldown (Medium)
-[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/) / [力扣](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/)
+[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/) / [力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/)
题目描述:可以进行多次交易,但卖出之后需要有一天的冷却时间才能再次买入。
@@ -1197,7 +1238,7 @@ public int maxProfit(int[] prices) {
714\. Best Time to Buy and Sell Stock with Transaction Fee (Medium)
-[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/) / [力扣](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/)
+[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/) / [力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/)
```text
Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
@@ -1239,7 +1280,7 @@ public int maxProfit(int[] prices, int fee) {
123\. Best Time to Buy and Sell Stock III (Hard)
-[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) / [力扣](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/description/)
+[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) / [力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/)
四个变量依次是到当天为止完成第一次买入、第一次卖出、第二次买入、第二次卖出后的最大收益,第二次买入以第一次卖出的收益为起点。
@@ -1269,7 +1310,7 @@ public int maxProfit(int[] prices) {
188\. Best Time to Buy and Sell Stock IV (Hard)
-[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) / [力扣](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/description/)
+[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) / [力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/)
maxProfit[i][j] 表示最多交易 i 次、到第 j 天为止的最大收益。第 j 天要么不卖出,收益为 maxProfit[i][j - 1];要么在之前的某天 m 买入、第 j 天卖出,收益为 prices[j] + max(maxProfit[i - 1][m] - prices[m]),localMax 在遍历 j 时维护这个最大值。k >= n / 2 时次数不构成限制,累加所有上涨的差价即可。
@@ -1303,7 +1344,7 @@ public int maxProfit(int k, int[] prices) {
583\. Delete Operation for Two Strings (Medium)
-[Leetcode](https://leetcode.com/problems/delete-operation-for-two-strings/description/) / [力扣](https://leetcode-cn.com/problems/delete-operation-for-two-strings/description/)
+[Leetcode](https://leetcode.com/problems/delete-operation-for-two-strings/description/) / [力扣](https://leetcode.cn/problems/delete-operation-for-two-strings/description/)
```text
Input: "sea", "eat"
@@ -1334,7 +1375,7 @@ public int minDistance(String word1, String word2) {
72\. Edit Distance (Medium)
-[Leetcode](https://leetcode.com/problems/edit-distance/description/) / [力扣](https://leetcode-cn.com/problems/edit-distance/description/)
+[Leetcode](https://leetcode.com/problems/edit-distance/description/) / [力扣](https://leetcode.cn/problems/edit-distance/description/)
```text
Example 1:
@@ -1395,7 +1436,7 @@ public int minDistance(String word1, String word2) {
650\. 2 Keys Keyboard (Medium)
-[Leetcode](https://leetcode.com/problems/2-keys-keyboard/description/) / [力扣](https://leetcode-cn.com/problems/2-keys-keyboard/description/)
+[Leetcode](https://leetcode.com/problems/2-keys-keyboard/description/) / [力扣](https://leetcode.cn/problems/2-keys-keyboard/description/)
题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \345\217\214\346\214\207\351\222\210.md" "b/notes/Leetcode \351\242\230\350\247\243 - \345\217\214\346\214\207\351\222\210.md"
index 9ce627d11..1fea1b9c8 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \345\217\214\346\214\207\351\222\210.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \345\217\214\346\214\207\351\222\210.md"
@@ -22,7 +22,7 @@
167\. Two Sum II - Input Array Is Sorted (Medium)
-[Leetcode](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) / [力扣](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/description/)
+[Leetcode](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) / [力扣](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/description/)
```text
Input: numbers={2, 7, 11, 15}, target=9
@@ -65,7 +65,7 @@ public int[] twoSum(int[] numbers, int target) {
633\. Sum of Square Numbers (Medium)
-[Leetcode](https://leetcode.com/problems/sum-of-square-numbers/description/) / [力扣](https://leetcode-cn.com/problems/sum-of-square-numbers/description/)
+[Leetcode](https://leetcode.com/problems/sum-of-square-numbers/description/) / [力扣](https://leetcode.cn/problems/sum-of-square-numbers/description/)
```text
Input: 5
@@ -103,7 +103,7 @@ Explanation: 1 * 1 + 2 * 2 = 5
345\. Reverse Vowels of a String (Easy)
-[Leetcode](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) / [力扣](https://leetcode-cn.com/problems/reverse-vowels-of-a-string/description/)
+[Leetcode](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) / [力扣](https://leetcode.cn/problems/reverse-vowels-of-a-string/description/)
```text
Given s = "leetcode", return "leotcede".
@@ -148,7 +148,7 @@ public String reverseVowels(String s) {
680\. Valid Palindrome II (Easy)
-[Leetcode](https://leetcode.com/problems/valid-palindrome-ii/description/) / [力扣](https://leetcode-cn.com/problems/valid-palindrome-ii/description/)
+[Leetcode](https://leetcode.com/problems/valid-palindrome-ii/description/) / [力扣](https://leetcode.cn/problems/valid-palindrome-ii/description/)
```text
Input: "abca"
@@ -196,7 +196,7 @@ private boolean isPalindrome(String s, int i, int j) {
88\. Merge Sorted Array (Easy)
-[Leetcode](https://leetcode.com/problems/merge-sorted-array/description/) / [力扣](https://leetcode-cn.com/problems/merge-sorted-array/description/)
+[Leetcode](https://leetcode.com/problems/merge-sorted-array/description/) / [力扣](https://leetcode.cn/problems/merge-sorted-array/description/)
```text
Input:
@@ -226,11 +226,13 @@ public void merge(int[] nums1, int m, int[] nums2, int n) {
}
```
+每次循环写入一个位置,最多 m + n 次,时间复杂度 O(m + n);直接在 nums1 上归并,空间复杂度 O(1)。
+
## 6. 判断链表是否存在环
141\. Linked List Cycle (Easy)
-[Leetcode](https://leetcode.com/problems/linked-list-cycle/description/) / [力扣](https://leetcode-cn.com/problems/linked-list-cycle/description/)
+[Leetcode](https://leetcode.com/problems/linked-list-cycle/description/) / [力扣](https://leetcode.cn/problems/linked-list-cycle/description/)
使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。这是因为进入环之后,快指针每次比慢指针多走一步,两者的距离每次缩小 1,不会跳过慢指针。
@@ -253,11 +255,13 @@ public boolean hasCycle(ListNode head) {
}
```
+慢指针进入环之前最多走 N 步,进入环之后快指针每次追近 1 步,不超过环长就能追上,时间复杂度 O(N);只用两个指针,空间复杂度 O(1)。用 HashSet 记录访问过的节点也能判断,但需要 O(N) 的空间。
+
## 7. 最长子序列
524\. Longest Word in Dictionary through Deleting (Medium)
-[Leetcode](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/) / [力扣](https://leetcode-cn.com/problems/longest-word-in-dictionary-through-deleting/description/)
+[Leetcode](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/) / [力扣](https://leetcode.cn/problems/longest-word-in-dictionary-through-deleting/description/)
```text
Input:
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \345\223\210\345\270\214\350\241\250.md" "b/notes/Leetcode \351\242\230\350\247\243 - \345\223\210\345\270\214\350\241\250.md"
index 9e0f8f901..34102a6b1 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \345\223\210\345\270\214\350\241\250.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \345\223\210\345\270\214\350\241\250.md"
@@ -14,14 +14,14 @@
- Java 中的 HashSet 用于存储一个集合,可以查找元素是否在集合中。如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。
-- Java 中的 HashMap 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中(535\. Encode and Decode TinyURL (Medium),[Leetcode](https://leetcode.com/problems/encode-and-decode-tinyurl/description/) / [力扣](https://leetcode-cn.com/problems/encode-and-decode-tinyurl/description/)),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。
+- Java 中的 HashMap 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中(535\. Encode and Decode TinyURL (Medium),[Leetcode](https://leetcode.com/problems/encode-and-decode-tinyurl/description/) / [力扣](https://leetcode.cn/problems/encode-and-decode-tinyurl/description/)),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。
## 1. 数组中两个数的和为给定值
1\. Two Sum (Easy)
-[Leetcode](https://leetcode.com/problems/two-sum/description/) / [力扣](https://leetcode-cn.com/problems/two-sum/description/)
+[Leetcode](https://leetcode.com/problems/two-sum/description/) / [力扣](https://leetcode.cn/problems/two-sum/description/)
可以先对数组进行排序,然后使用双指针方法或者二分查找方法,时间复杂度为 O(NlogN)。需要注意的是,题目要求返回原数组的下标,排序会打乱下标,因此要把元素和原下标一起排序,空间复杂度为 O(N)。
@@ -45,7 +45,7 @@ public int[] twoSum(int[] nums, int target) {
217\. Contains Duplicate (Easy)
-[Leetcode](https://leetcode.com/problems/contains-duplicate/description/) / [力扣](https://leetcode-cn.com/problems/contains-duplicate/description/)
+[Leetcode](https://leetcode.com/problems/contains-duplicate/description/) / [力扣](https://leetcode.cn/problems/contains-duplicate/description/)
```java
public boolean containsDuplicate(int[] nums) {
@@ -61,7 +61,7 @@ public boolean containsDuplicate(int[] nums) {
594\. Longest Harmonious Subsequence (Easy)
-[Leetcode](https://leetcode.com/problems/longest-harmonious-subsequence/description/) / [力扣](https://leetcode-cn.com/problems/longest-harmonious-subsequence/description/)
+[Leetcode](https://leetcode.com/problems/longest-harmonious-subsequence/description/) / [力扣](https://leetcode.cn/problems/longest-harmonious-subsequence/description/)
```text
Input: [1,3,2,2,5,2,3,7]
@@ -91,7 +91,7 @@ public int findLHS(int[] nums) {
128\. Longest Consecutive Sequence (Medium)
-[Leetcode](https://leetcode.com/problems/longest-consecutive-sequence/description/) / [力扣](https://leetcode-cn.com/problems/longest-consecutive-sequence/description/)
+[Leetcode](https://leetcode.com/problems/longest-consecutive-sequence/description/) / [力扣](https://leetcode.cn/problems/longest-consecutive-sequence/description/)
```text
Given [100, 4, 200, 1, 3, 2],
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \345\233\276.md" "b/notes/Leetcode \351\242\230\350\247\243 - \345\233\276.md"
index ace24f8db..d5ff6b205 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \345\233\276.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \345\233\276.md"
@@ -20,7 +20,7 @@
785\. Is Graph Bipartite? (Medium)
-[Leetcode](https://leetcode.com/problems/is-graph-bipartite/description/) / [力扣](https://leetcode-cn.com/problems/is-graph-bipartite/description/)
+[Leetcode](https://leetcode.com/problems/is-graph-bipartite/description/) / [力扣](https://leetcode.cn/problems/is-graph-bipartite/description/)
```text
Input: [[1,3], [0,2], [1,3], [0,2]]
@@ -83,7 +83,7 @@ private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] gra
207\. Course Schedule (Medium)
-[Leetcode](https://leetcode.com/problems/course-schedule/description/) / [力扣](https://leetcode-cn.com/problems/course-schedule/description/)
+[Leetcode](https://leetcode.com/problems/course-schedule/description/) / [力扣](https://leetcode.cn/problems/course-schedule/description/)
```text
2, [[1,0]]
@@ -145,7 +145,7 @@ private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked,
210\. Course Schedule II (Medium)
-[Leetcode](https://leetcode.com/problems/course-schedule-ii/description/) / [力扣](https://leetcode-cn.com/problems/course-schedule-ii/description/)
+[Leetcode](https://leetcode.com/problems/course-schedule-ii/description/) / [力扣](https://leetcode.cn/problems/course-schedule-ii/description/)
```text
4, [[1,0],[2,0],[3,1],[3,2]]
@@ -246,7 +246,7 @@ public int[] findOrder(int numCourses, int[][] prerequisites) {
684\. Redundant Connection (Medium)
-[Leetcode](https://leetcode.com/problems/redundant-connection/description/) / [力扣](https://leetcode-cn.com/problems/redundant-connection/description/)
+[Leetcode](https://leetcode.com/problems/redundant-connection/description/) / [力扣](https://leetcode.cn/problems/redundant-connection/description/)
```text
Input: [[1,2], [1,3], [2,3]]
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \345\255\227\347\254\246\344\270\262.md" "b/notes/Leetcode \351\242\230\350\247\243 - \345\255\227\347\254\246\344\270\262.md"
index a0732c821..7084bacdb 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \345\255\227\347\254\246\344\270\262.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \345\255\227\347\254\246\344\270\262.md"
@@ -58,7 +58,7 @@ Return "student a am I"
242\. Valid Anagram (Easy)
-[Leetcode](https://leetcode.com/problems/valid-anagram/description/) / [力扣](https://leetcode-cn.com/problems/valid-anagram/description/)
+[Leetcode](https://leetcode.com/problems/valid-anagram/description/) / [力扣](https://leetcode.cn/problems/valid-anagram/description/)
```text
s = "anagram", t = "nagaram", return true.
@@ -91,7 +91,7 @@ public boolean isAnagram(String s, String t) {
409\. Longest Palindrome (Easy)
-[Leetcode](https://leetcode.com/problems/longest-palindrome/description/) / [力扣](https://leetcode-cn.com/problems/longest-palindrome/description/)
+[Leetcode](https://leetcode.com/problems/longest-palindrome/description/) / [力扣](https://leetcode.cn/problems/longest-palindrome/description/)
```text
Input : "abccccdd"
@@ -124,7 +124,7 @@ public int longestPalindrome(String s) {
205\. Isomorphic Strings (Easy)
-[Leetcode](https://leetcode.com/problems/isomorphic-strings/description/) / [力扣](https://leetcode-cn.com/problems/isomorphic-strings/description/)
+[Leetcode](https://leetcode.com/problems/isomorphic-strings/description/) / [力扣](https://leetcode.cn/problems/isomorphic-strings/description/)
```text
Given "egg", "add", return true.
@@ -154,7 +154,7 @@ public boolean isIsomorphic(String s, String t) {
647\. Palindromic Substrings (Medium)
-[Leetcode](https://leetcode.com/problems/palindromic-substrings/description/) / [力扣](https://leetcode-cn.com/problems/palindromic-substrings/description/)
+[Leetcode](https://leetcode.com/problems/palindromic-substrings/description/) / [力扣](https://leetcode.cn/problems/palindromic-substrings/description/)
```text
Input: "aaa"
@@ -188,7 +188,7 @@ private void extendSubstrings(String s, int start, int end) {
9\. Palindrome Number (Easy)
-[Leetcode](https://leetcode.com/problems/palindrome-number/description/) / [力扣](https://leetcode-cn.com/problems/palindrome-number/description/)
+[Leetcode](https://leetcode.com/problems/palindrome-number/description/) / [力扣](https://leetcode.cn/problems/palindrome-number/description/)
进阶要求不将整数转换为字符串进行判断。
@@ -215,7 +215,7 @@ public boolean isPalindrome(int x) {
696\. Count Binary Substrings (Easy)
-[Leetcode](https://leetcode.com/problems/count-binary-substrings/description/) / [力扣](https://leetcode-cn.com/problems/count-binary-substrings/description/)
+[Leetcode](https://leetcode.com/problems/count-binary-substrings/description/) / [力扣](https://leetcode.cn/problems/count-binary-substrings/description/)
```text
Input: "00110011"
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \346\216\222\345\272\217.md" "b/notes/Leetcode \351\242\230\350\247\243 - \346\216\222\345\272\217.md"
index ce8b6ce84..1980e4538 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \346\216\222\345\272\217.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \346\216\222\345\272\217.md"
@@ -31,7 +31,7 @@
215\. Kth Largest Element in an Array (Medium)
-[Leetcode](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) / [力扣](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/description/)
+[Leetcode](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) / [力扣](https://leetcode.cn/problems/kth-largest-element-in-an-array/description/)
```text
Input: [3,2,1,5,6,4] and k = 2
@@ -123,7 +123,7 @@ private void shuffle(int[] a) {
347\. Top K Frequent Elements (Medium)
-[Leetcode](https://leetcode.com/problems/top-k-frequent-elements/description/) / [力扣](https://leetcode-cn.com/problems/top-k-frequent-elements/description/)
+[Leetcode](https://leetcode.com/problems/top-k-frequent-elements/description/) / [力扣](https://leetcode.cn/problems/top-k-frequent-elements/description/)
```text
Given [1,1,1,2,2,3] and k = 2, return [1,2].
@@ -170,7 +170,7 @@ public int[] topKFrequent(int[] nums, int k) {
451\. Sort Characters By Frequency (Medium)
-[Leetcode](https://leetcode.com/problems/sort-characters-by-frequency/description/) / [力扣](https://leetcode-cn.com/problems/sort-characters-by-frequency/description/)
+[Leetcode](https://leetcode.com/problems/sort-characters-by-frequency/description/) / [力扣](https://leetcode.cn/problems/sort-characters-by-frequency/description/)
```text
Input:
@@ -228,7 +228,7 @@ public String frequencySort(String s) {
75\. Sort Colors (Medium)
-[Leetcode](https://leetcode.com/problems/sort-colors/description/) / [力扣](https://leetcode-cn.com/problems/sort-colors/description/)
+[Leetcode](https://leetcode.com/problems/sort-colors/description/) / [力扣](https://leetcode.cn/problems/sort-colors/description/)
```text
Input: [2,0,2,1,1,0]
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \346\220\234\347\264\242.md" "b/notes/Leetcode \351\242\230\350\247\243 - \346\220\234\347\264\242.md"
index 155a05f13..b7dd3a5f4 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \346\220\234\347\264\242.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \346\220\234\347\264\242.md"
@@ -70,7 +70,7 @@
1091\. Shortest Path in Binary Matrix (Medium)
-[Leetcode](https://leetcode.com/problems/shortest-path-in-binary-matrix/) / [力扣](https://leetcode-cn.com/problems/shortest-path-in-binary-matrix/)
+[Leetcode](https://leetcode.com/problems/shortest-path-in-binary-matrix/) / [力扣](https://leetcode.cn/problems/shortest-path-in-binary-matrix/)
```text
Input: grid = [[0,0,0],
@@ -121,7 +121,7 @@ public int shortestPathBinaryMatrix(int[][] grids) {
279\. Perfect Squares (Medium)
-[Leetcode](https://leetcode.com/problems/perfect-squares/description/) / [力扣](https://leetcode-cn.com/problems/perfect-squares/description/)
+[Leetcode](https://leetcode.com/problems/perfect-squares/description/) / [力扣](https://leetcode.cn/problems/perfect-squares/description/)
```text
For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
@@ -186,7 +186,7 @@ private List generateSquares(int n) {
127\. Word Ladder (Hard)
-[Leetcode](https://leetcode.com/problems/word-ladder/description/) / [力扣](https://leetcode-cn.com/problems/word-ladder/description/)
+[Leetcode](https://leetcode.com/problems/word-ladder/description/) / [力扣](https://leetcode.cn/problems/word-ladder/description/)
```text
Input:
@@ -301,7 +301,7 @@ private int getShortestPath(List[] graphic, int start, int end) {
695\. Max Area of Island (Medium)
-[Leetcode](https://leetcode.com/problems/max-area-of-island/description/) / [力扣](https://leetcode-cn.com/problems/max-area-of-island/description/)
+[Leetcode](https://leetcode.com/problems/max-area-of-island/description/) / [力扣](https://leetcode.cn/problems/max-area-of-island/description/)
```text
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
@@ -352,7 +352,7 @@ private int dfs(int[][] grid, int r, int c) {
200\. Number of Islands (Medium)
-[Leetcode](https://leetcode.com/problems/number-of-islands/description/) / [力扣](https://leetcode-cn.com/problems/number-of-islands/description/)
+[Leetcode](https://leetcode.com/problems/number-of-islands/description/) / [力扣](https://leetcode.cn/problems/number-of-islands/description/)
```text
Input:
@@ -399,6 +399,8 @@ private void dfs(char[][] grid, int i, int j) {
}
```
+每个格子最多被改为 0 一次,之后对它的 dfs 调用都立即返回,而每个格子最多被 4 个相邻格子各调用一次,时间复杂度 O(MN);全是 1 时递归深度为 MN,空间复杂度 O(MN)。
+
需要注意的是,递归深度最多等于岛屿的格子数,300 × 300 的网格全是 1 时深达 9 万层,JDK 21 默认的线程栈会溢出(见[附录 A1](#a1-岛屿数量的递归深度)),这时可以把 dfs 换成下面的 BFS:
```java
@@ -424,7 +426,7 @@ private void bfs(char[][] grid, int i, int j) {
547\. Number of Provinces (Medium)
-[Leetcode](https://leetcode.com/problems/number-of-provinces/description/) / [力扣](https://leetcode-cn.com/problems/number-of-provinces/description/)
+[Leetcode](https://leetcode.com/problems/number-of-provinces/description/) / [力扣](https://leetcode.cn/problems/number-of-provinces/description/)
```text
Input:
@@ -470,7 +472,7 @@ private void dfs(int[][] M, int i, boolean[] hasVisited) {
130\. Surrounded Regions (Medium)
-[Leetcode](https://leetcode.com/problems/surrounded-regions/description/) / [力扣](https://leetcode-cn.com/problems/surrounded-regions/description/)
+[Leetcode](https://leetcode.com/problems/surrounded-regions/description/) / [力扣](https://leetcode.cn/problems/surrounded-regions/description/)
```text
For example,
@@ -539,7 +541,7 @@ private void dfs(char[][] board, int r, int c) {
417\. Pacific Atlantic Water Flow (Medium)
-[Leetcode](https://leetcode.com/problems/pacific-atlantic-water-flow/description/) / [力扣](https://leetcode-cn.com/problems/pacific-atlantic-water-flow/description/)
+[Leetcode](https://leetcode.com/problems/pacific-atlantic-water-flow/description/) / [力扣](https://leetcode.cn/problems/pacific-atlantic-water-flow/description/)
```text
Given the following 5x5 matrix:
@@ -651,7 +653,7 @@ backtracking(当前选择):
17\. Letter Combinations of a Phone Number (Medium)
-[Leetcode](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) / [力扣](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/description/)
+[Leetcode](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) / [力扣](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/)
@@ -691,7 +693,7 @@ private void doCombination(StringBuilder prefix, List combinations, fina
93\. Restore IP Addresses (Medium)
-[Leetcode](https://leetcode.com/problems/restore-ip-addresses/description/) / [力扣](https://leetcode-cn.com/problems/restore-ip-addresses/description/)
+[Leetcode](https://leetcode.com/problems/restore-ip-addresses/description/) / [力扣](https://leetcode.cn/problems/restore-ip-addresses/description/)
```text
Given "25525511135",
@@ -736,7 +738,7 @@ private void doRestore(int k, StringBuilder tempAddress, List addresses,
79\. Word Search (Medium)
-[Leetcode](https://leetcode.com/problems/word-search/description/) / [力扣](https://leetcode-cn.com/problems/word-search/description/)
+[Leetcode](https://leetcode.com/problems/word-search/description/) / [力扣](https://leetcode.cn/problems/word-search/description/)
```text
For example,
@@ -809,7 +811,7 @@ private boolean backtracking(int curLen, int r, int c, boolean[][] visited, fina
257\. Binary Tree Paths (Easy)
-[Leetcode](https://leetcode.com/problems/binary-tree-paths/description/) / [力扣](https://leetcode-cn.com/problems/binary-tree-paths/description/)
+[Leetcode](https://leetcode.com/problems/binary-tree-paths/description/) / [力扣](https://leetcode.cn/problems/binary-tree-paths/description/)
```text
1
@@ -868,7 +870,7 @@ private String buildPath(List values) {
46\. Permutations (Medium)
-[Leetcode](https://leetcode.com/problems/permutations/description/) / [力扣](https://leetcode-cn.com/problems/permutations/description/)
+[Leetcode](https://leetcode.com/problems/permutations/description/) / [力扣](https://leetcode.cn/problems/permutations/description/)
```text
[1,2,3] have the following permutations:
@@ -917,7 +919,7 @@ private void backtracking(List permuteList, List> permute
47\. Permutations II (Medium)
-[Leetcode](https://leetcode.com/problems/permutations-ii/description/) / [力扣](https://leetcode-cn.com/problems/permutations-ii/description/)
+[Leetcode](https://leetcode.com/problems/permutations-ii/description/) / [力扣](https://leetcode.cn/problems/permutations-ii/description/)
```text
[1,1,2] have the following unique permutations:
@@ -968,7 +970,7 @@ private void backtracking(List permuteList, List> permute
77\. Combinations (Medium)
-[Leetcode](https://leetcode.com/problems/combinations/description/) / [力扣](https://leetcode-cn.com/problems/combinations/description/)
+[Leetcode](https://leetcode.com/problems/combinations/description/) / [力扣](https://leetcode.cn/problems/combinations/description/)
```text
If n = 4 and k = 2, a solution is:
@@ -1009,7 +1011,7 @@ private void backtracking(List combineList, List> combina
39\. Combination Sum (Medium)
-[Leetcode](https://leetcode.com/problems/combination-sum/description/) / [力扣](https://leetcode-cn.com/problems/combination-sum/description/)
+[Leetcode](https://leetcode.com/problems/combination-sum/description/) / [力扣](https://leetcode.cn/problems/combination-sum/description/)
```text
given candidate set [2, 3, 6, 7] and target 7,
@@ -1047,7 +1049,7 @@ private void backtracking(List tempCombination, List> com
40\. Combination Sum II (Medium)
-[Leetcode](https://leetcode.com/problems/combination-sum-ii/description/) / [力扣](https://leetcode-cn.com/problems/combination-sum-ii/description/)
+[Leetcode](https://leetcode.com/problems/combination-sum-ii/description/) / [力扣](https://leetcode.cn/problems/combination-sum-ii/description/)
```text
For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8,
@@ -1096,7 +1098,7 @@ private void backtracking(List tempCombination, List> com
216\. Combination Sum III (Medium)
-[Leetcode](https://leetcode.com/problems/combination-sum-iii/description/) / [力扣](https://leetcode-cn.com/problems/combination-sum-iii/description/)
+[Leetcode](https://leetcode.com/problems/combination-sum-iii/description/) / [力扣](https://leetcode.cn/problems/combination-sum-iii/description/)
```text
Input: k = 3, n = 9
@@ -1138,7 +1140,7 @@ private void backtracking(int k, int n, int start,
78\. Subsets (Medium)
-[Leetcode](https://leetcode.com/problems/subsets/description/) / [力扣](https://leetcode-cn.com/problems/subsets/description/)
+[Leetcode](https://leetcode.com/problems/subsets/description/) / [力扣](https://leetcode.cn/problems/subsets/description/)
找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复。
@@ -1173,7 +1175,7 @@ private void backtracking(int start, List tempSubset, List tempSubset, List
@@ -1339,7 +1341,7 @@ private int cubeNum(int i, int j) {
51\. N-Queens (Hard)
-[Leetcode](https://leetcode.com/problems/n-queens/description/) / [力扣](https://leetcode-cn.com/problems/n-queens/description/)
+[Leetcode](https://leetcode.com/problems/n-queens/description/) / [力扣](https://leetcode.cn/problems/n-queens/description/)
@@ -1407,7 +1409,7 @@ private void backtracking(int row) {
22\. Generate Parentheses (Medium)
-[Leetcode](https://leetcode.com/problems/generate-parentheses/description/) / [力扣](https://leetcode-cn.com/problems/generate-parentheses/description/)
+[Leetcode](https://leetcode.com/problems/generate-parentheses/description/) / [力扣](https://leetcode.cn/problems/generate-parentheses/description/)
```text
Input: n = 3
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\345\255\246.md" "b/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\345\255\246.md"
index a2ec3928e..9091564ad 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\345\255\246.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\345\255\246.md"
@@ -52,7 +52,7 @@ x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) \* 3max(
204\. Count Primes (Medium)
-[Leetcode](https://leetcode.com/problems/count-primes/description/) / [力扣](https://leetcode-cn.com/problems/count-primes/description/)
+[Leetcode](https://leetcode.com/problems/count-primes/description/) / [力扣](https://leetcode.cn/problems/count-primes/description/)
埃拉托斯特尼筛法在每次找到一个素数时,将能被素数整除的数排除掉。
@@ -134,7 +134,7 @@ private boolean isEven(int x) {
504\. Base 7 (Easy)
-[Leetcode](https://leetcode.com/problems/base-7/description/) / [力扣](https://leetcode-cn.com/problems/base-7/description/)
+[Leetcode](https://leetcode.com/problems/base-7/description/) / [力扣](https://leetcode.cn/problems/base-7/description/)
```java
public String convertToBase7(int num) {
@@ -167,7 +167,7 @@ public String convertToBase7(int num) {
405\. Convert a Number to Hexadecimal (Easy)
-[Leetcode](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/) / [力扣](https://leetcode-cn.com/problems/convert-a-number-to-hexadecimal/description/)
+[Leetcode](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/) / [力扣](https://leetcode.cn/problems/convert-a-number-to-hexadecimal/description/)
```text
Input:
@@ -202,7 +202,7 @@ public String toHex(int num) {
168\. Excel Sheet Column Title (Easy)
-[Leetcode](https://leetcode.com/problems/excel-sheet-column-title/description/) / [力扣](https://leetcode-cn.com/problems/excel-sheet-column-title/description/)
+[Leetcode](https://leetcode.com/problems/excel-sheet-column-title/description/) / [力扣](https://leetcode.cn/problems/excel-sheet-column-title/description/)
```text
1 -> A
@@ -232,7 +232,7 @@ public String convertToTitle(int n) {
172\. Factorial Trailing Zeroes (Medium)
-[Leetcode](https://leetcode.com/problems/factorial-trailing-zeroes/description/) / [力扣](https://leetcode-cn.com/problems/factorial-trailing-zeroes/description/)
+[Leetcode](https://leetcode.com/problems/factorial-trailing-zeroes/description/) / [力扣](https://leetcode.cn/problems/factorial-trailing-zeroes/description/)
尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。
@@ -252,7 +252,7 @@ public int trailingZeroes(int n) {
67\. Add Binary (Easy)
-[Leetcode](https://leetcode.com/problems/add-binary/description/) / [力扣](https://leetcode-cn.com/problems/add-binary/description/)
+[Leetcode](https://leetcode.com/problems/add-binary/description/) / [力扣](https://leetcode.cn/problems/add-binary/description/)
```text
a = "11"
@@ -282,7 +282,7 @@ public String addBinary(String a, String b) {
415\. Add Strings (Easy)
-[Leetcode](https://leetcode.com/problems/add-strings/description/) / [力扣](https://leetcode-cn.com/problems/add-strings/description/)
+[Leetcode](https://leetcode.com/problems/add-strings/description/) / [力扣](https://leetcode.cn/problems/add-strings/description/)
字符串的值为非负整数。
@@ -306,7 +306,7 @@ public String addStrings(String num1, String num2) {
462\. Minimum Moves to Equal Array Elements II (Medium)
-[Leetcode](https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/description/) / [力扣](https://leetcode-cn.com/problems/minimum-moves-to-equal-array-elements-ii/description/)
+[Leetcode](https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/description/) / [力扣](https://leetcode.cn/problems/minimum-moves-to-equal-array-elements-ii/description/)
```text
Input:
@@ -404,7 +404,7 @@ private void swap(int[] nums, int i, int j) {
169\. Majority Element (Easy)
-[Leetcode](https://leetcode.com/problems/majority-element/description/) / [力扣](https://leetcode-cn.com/problems/majority-element/description/)
+[Leetcode](https://leetcode.com/problems/majority-element/description/) / [力扣](https://leetcode.cn/problems/majority-element/description/)
先对数组排序,最中间那个数出现次数一定多于 n / 2。
@@ -438,7 +438,7 @@ public int majorityElement(int[] nums) {
367\. Valid Perfect Square (Easy)
-[Leetcode](https://leetcode.com/problems/valid-perfect-square/description/) / [力扣](https://leetcode-cn.com/problems/valid-perfect-square/description/)
+[Leetcode](https://leetcode.com/problems/valid-perfect-square/description/) / [力扣](https://leetcode.cn/problems/valid-perfect-square/description/)
```text
Input: 16
@@ -466,7 +466,7 @@ public boolean isPerfectSquare(int num) {
326\. Power of Three (Easy)
-[Leetcode](https://leetcode.com/problems/power-of-three/description/) / [力扣](https://leetcode-cn.com/problems/power-of-three/description/)
+[Leetcode](https://leetcode.com/problems/power-of-three/description/) / [力扣](https://leetcode.cn/problems/power-of-three/description/)
1162261467 是 int 范围内最大的 3 的幂(319)。3 是素数,319 的正约数只有 30 \~ 319,因此能整除它的正整数一定是 3 的幂。
@@ -480,7 +480,7 @@ public boolean isPowerOfThree(int n) {
238\. Product of Array Except Self (Medium)
-[Leetcode](https://leetcode.com/problems/product-of-array-except-self/description/) / [力扣](https://leetcode-cn.com/problems/product-of-array-except-self/description/)
+[Leetcode](https://leetcode.com/problems/product-of-array-except-self/description/) / [力扣](https://leetcode.cn/problems/product-of-array-except-self/description/)
```text
For example, given [1,2,3,4], return [24,12,8,6].
@@ -522,7 +522,7 @@ public int[] productExceptSelf(int[] nums) {
628\. Maximum Product of Three Numbers (Easy)
-[Leetcode](https://leetcode.com/problems/maximum-product-of-three-numbers/description/) / [力扣](https://leetcode-cn.com/problems/maximum-product-of-three-numbers/description/)
+[Leetcode](https://leetcode.com/problems/maximum-product-of-three-numbers/description/) / [力扣](https://leetcode.cn/problems/maximum-product-of-three-numbers/description/)
```text
Input: [1,2,3,4]
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\347\273\204\344\270\216\347\237\251\351\230\265.md" "b/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\347\273\204\344\270\216\347\237\251\351\230\265.md"
index 669830fb9..6bfb65b06 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\347\273\204\344\270\216\347\237\251\351\230\265.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \346\225\260\347\273\204\344\270\216\347\237\251\351\230\265.md"
@@ -23,7 +23,7 @@
283\. Move Zeroes (Easy)
-[Leetcode](https://leetcode.com/problems/move-zeroes/description/) / [力扣](https://leetcode-cn.com/problems/move-zeroes/description/)
+[Leetcode](https://leetcode.com/problems/move-zeroes/description/) / [力扣](https://leetcode.cn/problems/move-zeroes/description/)
```text
For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].
@@ -47,7 +47,7 @@ public void moveZeroes(int[] nums) {
566\. Reshape the Matrix (Easy)
-[Leetcode](https://leetcode.com/problems/reshape-the-matrix/description/) / [力扣](https://leetcode-cn.com/problems/reshape-the-matrix/description/)
+[Leetcode](https://leetcode.com/problems/reshape-the-matrix/description/) / [力扣](https://leetcode.cn/problems/reshape-the-matrix/description/)
```text
Input:
@@ -85,7 +85,7 @@ public int[][] matrixReshape(int[][] nums, int r, int c) {
485\. Max Consecutive Ones (Easy)
-[Leetcode](https://leetcode.com/problems/max-consecutive-ones/description/) / [力扣](https://leetcode-cn.com/problems/max-consecutive-ones/description/)
+[Leetcode](https://leetcode.com/problems/max-consecutive-ones/description/) / [力扣](https://leetcode.cn/problems/max-consecutive-ones/description/)
```java
public int findMaxConsecutiveOnes(int[] nums) {
@@ -102,7 +102,7 @@ public int findMaxConsecutiveOnes(int[] nums) {
240\. Search a 2D Matrix II (Medium)
-[Leetcode](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) / [力扣](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/description/)
+[Leetcode](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) / [力扣](https://leetcode.cn/problems/search-a-2d-matrix-ii/description/)
```text
Input:
@@ -141,7 +141,7 @@ public boolean searchMatrix(int[][] matrix, int target) {
378\. Kth Smallest Element in a Sorted Matrix (Medium)
-[Leetcode](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) / [力扣](https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/description/)
+[Leetcode](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) / [力扣](https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix/description/)
```text
matrix = [
@@ -154,7 +154,7 @@ k = 8,
return 13.
```
-解题参考:[Share my thoughts and Clean Java Code](https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/discuss/85173)
+解题参考:[Share my thoughts and Clean Java Code](https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix/discuss/85173)
二分查找解法:在 [matrix[0][0], matrix[m - 1][n - 1]] 这个值域上二分,统计矩阵中不大于 mid 的元素个数 cnt。cnt < k 说明第 k 小的数比 mid 大,否则不大于 mid。循环结束时 lo 是使 cnt >= k 的最小值,这个值一定是矩阵中的元素。
@@ -209,7 +209,7 @@ class Tuple implements Comparable {
645\. Set Mismatch (Easy)
-[Leetcode](https://leetcode.com/problems/set-mismatch/description/) / [力扣](https://leetcode-cn.com/problems/set-mismatch/description/)
+[Leetcode](https://leetcode.com/problems/set-mismatch/description/) / [力扣](https://leetcode.cn/problems/set-mismatch/description/)
```text
Input: nums = [1,2,2,4]
@@ -246,7 +246,7 @@ private void swap(int[] nums, int i, int j) {
287\. Find the Duplicate Number (Medium)
-[Leetcode](https://leetcode.com/problems/find-the-duplicate-number/description/) / [力扣](https://leetcode-cn.com/problems/find-the-duplicate-number/description/)
+[Leetcode](https://leetcode.com/problems/find-the-duplicate-number/description/) / [力扣](https://leetcode.cn/problems/find-the-duplicate-number/description/)
要求不能修改数组,也不能使用额外的空间。
@@ -292,7 +292,7 @@ public int findDuplicate(int[] nums) {
667\. Beautiful Arrangement II (Medium)
-[Leetcode](https://leetcode.com/problems/beautiful-arrangement-ii/description/) / [力扣](https://leetcode-cn.com/problems/beautiful-arrangement-ii/description/)
+[Leetcode](https://leetcode.com/problems/beautiful-arrangement-ii/description/) / [力扣](https://leetcode.cn/problems/beautiful-arrangement-ii/description/)
```text
Input: n = 3, k = 2
@@ -322,7 +322,7 @@ public int[] constructArray(int n, int k) {
697\. Degree of an Array (Easy)
-[Leetcode](https://leetcode.com/problems/degree-of-an-array/description/) / [力扣](https://leetcode-cn.com/problems/degree-of-an-array/description/)
+[Leetcode](https://leetcode.com/problems/degree-of-an-array/description/) / [力扣](https://leetcode.cn/problems/degree-of-an-array/description/)
```text
Input: [1,2,2,3,1,4,2]
@@ -365,7 +365,7 @@ public int findShortestSubArray(int[] nums) {
766\. Toeplitz Matrix (Easy)
-[Leetcode](https://leetcode.com/problems/toeplitz-matrix/description/) / [力扣](https://leetcode-cn.com/problems/toeplitz-matrix/description/)
+[Leetcode](https://leetcode.com/problems/toeplitz-matrix/description/) / [力扣](https://leetcode.cn/problems/toeplitz-matrix/description/)
```text
1234
@@ -405,7 +405,7 @@ private boolean check(int[][] matrix, int expectValue, int row, int col) {
565\. Array Nesting (Medium)
-[Leetcode](https://leetcode.com/problems/array-nesting/description/) / [力扣](https://leetcode-cn.com/problems/array-nesting/description/)
+[Leetcode](https://leetcode.com/problems/array-nesting/description/) / [力扣](https://leetcode.cn/problems/array-nesting/description/)
```text
Input: A = [5,4,0,3,1,6,2]
@@ -443,7 +443,7 @@ public int arrayNesting(int[] nums) {
769\. Max Chunks To Make Sorted (Medium)
-[Leetcode](https://leetcode.com/problems/max-chunks-to-make-sorted/description/) / [力扣](https://leetcode-cn.com/problems/max-chunks-to-make-sorted/description/)
+[Leetcode](https://leetcode.com/problems/max-chunks-to-make-sorted/description/) / [力扣](https://leetcode.cn/problems/max-chunks-to-make-sorted/description/)
```text
Input: arr = [1,0,2,3,4]
@@ -499,6 +499,8 @@ public int[][] merge(int[][] intervals) {
}
```
+时间复杂度为 O(NlogN),主要花在排序上,排序后只扫描一遍;除结果外,对象数组排序使用的 TimSort 最多需要 O(N) 的辅助空间。
+
## 14. 下一个排列
31\. Next Permutation (Medium)
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \346\240\210\345\222\214\351\230\237\345\210\227.md" "b/notes/Leetcode \351\242\230\350\247\243 - \346\240\210\345\222\214\351\230\237\345\210\227.md"
index 24fe90add..1cd6e6ee3 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \346\240\210\345\222\214\351\230\237\345\210\227.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \346\240\210\345\222\214\351\230\237\345\210\227.md"
@@ -17,7 +17,7 @@
232\. Implement Queue using Stacks (Easy)
-[Leetcode](https://leetcode.com/problems/implement-queue-using-stacks/description/) / [力扣](https://leetcode-cn.com/problems/implement-queue-using-stacks/description/)
+[Leetcode](https://leetcode.com/problems/implement-queue-using-stacks/description/) / [力扣](https://leetcode.cn/problems/implement-queue-using-stacks/description/)
栈的顺序为后进先出,而队列的顺序为先进先出。使用两个栈实现队列,一个元素需要经过两个栈才能出队列,在经过第一个栈时元素顺序被反转,经过第二个栈时再次被反转,此时就是先进先出顺序。
@@ -61,7 +61,7 @@ class MyQueue {
225\. Implement Stack using Queues (Easy)
-[Leetcode](https://leetcode.com/problems/implement-stack-using-queues/description/) / [力扣](https://leetcode-cn.com/problems/implement-stack-using-queues/description/)
+[Leetcode](https://leetcode.com/problems/implement-stack-using-queues/description/) / [力扣](https://leetcode.cn/problems/implement-stack-using-queues/description/)
在将一个元素 x 插入队列时,为了维护原来的后进先出顺序,需要让 x 插入队列首部。而队列的默认插入顺序是队列尾部,因此在将 x 插入队列尾部之后,需要让除了 x 之外的所有元素出队列,再入队列。
@@ -100,7 +100,7 @@ class MyStack {
155\. Min Stack (Medium)
-[Leetcode](https://leetcode.com/problems/min-stack/description/) / [力扣](https://leetcode-cn.com/problems/min-stack/description/)
+[Leetcode](https://leetcode.com/problems/min-stack/description/) / [力扣](https://leetcode.cn/problems/min-stack/description/)
用一个辅助栈 minStack 与数据栈同步压入和弹出,minStack 的每个位置保存数据栈从栈底到该位置的最小值。例如依次压入 3、5、2,minStack 为 3、3、2;弹出 2 之后,栈顶的 3 就是剩下元素的最小值。
@@ -145,7 +145,7 @@ class MinStack {
20\. Valid Parentheses (Easy)
-[Leetcode](https://leetcode.com/problems/valid-parentheses/description/) / [力扣](https://leetcode-cn.com/problems/valid-parentheses/description/)
+[Leetcode](https://leetcode.com/problems/valid-parentheses/description/) / [力扣](https://leetcode.cn/problems/valid-parentheses/description/)
```text
"()[]{}"
@@ -178,11 +178,13 @@ public boolean isValid(String s) {
}
```
+每个字符最多入栈、出栈一次,时间复杂度 O(N);全是左括号时栈中有 N 个元素,空间复杂度 O(N)。
+
## 5. 数组中元素与下一个比它大的元素之间的距离
739\. Daily Temperatures (Medium)
-[Leetcode](https://leetcode.com/problems/daily-temperatures/description/) / [力扣](https://leetcode-cn.com/problems/daily-temperatures/description/)
+[Leetcode](https://leetcode.com/problems/daily-temperatures/description/) / [力扣](https://leetcode.cn/problems/daily-temperatures/description/)
```text
Input: [73, 74, 75, 71, 69, 72, 76, 73]
@@ -215,7 +217,7 @@ public int[] dailyTemperatures(int[] temperatures) {
503\. Next Greater Element II (Medium)
-[Leetcode](https://leetcode.com/problems/next-greater-element-ii/description/) / [力扣](https://leetcode-cn.com/problems/next-greater-element-ii/description/)
+[Leetcode](https://leetcode.com/problems/next-greater-element-ii/description/) / [力扣](https://leetcode.cn/problems/next-greater-element-ii/description/)
```text
Input: [1,2,1]
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \346\240\221.md" "b/notes/Leetcode \351\242\230\350\247\243 - \346\240\221.md"
index 8c7a67330..0adea0881 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \346\240\221.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \346\240\221.md"
@@ -16,6 +16,7 @@
* [12. 相同节点值的最大路径长度](#12-相同节点值的最大路径长度)
* [13. 间隔遍历](#13-间隔遍历)
* [14. 找出二叉树中第二小的节点](#14-找出二叉树中第二小的节点)
+ * [15. 二叉树中的最大路径和](#15-二叉树中的最大路径和)
* [层次遍历](#层次遍历)
* [1. 一棵树每层节点的平均数](#1-一棵树每层节点的平均数)
* [2. 得到左下角的节点](#2-得到左下角的节点)
@@ -49,7 +50,7 @@
104\. Maximum Depth of Binary Tree (Easy)
-[Leetcode](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/) / [力扣](https://leetcode.cn/problems/maximum-depth-of-binary-tree/description/)
```java
public int maxDepth(TreeNode root) {
@@ -62,7 +63,7 @@ public int maxDepth(TreeNode root) {
110\. Balanced Binary Tree (Easy)
-[Leetcode](https://leetcode.com/problems/balanced-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/balanced-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/balanced-binary-tree/description/) / [力扣](https://leetcode.cn/problems/balanced-binary-tree/description/)
```text
3
@@ -95,7 +96,7 @@ public int maxDepth(TreeNode root) {
543\. Diameter of Binary Tree (Easy)
-[Leetcode](https://leetcode.com/problems/diameter-of-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/diameter-of-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/diameter-of-binary-tree/description/) / [力扣](https://leetcode.cn/problems/diameter-of-binary-tree/description/)
```text
Input:
@@ -132,7 +133,7 @@ private int depth(TreeNode root) {
226\. Invert Binary Tree (Easy)
-[Leetcode](https://leetcode.com/problems/invert-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/invert-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/invert-binary-tree/description/) / [力扣](https://leetcode.cn/problems/invert-binary-tree/description/)
```java
public TreeNode invertTree(TreeNode root) {
@@ -148,7 +149,7 @@ public TreeNode invertTree(TreeNode root) {
617\. Merge Two Binary Trees (Easy)
-[Leetcode](https://leetcode.com/problems/merge-two-binary-trees/description/) / [力扣](https://leetcode-cn.com/problems/merge-two-binary-trees/description/)
+[Leetcode](https://leetcode.com/problems/merge-two-binary-trees/description/) / [力扣](https://leetcode.cn/problems/merge-two-binary-trees/description/)
```text
Input:
@@ -183,7 +184,7 @@ public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
112\. Path Sum (Easy)
-[Leetcode](https://leetcode.com/problems/path-sum/description/) / [力扣](https://leetcode-cn.com/problems/path-sum/description/)
+[Leetcode](https://leetcode.com/problems/path-sum/description/) / [力扣](https://leetcode.cn/problems/path-sum/description/)
```text
Given the below binary tree and sum = 22,
@@ -213,7 +214,7 @@ public boolean hasPathSum(TreeNode root, int sum) {
437\. Path Sum III (Medium)
-[Leetcode](https://leetcode.com/problems/path-sum-iii/description/) / [力扣](https://leetcode-cn.com/problems/path-sum-iii/description/)
+[Leetcode](https://leetcode.com/problems/path-sum-iii/description/) / [力扣](https://leetcode.cn/problems/path-sum-iii/description/)
```text
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
@@ -259,7 +260,7 @@ private int pathSumStartWithRoot(TreeNode root, long sum) {
572\. Subtree of Another Tree (Easy)
-[Leetcode](https://leetcode.com/problems/subtree-of-another-tree/description/) / [力扣](https://leetcode-cn.com/problems/subtree-of-another-tree/description/)
+[Leetcode](https://leetcode.com/problems/subtree-of-another-tree/description/) / [力扣](https://leetcode.cn/problems/subtree-of-another-tree/description/)
```text
Given tree s:
@@ -312,7 +313,7 @@ private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) {
101\. Symmetric Tree (Easy)
-[Leetcode](https://leetcode.com/problems/symmetric-tree/description/) / [力扣](https://leetcode-cn.com/problems/symmetric-tree/description/)
+[Leetcode](https://leetcode.com/problems/symmetric-tree/description/) / [力扣](https://leetcode.cn/problems/symmetric-tree/description/)
```text
1
@@ -340,7 +341,7 @@ private boolean isSymmetric(TreeNode t1, TreeNode t2) {
111\. Minimum Depth of Binary Tree (Easy)
-[Leetcode](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/) / [力扣](https://leetcode.cn/problems/minimum-depth-of-binary-tree/description/)
树的根节点到叶子节点的最小路径长度。
@@ -360,7 +361,7 @@ public int minDepth(TreeNode root) {
404\. Sum of Left Leaves (Easy)
-[Leetcode](https://leetcode.com/problems/sum-of-left-leaves/description/) / [力扣](https://leetcode-cn.com/problems/sum-of-left-leaves/description/)
+[Leetcode](https://leetcode.com/problems/sum-of-left-leaves/description/) / [力扣](https://leetcode.cn/problems/sum-of-left-leaves/description/)
```text
3
@@ -389,7 +390,7 @@ private boolean isLeaf(TreeNode node){
687\. Longest Univalue Path (Medium)
-[Leetcode](https://leetcode.com/problems/longest-univalue-path/) / [力扣](https://leetcode-cn.com/problems/longest-univalue-path/)
+[Leetcode](https://leetcode.com/problems/longest-univalue-path/) / [力扣](https://leetcode.cn/problems/longest-univalue-path/)
```text
1
@@ -426,7 +427,7 @@ private int dfs(TreeNode root){
337\. House Robber III (Medium)
-[Leetcode](https://leetcode.com/problems/house-robber-iii/description/) / [力扣](https://leetcode-cn.com/problems/house-robber-iii/description/)
+[Leetcode](https://leetcode.com/problems/house-robber-iii/description/) / [力扣](https://leetcode.cn/problems/house-robber-iii/description/)
```text
3
@@ -459,7 +460,7 @@ public int rob(TreeNode root) {
671\. Second Minimum Node In a Binary Tree (Easy)
-[Leetcode](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/second-minimum-node-in-a-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/description/) / [力扣](https://leetcode.cn/problems/second-minimum-node-in-a-binary-tree/description/)
```text
Input:
@@ -490,6 +491,50 @@ public int findSecondMinimumValue(TreeNode root) {
}
```
+### 15. 二叉树中的最大路径和
+
+124\. Binary Tree Maximum Path Sum (Hard)
+
+[Leetcode](https://leetcode.com/problems/binary-tree-maximum-path-sum/) / [力扣](https://leetcode.cn/problems/binary-tree-maximum-path-sum/)
+
+```text
+Input:
+ 2
+ / \
+ -6 3
+ / \ / \
+ 3 1 4 6
+
+Output: 13,路径为 4 -> 3 -> 6
+```
+
+做法与 [两节点的最长路径](#3-两节点的最长路径) 相同:以每个节点为最高点算一次经过它的最大路径,取最大值。不同的有两处:
+
+- 子树的贡献是负数时不如不走,记为 0,例如 -6 这棵子树向上最多贡献 -6 + 3 = -3,所以根节点不接它;
+- 递归返回的只能是向下走一侧的最大和,不能是两侧之和。3 这个节点两侧都接上是 4 + 3 + 6 = 13,但返回给父节点的是 3 + 6 = 9,否则路径会在 3 处分叉。
+
+节点值可能全是负数,max 的初值取 Integer.MIN_VALUE,不能取 0。
+
+```java
+private int max = Integer.MIN_VALUE;
+
+public int maxPathSum(TreeNode root) {
+ gain(root);
+ return max;
+}
+
+// 以 root 为起点向下走一侧的最大路径和
+private int gain(TreeNode root) {
+ if (root == null) return 0;
+ int left = Math.max(gain(root.left), 0);
+ int right = Math.max(gain(root.right), 0);
+ max = Math.max(max, root.val + left + right);
+ return root.val + Math.max(left, right);
+}
+```
+
+每个节点访问一次,时间复杂度 O(N);递归深度为树的高度,空间复杂度 O(H)。
+
## 层次遍历
使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。按层输出(102 题)和之字形输出的写法见[剑指 Offer 32.2](32.2%20把二叉树打印成多行.md)、[32.3](32.3%20按之字形顺序打印二叉树.md)。
@@ -498,7 +543,7 @@ public int findSecondMinimumValue(TreeNode root) {
637\. Average of Levels in Binary Tree (Easy)
-[Leetcode](https://leetcode.com/problems/average-of-levels-in-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/average-of-levels-in-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/average-of-levels-in-binary-tree/description/) / [力扣](https://leetcode.cn/problems/average-of-levels-in-binary-tree/description/)
```java
public List averageOfLevels(TreeNode root) {
@@ -525,7 +570,7 @@ public List averageOfLevels(TreeNode root) {
513\. Find Bottom Left Tree Value (Medium)
-[Leetcode](https://leetcode.com/problems/find-bottom-left-tree-value/description/) / [力扣](https://leetcode-cn.com/problems/find-bottom-left-tree-value/description/)
+[Leetcode](https://leetcode.com/problems/find-bottom-left-tree-value/description/) / [力扣](https://leetcode.cn/problems/find-bottom-left-tree-value/description/)
```text
Input:
@@ -613,7 +658,7 @@ void dfs(TreeNode root) {
144\. Binary Tree Preorder Traversal (Easy)
-[Leetcode](https://leetcode.com/problems/binary-tree-preorder-traversal/description/) / [力扣](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/description/)
+[Leetcode](https://leetcode.com/problems/binary-tree-preorder-traversal/description/) / [力扣](https://leetcode.cn/problems/binary-tree-preorder-traversal/description/)
```java
public List preorderTraversal(TreeNode root) {
@@ -635,7 +680,7 @@ public List preorderTraversal(TreeNode root) {
145\. Binary Tree Postorder Traversal (Easy)
-[Leetcode](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) / [力扣](https://leetcode-cn.com/problems/binary-tree-postorder-traversal/description/)
+[Leetcode](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) / [力扣](https://leetcode.cn/problems/binary-tree-postorder-traversal/description/)
前序遍历为 root -\> left -\> right,后序遍历为 left -\> right -\> root。可以修改前序遍历成为 root -\> right -\> left,那么这个顺序就和后序遍历正好相反。
@@ -687,7 +732,7 @@ public List postorderTraversal(TreeNode root) {
94\. Binary Tree Inorder Traversal (Easy)
-[Leetcode](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) / [力扣](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/description/)
+[Leetcode](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) / [力扣](https://leetcode.cn/problems/binary-tree-inorder-traversal/description/)
从 cur 一路向左压栈直到为空,弹出栈顶访问后转向它的右子树。以上面的树为例:
@@ -722,7 +767,7 @@ public List inorderTraversal(TreeNode root) {
669\. Trim a Binary Search Tree (Medium)
-[Leetcode](https://leetcode.com/problems/trim-a-binary-search-tree/description/) / [力扣](https://leetcode-cn.com/problems/trim-a-binary-search-tree/description/)
+[Leetcode](https://leetcode.com/problems/trim-a-binary-search-tree/description/) / [力扣](https://leetcode.cn/problems/trim-a-binary-search-tree/description/)
```text
Input:
@@ -766,7 +811,7 @@ public TreeNode trimBST(TreeNode root, int L, int R) {
230\. Kth Smallest Element in a BST (Medium)
-[Leetcode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) / [力扣](https://leetcode-cn.com/problems/kth-smallest-element-in-a-bst/description/)
+[Leetcode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) / [力扣](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/description/)
中序遍历解法:
@@ -812,7 +857,7 @@ private int count(TreeNode node) {
538\. Convert BST to Greater Tree (Medium)
-[Leetcode](https://leetcode.com/problems/convert-bst-to-greater-tree/description/) / [力扣](https://leetcode-cn.com/problems/convert-bst-to-greater-tree/description/)
+[Leetcode](https://leetcode.com/problems/convert-bst-to-greater-tree/description/) / [力扣](https://leetcode.cn/problems/convert-bst-to-greater-tree/description/)
```text
Input: The root of a Binary Search Tree like this:
@@ -851,7 +896,7 @@ private void traver(TreeNode node) {
235\. Lowest Common Ancestor of a Binary Search Tree (Medium)
-[Leetcode](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) / [力扣](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/)
+[Leetcode](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) / [力扣](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/description/)
```text
_______6______
@@ -879,7 +924,7 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
236\. Lowest Common Ancestor of a Binary Tree (Medium)
-[Leetcode](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/description/)
+[Leetcode](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) / [力扣](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/)
```text
_______3______
@@ -912,7 +957,7 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
108\. Convert Sorted Array to Binary Search Tree (Easy)
-[Leetcode](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) / [力扣](https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/description/)
+[Leetcode](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) / [力扣](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/description/)
取中间的元素作为根,两侧的元素个数最多相差 1,因此树是平衡的。
@@ -935,7 +980,7 @@ private TreeNode toBST(int[] nums, int sIdx, int eIdx){
109\. Convert Sorted List to Binary Search Tree (Medium)
-[Leetcode](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/) / [力扣](https://leetcode-cn.com/problems/convert-sorted-list-to-binary-search-tree/description/)
+[Leetcode](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/) / [力扣](https://leetcode.cn/problems/convert-sorted-list-to-binary-search-tree/description/)
```text
Given the sorted linked list: [-10,-3,0,5,9],
@@ -980,7 +1025,7 @@ private ListNode preMid(ListNode head) {
653\. Two Sum IV - Input is a BST (Easy)
-[Leetcode](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) / [力扣](https://leetcode-cn.com/problems/two-sum-iv-input-is-a-bst/description/)
+[Leetcode](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) / [力扣](https://leetcode.cn/problems/two-sum-iv-input-is-a-bst/description/)
```text
Input:
@@ -1026,7 +1071,7 @@ private void inOrder(TreeNode root, List nums) {
530\. Minimum Absolute Difference in BST (Easy)
-[Leetcode](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) / [力扣](https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/description/)
+[Leetcode](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) / [力扣](https://leetcode.cn/problems/minimum-absolute-difference-in-bst/description/)
```text
Input:
@@ -1066,7 +1111,7 @@ private void inOrder(TreeNode node) {
501\. Find Mode in Binary Search Tree (Easy)
-[Leetcode](https://leetcode.com/problems/find-mode-in-binary-search-tree/description/) / [力扣](https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/description/)
+[Leetcode](https://leetcode.com/problems/find-mode-in-binary-search-tree/description/) / [力扣](https://leetcode.cn/problems/find-mode-in-binary-search-tree/description/)
```text
1
@@ -1127,7 +1172,7 @@ Trie,又称前缀树或字典树,用于判断字符串是否存在或者是
208\. Implement Trie (Prefix Tree) (Medium)
-[Leetcode](https://leetcode.com/problems/implement-trie-prefix-tree/description/) / [力扣](https://leetcode-cn.com/problems/implement-trie-prefix-tree/description/)
+[Leetcode](https://leetcode.com/problems/implement-trie-prefix-tree/description/) / [力扣](https://leetcode.cn/problems/implement-trie-prefix-tree/description/)
```java
class Trie {
@@ -1193,7 +1238,7 @@ class Trie {
677\. Map Sum Pairs (Medium)
-[Leetcode](https://leetcode.com/problems/map-sum-pairs/description/) / [力扣](https://leetcode-cn.com/problems/map-sum-pairs/description/)
+[Leetcode](https://leetcode.com/problems/map-sum-pairs/description/) / [力扣](https://leetcode.cn/problems/map-sum-pairs/description/)
```text
Input: insert("apple", 3), Output: Null
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \350\264\252\345\277\203\346\200\235\346\203\263.md" "b/notes/Leetcode \351\242\230\350\247\243 - \350\264\252\345\277\203\346\200\235\346\203\263.md"
index b407cd068..cd9279ad5 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \350\264\252\345\277\203\346\200\235\346\203\263.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \350\264\252\345\277\203\346\200\235\346\203\263.md"
@@ -22,7 +22,7 @@
455\. Assign Cookies (Easy)
-[Leetcode](https://leetcode.com/problems/assign-cookies/description/) / [力扣](https://leetcode-cn.com/problems/assign-cookies/description/)
+[Leetcode](https://leetcode.com/problems/assign-cookies/description/) / [力扣](https://leetcode.cn/problems/assign-cookies/description/)
```text
Input: grid[1,3], size[1,2,4]
@@ -63,7 +63,7 @@ public int findContentChildren(int[] grid, int[] size) {
435\. Non-overlapping Intervals (Medium)
-[Leetcode](https://leetcode.com/problems/non-overlapping-intervals/description/) / [力扣](https://leetcode-cn.com/problems/non-overlapping-intervals/description/)
+[Leetcode](https://leetcode.com/problems/non-overlapping-intervals/description/) / [力扣](https://leetcode.cn/problems/non-overlapping-intervals/description/)
```text
Input: [ [1,2], [1,2], [1,2] ]
@@ -114,7 +114,7 @@ public int eraseOverlapIntervals(int[][] intervals) {
452\. Minimum Number of Arrows to Burst Balloons (Medium)
-[Leetcode](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) / [力扣](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/description/)
+[Leetcode](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) / [力扣](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/description/)
```text
Input:
@@ -150,7 +150,7 @@ public int findMinArrowShots(int[][] points) {
406\. Queue Reconstruction by Height(Medium)
-[Leetcode](https://leetcode.com/problems/queue-reconstruction-by-height/description/) / [力扣](https://leetcode-cn.com/problems/queue-reconstruction-by-height/description/)
+[Leetcode](https://leetcode.com/problems/queue-reconstruction-by-height/description/) / [力扣](https://leetcode.cn/problems/queue-reconstruction-by-height/description/)
```text
Input:
@@ -197,7 +197,7 @@ public int[][] reconstructQueue(int[][] people) {
121\. Best Time to Buy and Sell Stock (Easy)
-[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) / [力扣](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/description/)
+[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) / [力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/)
题目描述:一次股票交易包含买入和卖出,只进行一次交易,求最大收益。
@@ -217,12 +217,14 @@ public int maxProfit(int[] prices) {
}
```
+只遍历一遍数组,时间复杂度 O(N),空间复杂度 O(1)。
+
## 6. 买卖股票的最大收益 II
122\. Best Time to Buy and Sell Stock II (Medium)
-[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) / [力扣](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/description/)
+[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) / [力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/)
题目描述:可以进行多次交易,多次交易之间不能交叉进行。
@@ -245,7 +247,7 @@ public int maxProfit(int[] prices) {
605\. Can Place Flowers (Easy)
-[Leetcode](https://leetcode.com/problems/can-place-flowers/description/) / [力扣](https://leetcode-cn.com/problems/can-place-flowers/description/)
+[Leetcode](https://leetcode.com/problems/can-place-flowers/description/) / [力扣](https://leetcode.cn/problems/can-place-flowers/description/)
```text
Input: flowerbed = [1,0,0,0,1], n = 1
@@ -279,7 +281,7 @@ public boolean canPlaceFlowers(int[] flowerbed, int n) {
392\. Is Subsequence (Easy)
-[Leetcode](https://leetcode.com/problems/is-subsequence/description/) / [力扣](https://leetcode-cn.com/problems/is-subsequence/description/)
+[Leetcode](https://leetcode.com/problems/is-subsequence/description/) / [力扣](https://leetcode.cn/problems/is-subsequence/description/)
```text
s = "abc", t = "ahbgdc"
@@ -305,7 +307,7 @@ public boolean isSubsequence(String s, String t) {
665\. Non-decreasing Array (Medium)
-[Leetcode](https://leetcode.com/problems/non-decreasing-array/description/) / [力扣](https://leetcode-cn.com/problems/non-decreasing-array/description/)
+[Leetcode](https://leetcode.com/problems/non-decreasing-array/description/) / [力扣](https://leetcode.cn/problems/non-decreasing-array/description/)
```text
Input: [4,2,3]
@@ -343,7 +345,7 @@ public boolean checkPossibility(int[] nums) {
53\. Maximum Subarray (Medium)
-[Leetcode](https://leetcode.com/problems/maximum-subarray/description/) / [力扣](https://leetcode-cn.com/problems/maximum-subarray/description/)
+[Leetcode](https://leetcode.com/problems/maximum-subarray/description/) / [力扣](https://leetcode.cn/problems/maximum-subarray/description/)
```text
For example, given the array [-2,1,-3,4,-1,2,1,-5,4],
@@ -367,11 +369,13 @@ public int maxSubArray(int[] nums) {
}
```
+只遍历一遍数组,时间复杂度 O(N);只保存 preSum 和 maxSum,空间复杂度 O(1)。
+
## 11. 分隔字符串使同种字符出现在一起
763\. Partition Labels (Medium)
-[Leetcode](https://leetcode.com/problems/partition-labels/description/) / [力扣](https://leetcode-cn.com/problems/partition-labels/description/)
+[Leetcode](https://leetcode.com/problems/partition-labels/description/) / [力扣](https://leetcode.cn/problems/partition-labels/description/)
```text
Input: S = "ababcbacadefegdehijhklij"
diff --git "a/notes/Leetcode \351\242\230\350\247\243 - \351\223\276\350\241\250.md" "b/notes/Leetcode \351\242\230\350\247\243 - \351\223\276\350\241\250.md"
index d8b44edfb..6943d2a4b 100644
--- "a/notes/Leetcode \351\242\230\350\247\243 - \351\223\276\350\241\250.md"
+++ "b/notes/Leetcode \351\242\230\350\247\243 - \351\223\276\350\241\250.md"
@@ -25,7 +25,7 @@
160\. Intersection of Two Linked Lists (Easy)
-[Leetcode](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) / [力扣](https://leetcode-cn.com/problems/intersection-of-two-linked-lists/description/)
+[Leetcode](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) / [力扣](https://leetcode.cn/problems/intersection-of-two-linked-lists/description/)
例如以下示例中 A 和 B 两个链表相交于 c1:
@@ -79,7 +79,7 @@ public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
206\. Reverse Linked List (Easy)
-[Leetcode](https://leetcode.com/problems/reverse-linked-list/description/) / [力扣](https://leetcode-cn.com/problems/reverse-linked-list/description/)
+[Leetcode](https://leetcode.com/problems/reverse-linked-list/description/) / [力扣](https://leetcode.cn/problems/reverse-linked-list/description/)
递归:先把 head.next 之后的部分反转,返回新的头节点;此时 next 是反转后那一段的尾节点,把 head 接在 next 后面,再断开 head 原来的 next 指针。例如 1→2→3,反转 2→3 得到 3→2 后,令 2.next = 1、1.next = null,得到 3→2→1。
@@ -115,7 +115,7 @@ public ListNode reverseList(ListNode head) {
21\. Merge Two Sorted Lists (Easy)
-[Leetcode](https://leetcode.com/problems/merge-two-sorted-lists/description/) / [力扣](https://leetcode-cn.com/problems/merge-two-sorted-lists/description/)
+[Leetcode](https://leetcode.com/problems/merge-two-sorted-lists/description/) / [力扣](https://leetcode.cn/problems/merge-two-sorted-lists/description/)
比较两个链表的头节点,较小的那个作为结果的头节点,它的 next 指向剩余部分归并的结果。递归最坏需要 O(m + n) 的栈空间(m、n 为两个链表的长度),链表很长时可以改用迭代:用虚拟头节点 dummy 和尾指针 tail,每次把较小的节点接到 tail 后面,代码见[链表排序](#14-链表排序)中的 merge()。
@@ -137,7 +137,7 @@ public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
83\. Remove Duplicates from Sorted List (Easy)
-[Leetcode](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) / [力扣](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/description/)
+[Leetcode](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) / [力扣](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/)
```text
Given 1->1->2, return 1->2.
@@ -158,7 +158,7 @@ public ListNode deleteDuplicates(ListNode head) {
19\. Remove Nth Node From End of List (Medium)
-[Leetcode](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) / [力扣](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/description/)
+[Leetcode](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) / [力扣](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/)
```text
Given linked list: 1->2->3->4->5, and n = 2.
@@ -188,7 +188,7 @@ public ListNode removeNthFromEnd(ListNode head, int n) {
24\. Swap Nodes in Pairs (Medium)
-[Leetcode](https://leetcode.com/problems/swap-nodes-in-pairs/description/) / [力扣](https://leetcode-cn.com/problems/swap-nodes-in-pairs/description/)
+[Leetcode](https://leetcode.com/problems/swap-nodes-in-pairs/description/) / [力扣](https://leetcode.cn/problems/swap-nodes-in-pairs/description/)
```text
Given 1->2->3->4, you should return the list as 2->1->4->3.
@@ -220,7 +220,7 @@ public ListNode swapPairs(ListNode head) {
445\. Add Two Numbers II (Medium)
-[Leetcode](https://leetcode.com/problems/add-two-numbers-ii/description/) / [力扣](https://leetcode-cn.com/problems/add-two-numbers-ii/description/)
+[Leetcode](https://leetcode.com/problems/add-two-numbers-ii/description/) / [力扣](https://leetcode.cn/problems/add-two-numbers-ii/description/)
```text
Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
@@ -263,7 +263,7 @@ private Stack buildStack(ListNode l) {
234\. Palindrome Linked List (Easy)
-[Leetcode](https://leetcode.com/problems/palindrome-linked-list/description/) / [力扣](https://leetcode-cn.com/problems/palindrome-linked-list/description/)
+[Leetcode](https://leetcode.com/problems/palindrome-linked-list/description/) / [力扣](https://leetcode.cn/problems/palindrome-linked-list/description/)
进阶要求:以 O(1) 的空间复杂度来求解。
@@ -314,7 +314,7 @@ private boolean isEqual(ListNode l1, ListNode l2) {
725\. Split Linked List in Parts (Medium)
-[Leetcode](https://leetcode.com/problems/split-linked-list-in-parts/description/) / [力扣](https://leetcode-cn.com/problems/split-linked-list-in-parts/description/)
+[Leetcode](https://leetcode.com/problems/split-linked-list-in-parts/description/) / [力扣](https://leetcode.cn/problems/split-linked-list-in-parts/description/)
```text
Input:
@@ -358,7 +358,7 @@ public ListNode[] splitListToParts(ListNode head, int k) {
328\. Odd Even Linked List (Medium)
-[Leetcode](https://leetcode.com/problems/odd-even-linked-list/description/) / [力扣](https://leetcode-cn.com/problems/odd-even-linked-list/description/)
+[Leetcode](https://leetcode.com/problems/odd-even-linked-list/description/) / [力扣](https://leetcode.cn/problems/odd-even-linked-list/description/)
```text
Example:
@@ -430,6 +430,8 @@ public ListNode reverseKGroup(ListNode head, int k) {
}
```
+每个节点在找尾节点时被访问一次、翻转时再被访问一次,时间复杂度 O(N);只用了几个指针,空间复杂度 O(1)。
+
## 12. 重排链表
143\. Reorder List (Medium)
@@ -479,6 +481,8 @@ private ListNode reverse(ListNode head) {
}
```
+找中点、反转后半段、交替连接各遍历一次链表,时间复杂度 O(N);只修改指针,空间复杂度 O(1)。
+
## 13. 合并 K 个有序链表
23\. Merge k Sorted Lists (Hard)
diff --git a/notes/Linux.md b/notes/Linux.md
index 18f41ef74..f0e430a02 100644
--- a/notes/Linux.md
+++ b/notes/Linux.md
@@ -1186,7 +1186,7 @@ $ top -d 2
$ netstat -anp | grep port
```
-netstat 的替代命令是 ss,用法见[端口与磁盘](#端口与磁盘)。
+netstat 的替代命令是 ss,用法见[端口与磁盘](#端口与磁盘)。Ubuntu 24.04 默认不安装 netstat 所在的 net-tools 包,需要时用 sudo apt install net-tools 安装。
#### 5. kill
diff --git a/notes/MySQL.md b/notes/MySQL.md
index 525c9d035..9e6bd3992 100644
--- a/notes/MySQL.md
+++ b/notes/MySQL.md
@@ -51,6 +51,7 @@
* [A11 redo log 的检查点](#a11-redo-log-的检查点)
* [A12 主从复制的初始化与延迟](#a12-主从复制的初始化与延迟)
* [A13 InnoDB 与 MyISAM 的并发读写](#a13-innodb-与-myisam-的并发读写)
+ * [A14 找到持有锁的源头事务](#a14-找到持有锁的源头事务)
* [参考资料](#参考资料)
@@ -150,7 +151,7 @@ MEMORY 引擎支持哈希索引,InnoDB 不能显式创建哈希索引,但有
- 联合索引:建立在多个列上的索引,见下文的 [多列索引](#2-多列索引);
- 前缀索引:只索引字符串的前一部分,见下文的 [前缀索引](#4-前缀索引)。
-唯一索引和普通索引的查询速度差别很小:等值查询时唯一索引找到一条就停止(type 为 const),普通索引还要往后多读一条(type 为 ref)。区别主要在写入:普通索引的页不在缓冲池中时,InnoDB 可以先把修改记在 change buffer 中,以后读入这一页时再合并;唯一索引插入前必须读出这一页检查是否重复,用不上 change buffer。MySQL 8.4 起 change buffer 默认关闭,这个差别也就没有了。
+唯一索引和普通索引的查询速度差别很小:等值查询时唯一索引找到一条就停止(type 为 const),普通索引还要往后多读一条(type 为 ref)。区别主要在写入:普通索引的页不在缓冲池中时,InnoDB 可以先把修改记在 change buffer 中,以后读入这一页时再合并;唯一索引插入前必须读出这一页检查是否重复,用不上 change buffer。MySQL 8.4 把 change buffer 默认关闭,这个差别也就没有了;9.5 起又默认开启(innodb_change_buffering 为 all),所以要看所用的版本和配置。
### 索引的优点
@@ -403,7 +404,7 @@ Count: 2 Time=0.70s (1s) Lock=0.00s (0s) Rows=0.5 (1), root[root]@localhost
- 走了索引但回表太多:改为覆盖索引,或者用 [深分页](#深分页) 中的延迟关联;
- 出现 Using filesort、Using temporary:让排序和分组的列利用索引的顺序,见 [索引列的顺序](#3-索引列的顺序)。filesort 在排序缓冲中排序,缓冲最大为 sort_buffer_size(默认 256 KB),放不下时分块排序后写入临时文件再归并,所以先让索引满足排序,再考虑调大这个参数;
- 连接查询中被驱动表的连接列没有索引:外层的每一行都要扫描一遍被驱动表,应在连接列上建索引;MySQL 8.0.18 起这种情况改用哈希连接,但仍要读完整张被驱动表;
-- Lock_time 大,或者同一条语句的耗时忽长忽短:通常是在等待行锁或元数据锁,见 [MySQL 中的锁](数据库系统原理.md#mysql-中的锁);
+- Lock_time 大,或者同一条语句的耗时忽长忽短:通常是在等待行锁或元数据锁,见 [MySQL 中的锁](数据库系统原理.md#mysql-中的锁)。行锁等待查询 sys.innodb_lock_waits(元数据锁等待查 performance_schema.metadata_locks),可以看到谁在等谁,等待的连接也可能挡住别人,只作为 blocking_pid 出现、自己不在等待的那个连接才是源头;它执行完语句却没有提交时,blocking_query 为 NULL,在 PROCESSLIST 中显示为 Sleep,要到应用中找到没有提交的事务,紧急时用 KILL 结束这个连接,它的事务会回滚(MySQL 8.0 实测,见[附录 A14](#a14-找到持有锁的源头事务));
- 统计信息不准导致选错了索引:执行 ANALYZE TABLE 更新统计信息,必要时用 FORCE INDEX 指定索引;
- 索引已经用对,但要读的行本身就很多(例如按月统计几千万条订单):定时汇总到统计表,或者缓存查询结果。
@@ -655,7 +656,7 @@ TIMESTAMP 比 DATETIME 少占用 1 字节,但是只能表示到 2038 年,需
订单按 user_id 分片后,按订单号或者按商家查询时不知道数据在哪个分片,只能查询所有分片再合并。常用的做法:
-- 基因法:生成订单号时把最后 4 个二进制位设为 user_id % 16,订单号 % 16 就等于 user_id % 16,按订单号也能算出所在的表;
+- 基因法:生成订单号时在末尾预留 4 个二进制位放 user_id % 16,订单号 % 16 就等于 user_id % 16,按订单号也能算出所在的表。需要注意的是,要从序号中让出这 4 位(例如雪花算法的 12 位序号缩为 8 位),直接覆盖原来的低 4 位会让同一毫秒内的订单号重复;
- 映射表:另存一张订单号到 user_id 的映射,先查映射再查订单;
- 按另一个维度再存一份:例如按商家 ID 分片再存一份订单,或者同步到 Elasticsearch,通常订阅 binlog 异步更新,所以会有短暂的延迟。
@@ -683,7 +684,7 @@ ShardingSphere 等中间件负责按分片键路由 SQL、改写分页条件和
连接由从服务器发起:I/O 线程告诉主服务器从 binlog 的哪个位置开始发送(使用全局事务 ID,即 GTID 时,告诉它已经执行过哪些事务),之后由 dump 线程主动推送新的 binlog,Canal 这类同步工具也是这样伪装成从库的。
-MySQL 8.0.27 起 replica_parallel_workers 默认为 4,从服务器上实际是一个协调线程读取中继日志,再把事务分给 4 个工作线程并行重放,以减小复制延迟。
+MySQL 8.0.27 起 replica_parallel_workers 默认为 4,从服务器上实际是一个协调线程读取中继日志,再把事务分给 4 个工作线程并行重放,以减小复制延迟。哪些事务可以并行,由主库写在 binlog 中的依赖信息决定:8.0 默认(binlog_transaction_dependency_tracking 为 COMMIT_ORDER)只让主库上同时处于提交阶段的事务并行,主库并发低、事务一个接一个提交时,从库上仍是串行重放;设为 WRITESET 后还按修改的行判断,能让更多互不冲突的事务并行(写集为空或不完整、修改没有主键和唯一键的表或外键中的父表时,仍按提交顺序判断),8.4 删除了这个参数,固定使用 WRITESET。
@@ -704,7 +705,7 @@ MySQL 8.0.27 起 replica_parallel_workers 默认为 4,从服务器上实际是
MySQL 默认是异步复制,主服务器提交事务后不等从服务器,从服务器的数据会落后一段时间,称为复制延迟。用户刚修改完资料就去从库读取,可能读到修改前的数据。
-延迟主要来自三方面:一是大事务,binlog 在提交时才整体写入,从库要等主库提交后才开始重放,例如主库上执行 32 秒的 100 万行 UPDATE,让紧随其后的小修改 1 分钟以上才在从库上可见(见[附录 A12](#a12-主从复制的初始化与延迟)),所以大批量修改要 [分批执行](#1-切分大查询);二是从库重放跟不上;三是网络慢。
+延迟主要来自三方面:一是大事务,binlog 在提交时才整体写入,从库要等主库提交后才开始重放,例如主库上执行 32 秒的 100 万行 UPDATE,让紧随其后的小修改 1 分钟以上才在从库上可见(见[附录 A12](#a12-主从复制的初始化与延迟)),所以大批量修改要 [分批执行](#1-切分大查询);二是从库重放跟不上,例如从库的配置或负载不如主库、按上面的依赖信息只能串行重放,或者表没有主键和唯一索引,ROW 格式下从库要扫描表才能找到被修改的行;三是网络慢。
排查时用 SHOW REPLICA STATUS 分清是接收慢还是重放慢:接收位置(Source_Log_File、Read_Source_Log_Pos)已追上主库而 Executed_Gtid_Set 落后于 Retrieved_Gtid_Set,是重放慢;接收位置落后,是网络或接收慢,这时 Seconds_Behind_Source 可能显示为 0。
@@ -716,7 +717,7 @@ MySQL 默认是异步复制,主服务器提交事务后不等从服务器,
使用缓存时,未命中的读请求还可能把从库中的旧值写回缓存,见 [缓存一致性](缓存.md#缓存一致性)。
-异步复制时主库宕机,已经提交、还没有发给从库的事务在切换后就丢失了。半同步复制让至少一个从库把 binlog 写入中继日志后,主库才返回提交成功,但并不保证从库已经执行完,读从库仍可能读到旧数据;等待超时后主库会退回异步复制。
+异步复制时主库宕机,已经提交、还没有发给从库的事务在切换后就丢失了。半同步复制让至少一个从库把 binlog 写入中继日志并刷盘后,主库才返回提交成功。默认的 AFTER_SYNC 在主库的存储引擎提交之前等待确认,等待期间主库上的其它会话也看不到这个事务;改为 AFTER_COMMIT 时先提交再等待,其它会话可能已经读到一个主库宕机后会丢失的事务。半同步复制并不保证从库已经执行完,读从库仍可能读到旧数据;等待超时后主库会退回异步复制。
## 七、日志 🔥
@@ -936,7 +937,7 @@ LIMIT 1000000, 10 1547.4 ms EXPLAIN: type=ALL, Extra=Using filesort
COUNT(*) 130.38 ms
```
-四条分页语句都按 (create_time, id) 排序(2026-10-30 重跑),后三条返回的 10 个 id 相同(1000000、1017679……1159111)。
+四条分页语句都按 (create_time, id) 排序,后三条返回的 10 个 id 相同(1000000、1017679……1159111)。
按 (create_time, id) 排序后第 1000000 行为 id = 982321、create_time = '2025-01-12 13:46:39',这就是游标分页示例中使用的游标。
@@ -1186,7 +1187,30 @@ CREATE TABLE t_InnoDB (id INT PRIMARY KEY, c INT, pad CHAR(100)) ENGINE=InnoDB;
CREATE TABLE t_MyISAM (id INT PRIMARY KEY, c INT, pad CHAR(100)) ENGINE=MyISAM;
```
-存储过程 p_InnoDB(pct)、p_MyISAM(pct) 每次调用执行 1000 条按主键随机访问的语句,其中 pct% 是 UPDATE ... SET c = c + 1 WHERE id = k(自动提交,innodb_flush_log_at_trx_commit 和 sync_binlog 都为 1),其余是 SELECT c ... WHERE id = k。用 mysqlslap 启动 16 个客户端,每个客户端调用 4 次,取 3 轮的平均耗时,前后跑了两遍:
+存储过程 p_InnoDB(pct)、p_MyISAM(pct) 每次调用执行 1000 条按主键随机访问的语句,其中 pct% 是 UPDATE ... SET c = c + 1 WHERE id = k(自动提交,innodb_flush_log_at_trx_commit 和 sync_binlog 都为 1),其余是 SELECT c ... WHERE id = k。用 mysqlslap 启动 16 个客户端,每个客户端调用 4 次,取 3 轮的平均耗时,前后跑了两遍。`--number-of-queries` 是所有客户端合计的调用次数,64 ÷ 16 = 4:
+
+```bash
+mysqlslap --create-schema=bench --concurrency=16 --iterations=3 --number-of-queries=64 --query="CALL p_InnoDB(50)"
+```
+
+表和存储过程都建在 bench 库中,存储过程的定义如下(p_MyISAM 相同,只把表名换成 t_MyISAM):
+
+```sql
+DELIMITER //
+CREATE PROCEDURE p_InnoDB(pct INT)
+BEGIN
+ DECLARE i INT DEFAULT 0; DECLARE k INT; DECLARE v INT;
+ WHILE i < 1000 DO
+ SET k = FLOOR(1 + RAND() * 100000);
+ IF RAND() * 100 < pct THEN UPDATE t_InnoDB SET c = c + 1 WHERE id = k;
+ ELSE SELECT c INTO v FROM t_InnoDB WHERE id = k; END IF;
+ SET i = i + 1;
+ END WHILE;
+END//
+DELIMITER ;
+```
+
+两遍的平均耗时:
```text
写比例 InnoDB MyISAM
@@ -1194,6 +1218,29 @@ CREATE TABLE t_MyISAM (id INT PRIMARY KEY, c INT, pad CHAR(100)) ENGINE=MyISAM;
50% 1.578 / 1.001 s 3.355 / 4.687 s
```
+### A14 找到持有锁的源头事务
+
+MySQL 8.0.44,Apple M1 Pro。连接 29077 执行 UPDATE account SET balance = balance - 1 WHERE id = 1 后不提交;连接 29078、29079 随后执行 UPDATE account SET balance = balance + 1 WHERE id = 1(应用一侧的线程栈见 [Java 虚拟机附录 A23](Java%20虚拟机.md#a23-等待连接池时的线程栈)):
+
+```sql
+SELECT waiting_pid, waiting_query, blocking_pid, blocking_query FROM sys.innodb_lock_waits;
+SELECT id, command, time, state, info FROM information_schema.processlist;
+```
+
+```text
+waiting_pid waiting_query blocking_pid blocking_query
+29078 UPDATE account SET balance = balance + 1 WHERE id = 1 29077 NULL
+29079 UPDATE account SET balance = balance + 1 WHERE id = 1 29077 NULL
+29079 UPDATE account SET balance = balance + 1 WHERE id = 1 29078 UPDATE account SET balance = balance + 1 WHERE id = 1
+
+id command time state info
+29077 Sleep 6 NULL
+29078 Query 6 updating UPDATE account SET balance = balance + 1 WHERE id = 1
+29079 Query 6 updating UPDATE account SET balance = balance + 1 WHERE id = 1
+```
+
+29079 同时被 29077 和排在它前面的 29078 挡住;只作为阻塞方出现的是 29077,它没有在执行语句,blocking_query 为 NULL。
+
## 参考资料
- [MySQL 8.0: InnoDB Restrictions and Limitations](https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions-limitations.html)
diff --git "a/notes/Redis - \350\277\233\351\230\266.md" "b/notes/Redis - \350\277\233\351\230\266.md"
index fd3e12a80..a1f0de367 100644
--- "a/notes/Redis - \350\277\233\351\230\266.md"
+++ "b/notes/Redis - \350\277\233\351\230\266.md"
@@ -123,6 +123,6 @@ SET 44 字节 encoding embstr,SET 45 字节 encoding raw
## 参考资料
-- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
+- 黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.
- [Redis 7.4.1 源码 dict.c](https://github.com/redis/redis/blob/7.4.1/src/dict.c)
- [Redis 7.4.1 源码 sds.c](https://github.com/redis/redis/blob/7.4.1/src/sds.c)
diff --git a/notes/Redis.md b/notes/Redis.md
index 5807861db..e1b7c6e00 100644
--- a/notes/Redis.md
+++ b/notes/Redis.md
@@ -314,7 +314,7 @@ HyperLogLog(HLL)用于估计一个集合中不同元素的个数,例如页
例如 100 万个不同的元素,PFCOUNT 返回 999674(误差 −0.03%),HLL 占 12 KB,同样的元素用 SET 保存要 30 MB;30 组各 100 万个元素的误差均方根为 0.80%,最大为 2.0%(Redis 7.4 实测,见[附录 A8](#a8-hyperloglog-的误差和内存))。
-GEO 用于“附近的门店”这类查询:GEOADD 保存经纬度,GEOSEARCH 查找某个点一定半径或矩形内的成员并按距离排序,GEODIST 计算两点间的距离。它底层就是一个 ZSET,经纬度交错编码成 52 位的 Geohash 整数作为分值,相邻的点分值相近,查询时只需查找覆盖查询范围的 9 个区域对应的分值区间;代价是距离和范围有误差,官方文档说明最坏可达 0.5%。
+GEO 用于“附近的门店”这类查询:GEOADD 保存经纬度,GEOSEARCH 查找某个点一定半径或矩形内的成员(加 ASC 时按距离由近到远排序),GEODIST 计算两点间的距离。它底层就是一个 ZSET,经纬度交错编码成 52 位的 Geohash 整数作为分值。同一个格子里的点分值落在一段连续的区间内,但格子边界两侧的点即使相距很近,分值也可能相差很远,所以查询时要查中心格和周围 8 个格子(最多 9 个分值区间),再逐个计算距离过滤;代价是有误差:距离按球面模型计算,官方文档说明由此产生的误差最坏可达 0.5%,经纬度编码成整数时也有精度损失。
## 三、使用场景 🔥
@@ -400,7 +400,7 @@ Set 可以实现交集、并集等操作,从而实现共同好友等功能。
转换后内存明显增加:512 个字段的 HASH 用 listpack 约占 10 KB,再写入一个字段转为 hashtable 后约占 40 KB(Redis 7.4 实测,见[附录 A14](#a14-hash-编码的转换))。
-Redis 7.0 之前用的是压缩列表(ziplist),每个元素要记录前一个元素的长度,这个长度占 1 或 5 个字节,在中间插入或删除元素时可能让后面的元素依次变长,称为连锁更新;listpack 的每个元素只记录自己的长度,避免了这个问题。
+Redis 7.0 之前,HASH、ZSET 和 quicklist 的节点用的是压缩列表(ziplist),每个元素要记录前一个元素的长度,这个长度占 1 或 5 个字节,在中间插入或删除元素时可能让后面的元素依次变长,称为连锁更新;listpack 的每个元素只记录自己的长度,不会连锁更新,但在中间插入时仍要移动后面的数据。小的 LIST 直接用 listpack、SET 的 listpack 编码是 7.2 起才有的。旧的配置名(例如 hash-max-ziplist-entries)在 7.x 中仍可作为新配置名的别名使用。
字符串的内容保存在简单动态字符串(Simple Dynamic String, SDS)中,它在字符数组前面记录了已用的长度 len 和分配的容量 alloc:
@@ -559,7 +559,7 @@ Redis 7.0 起 AOF 由多个文件组成:重写生成的基础文件,以及
-配置项 aof-use-rdb-preamble 默认开启,重写生成的基础文件使用 RDB 格式,称为混合持久化:重启时先载入 RDB 部分,再重放增量文件中的命令,兼顾了 RDB 恢复快和 AOF 丢失数据少的优点。两种持久化都开启时,重启后使用 AOF 恢复,因为它的数据通常更完整。
+配置项 aof-use-rdb-preamble 在 4.0 引入、5.0 起默认开启,重写生成的基础文件使用 RDB 格式,称为混合持久化:重启时先载入 RDB 部分,再重放增量文件中的命令,兼顾了 RDB 恢复快和 AOF 丢失数据少的优点。两种持久化都开启时,重启后使用 AOF 恢复,因为它的数据通常更完整。
两种方式的对比:
@@ -664,7 +664,7 @@ Redis 6.0 加入的 I/O 多线程(io-threads,默认关闭)分担的也是
从服务器第一次连接时进行全量复制:
-1. 从服务器发送 PSYNC,主服务器 fork 一个子进程生成 RDB 快照发给从服务器,并在这期间把新执行的写命令记在缓冲区中。Redis 7.0 起默认不写磁盘文件,子进程直接把快照通过网络发送(repl-diskless-sync yes);
+1. 从服务器发送 PSYNC,主服务器 fork 一个子进程生成 RDB 快照发给从服务器,并在这期间把新执行的写命令记在缓冲区中。Redis 7.0 起默认不写磁盘文件,子进程直接把快照通过网络发送(repl-diskless-sync yes)。Redis 8.0 起从服务器默认另开一条连接接收快照,期间新的写命令直接发给从服务器、由从服务器先缓存,载入快照后再执行,减轻了主服务器的内存压力;
2. 从服务器清空旧数据,载入快照,再执行缓冲区中的写命令;
3. 之后主服务器每执行一条写命令,就把它发给从服务器。
@@ -740,7 +740,7 @@ Redis Cluster 不需要哨兵,节点之间通过集群总线(端口号加 10
需要注意的是,主从之间是异步复制,主节点宕机前刚写入、还没有复制到从节点的数据会丢失,所以 Redis Cluster 不保证[强一致性](分布式.md#一致性)。网络分区时,少数派一侧的主节点超过 cluster-node-timeout 后拒绝写入,所以它在这之前接受的写入可能丢失;默认配置下读请求也会被拒绝(cluster-allow-reads-when-down 默认为 no)。
-扩容时要把一部分槽迁移到新节点。迁移以键为单位进行,期间一个槽的键一部分在源节点、一部分在目标节点,客户端靠 ASK 重定向找到它们。以把槽 5712 从 7002 迁移到 7003 为例(Redis 7.4 实测,见[附录 A13](#a13-槽迁移与-ask-重定向)):
+扩容时要把一部分槽迁移到新节点。迁移以键为单位进行,期间一个槽的键一部分在源节点、一部分在目标节点,客户端靠 ASK 重定向找到它们。Redis 8.4 起还可以用 CLUSTER MIGRATION 整槽迁移:先复制整个槽的快照和增量写入,追上后短暂暂停这些槽的写入,一次性切换归属,下面以键为单位的流程仍然可用。以把槽 5712 从 7002 迁移到 7003 为例(Redis 7.4 实测,见[附录 A13](#a13-槽迁移与-ask-重定向)):
- 先把 7003 上的槽 5712 标记为 IMPORTING、7002 上的标记为 MIGRATING,再用 MIGRATE 把键一批批搬到 7003,每个键在同一时刻只存在于其中一个节点;
- 客户端仍然先访问 7002:键还在 7002 就直接处理,例如 {user:1001}:cart 正常返回;键已经迁走或者不存在,返回 ASK 5712 127.0.0.1:7003;
@@ -784,7 +784,7 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据
## 附录:实测
-以下实验的环境均为 Apple M1 Pro(10 核)、macOS,客户端和服务器在同一台机器上,Redis 7.4.1 从源码编译,使用默认配置,不开启持久化。
+以下实验的环境均为 Apple M1 Pro(10 核)、macOS,客户端和服务器在同一台机器上,Redis 7.4.1 从源码编译,使用默认配置,不开启持久化。macOS 上编译默认使用 libc 的内存分配器(INFO 中 mem_allocator 为 libc),Linux 上默认是 jemalloc,内存占用的数字会有差别。
### A1 MySQL 与 Redis 的主键查询
@@ -1340,7 +1340,7 @@ maxmemory 默认 = 0
## 参考资料
- Carlson J L. Redis in Action [M]. Manning, 2013.
-- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
+- 黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.
- [REDIS IN ACTION](https://web.archive.org/web/20210421115011/https://redislabs.com/ebook/foreword/)
- [Skip Lists: Done Right](https://ticki.github.io/blog/skip-lists-done-right/)
- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
diff --git a/notes/Spring.md b/notes/Spring.md
index f1865965d..e48f82f04 100644
--- a/notes/Spring.md
+++ b/notes/Spring.md
@@ -187,6 +187,8 @@ public class CounterController {
另一种无法解决的情况与 @Async 有关。@Async 的代理由 AsyncAnnotationBeanPostProcessor 创建,它只在初始化之后生成代理,没有实现提前引用的 getEarlyBeanReference(),三级缓存中的工厂不会为它提前创建代理:B 拿到的是原始的 A,A 初始化完成后却被替换成了代理,B 中的引用与容器中的对象不一致。Spring 检查到这种情况时启动失败,提示 A 已经以原始版本注入到了 B 中、最终却被包装了;同样的结构把 @Async 换成 @Transactional 则可以正常启动,B 中注入的就是最终的代理(Spring Boot 3.3 实测,见[附录 A6](#a6-async-与-lazy-的循环依赖))。
+这是 Spring 6.1 的行为。在 Spring 6.2.19、7.0.10 上,允许循环依赖时同样的结构能够启动(见[附录 A6](#a6-async-与-lazy-的循环依赖)):6.2.3 起,容器在预先创建单例时遇到这个异常会跳过 A,之后从 B 开始重新创建。官方尚未确认这是否是预期行为,不能据此认为 @Async 支持循环依赖。Spring Boot 默认禁止循环依赖,所以在 Boot 中默认仍是启动失败。
+
构造器注入的循环依赖可以在其中一个参数上加 @Lazy 解开:Spring 先注入一个延迟解析的代理,第一次调用它的方法时才从容器中取出真正的 Bean。
需要注意的是,Spring Boot 2.6 起默认禁止循环依赖,出现时直接启动失败,设置 spring.main.allow-circular-references=true 才能恢复。循环依赖通常说明职责划分有问题,更好的做法是调整依赖关系,例如把互相调用的逻辑抽到新的 Bean C 中。
@@ -400,8 +402,8 @@ REQUIRED 的内层方法加入了外层的事务,用的是同一个连接。RE
传播行为指的是一个事务方法调用另一个事务方法时,事务如何传递。常用的有以下三种:
- REQUIRED:默认值,当前有事务就加入,没有就新建。内外两个方法属于同一个事务,一起提交或者一起回滚;
-- REQUIRES_NEW:挂起当前事务,新建一个独立的事务,两个事务分别提交和回滚,例如无论业务成功与否都要保存的操作日志;
-- NESTED:在当前事务中创建一个保存点,内层回滚时只回滚到保存点,外层回滚时内层也一起回滚。
+- REQUIRES_NEW:挂起当前事务,新建一个独立的事务,两个事务分别提交和回滚,例如无论业务成功与否都要保存的操作日志。需要注意的是,已有外层事务时,执行内层事务期间一个线程同时占用两个连接,并发高时所有线程可能都拿着外层的连接等内层的连接,直到获取连接超时;内层修改外层已加锁的行时,也会一直等到锁等待超时;
+- NESTED:在当前事务中创建一个保存点,内层回滚时只回滚到保存点,外层回滚时内层也一起回滚。保存点是 JDBC 连接的功能,只引入 JDBC 时 Spring Boot 默认的 JdbcTransactionManager 支持;引入 Spring Data JPA 后默认是 JpaTransactionManager,用 Hibernate 时在已有事务中使用 NESTED 会抛出 NestedTransactionNotSupportedException。没有当前事务时,NESTED 和 REQUIRED 一样新建事务。
其余四种用得较少:SUPPORTS 有事务就加入,没有就非事务执行;NOT_SUPPORTED 挂起事务后非事务执行;MANDATORY 没有事务就抛出异常;NEVER 有事务就抛出异常。
@@ -560,7 +562,7 @@ List byUserUnsafe(@Param("userId") String userId);
### Mapper 接口为什么不需要实现类
-MyBatis 在启动时为每个 Mapper 接口生成 JDK 动态代理,例如上面的 OrderMapper 注入的对象类型是 jdk.proxy2.$Proxy63,调用处理器是 MapperProxy。调用接口方法时,MapperProxy 按“接口全名 + 方法名”找到对应的 SQL,绑定参数后交给 SqlSession 执行,再把结果映射成返回类型。
+MyBatis 在启动时为每个 Mapper 接口注册一个代理工厂 MapperProxyFactory,获取 Mapper 时由它创建 JDK 动态代理;Spring 中由 MapperFactoryBean 在创建 Bean 时获取一次,注入的就是这个代理。例如上面的 OrderMapper 注入的对象类型是 jdk.proxy2.$Proxy63,调用处理器是 MapperProxy。调用接口方法时,MapperProxy 按“接口全名 + 方法名”找到对应的 SQL,绑定参数后交给 SqlSession 执行,再把结果映射成返回类型。
由于 SQL 只按方法名查找,Mapper 接口中的方法不能重载,两个同名方法都写了 SQL 时启动报错(见[附录 A12](#a12-mybatis-的结果映射重载与二级缓存))。
@@ -610,7 +612,7 @@ List byStatuses(@Param("userId") Integer userId, @Param("statuses") List<
### 缓存
- 一级缓存:默认开启,作用范围是一个 SqlSession,同一个会话中相同的查询只执行一次,执行增删改或提交后清空。与 Spring 集成时,不在事务中的每次调用都会使用新的 SqlSession,一级缓存基本不起作用;
-- 二级缓存:作用范围是一个 Mapper 的命名空间,多个会话共享,在 Mapper XML 中加 `` 或在接口上加 @CacheNamespace 开启,结果对象要实现 Serializable。查询结果在会话提交后才写入(只有查询的会话关闭时也会写入,回滚时丢弃),同一命名空间的增删改提交后清空。缓存在单个应用的内存中,多个实例之间不一致,实际项目中很少使用,通常用 Redis 等缓存代替。
+- 二级缓存:作用范围是一个 Mapper 的命名空间,多个会话共享,在 Mapper XML 中加 `` 或在接口上加 @CacheNamespace 开启,结果对象要实现 Serializable。查询结果在会话提交后才写入(只有查询的会话关闭时也会写入;执行过增删改的会话回滚时丢弃),同一命名空间的增删改提交后清空。缓存在单个应用的内存中,多个实例之间不一致,实际项目中很少使用,通常用 Redis 等缓存代替。
## 七、Spring 中的设计模式
@@ -677,15 +679,15 @@ Spring Boot 3.3.5(spring-boot-starter-aop、spring-boot-starter-data-redis)
```java
Integer n = jdbc.queryForObject(
- "SELECT COUNT(*) FROM t3_order WHERE user_id = ? AND sku_id = ?", Integer.class, userId, skuId);
+ "SELECT COUNT(*) FROM seckill_order WHERE user_id = ? AND sku_id = ?", Integer.class, userId, skuId);
if (n > 0) {
return false; // 一人一单
}
-jdbc.update("INSERT INTO t3_order (user_id, sku_id) VALUES (?, ?)", userId, skuId);
+jdbc.update("INSERT INTO seckill_order (user_id, sku_id) VALUES (?, ?)", userId, skuId);
return true;
```
-每轮先清空 t3_order,再用 20 个线程同时调用 createOrder(1024, 10086),统计返回 true 的次数和表中的行数。对照组加一个配置类 @EnableTransactionManagement(order = 0),让事务切面比锁的切面(@Order(1))更靠外:
+每轮先清空 seckill_order,再用 20 个线程同时调用 createOrder(1024, 10086),统计返回 true 的次数和表中的行数。对照组加一个配置类 @EnableTransactionManagement(order = 0),让事务切面比锁的切面(@Order(1))更靠外:
```text
锁在事务外(锁 @Order(1),事务切面为默认的 LOWEST_PRECEDENCE)
@@ -859,6 +861,8 @@ public static class CB { final CA a; CB(@Lazy CA a) { this.a = a; } }
通过它调用 b() 得到的 CB 就是容器中的 CB:true
```
+同样的 @Async 代码在 Spring 6.2.19、7.0.10 上启动成功,A、B 的构造顺序为 A、B、B、A,日志中有一条 Bean 'asyncA' marked for pre-instantiation (not lazy-init) but currently initialized by other thread - skipping it in mainline thread,B 中注入的 A 与容器中的是同一个代理对象。
+
### A7 Spring Boot 的启动过程
Spring Boot 3.3.5(spring-boot-starter-web)、JDK 21,Apple M1 Pro。application.properties 中设置 server.port=8097、关闭 Banner,日志级别为 WARN,只保留 Tomcat 的启动日志。在各个扩展点中打印距 JVM 加载启动类的毫秒数,用 java -jar 运行(代码有删减):
diff --git a/notes/pics/adapter-turkey.png b/notes/pics/adapter-turkey.png
index dcc5f8b86..83c63178b 100644
Binary files a/notes/pics/adapter-turkey.png and b/notes/pics/adapter-turkey.png differ
diff --git a/notes/pics/dns-resolve.png b/notes/pics/dns-resolve.png
index 73a1cc797..10bed2f66 100644
Binary files a/notes/pics/dns-resolve.png and b/notes/pics/dns-resolve.png differ
diff --git a/notes/pics/feed-push-pull.png b/notes/pics/feed-push-pull.png
index f6f75da76..5d89fb32b 100644
Binary files a/notes/pics/feed-push-pull.png and b/notes/pics/feed-push-pull.png differ
diff --git a/notes/pics/gc-cms.png b/notes/pics/gc-cms.png
index c2bc92bab..a7605a77c 100644
Binary files a/notes/pics/gc-cms.png and b/notes/pics/gc-cms.png differ
diff --git a/notes/pics/gc-collectors.png b/notes/pics/gc-collectors.png
index babd0517a..a87aabea8 100644
Binary files a/notes/pics/gc-collectors.png and b/notes/pics/gc-collectors.png differ
diff --git a/notes/pics/gc-parallel.png b/notes/pics/gc-parallel.png
index 08495ac3d..bbf4bb3b3 100644
Binary files a/notes/pics/gc-parallel.png and b/notes/pics/gc-parallel.png differ
diff --git a/notes/pics/gc-parnew.png b/notes/pics/gc-parnew.png
index 4b9412e87..ab6e4c08e 100644
Binary files a/notes/pics/gc-parnew.png and b/notes/pics/gc-parnew.png differ
diff --git a/notes/pics/gc-serial.png b/notes/pics/gc-serial.png
index 9add5ee9a..e1109df11 100644
Binary files a/notes/pics/gc-serial.png and b/notes/pics/gc-serial.png differ
diff --git a/notes/pics/harness-loop.png b/notes/pics/harness-loop.png
index ef8af9d54..2068d9bbe 100644
Binary files a/notes/pics/harness-loop.png and b/notes/pics/harness-loop.png differ
diff --git a/notes/pics/io-blocking.png b/notes/pics/io-blocking.png
index 54b063367..5956a4115 100644
Binary files a/notes/pics/io-blocking.png and b/notes/pics/io-blocking.png differ
diff --git a/notes/pics/io-nonblocking.png b/notes/pics/io-nonblocking.png
index e80926aec..aa5639272 100644
Binary files a/notes/pics/io-nonblocking.png and b/notes/pics/io-nonblocking.png differ
diff --git a/notes/pics/io-selector.png b/notes/pics/io-selector.png
index e0e8fe2f4..d894eaf8a 100644
Binary files a/notes/pics/io-selector.png and b/notes/pics/io-selector.png differ
diff --git a/notes/pics/jc-cpu-cache.png b/notes/pics/jc-cpu-cache.png
index ab1707b5d..c158cffda 100644
Binary files a/notes/pics/jc-cpu-cache.png and b/notes/pics/jc-cpu-cache.png differ
diff --git a/notes/pics/jc-volatile-barrier.png b/notes/pics/jc-volatile-barrier.png
index a379339b0..dc2389bb5 100644
Binary files a/notes/pics/jc-volatile-barrier.png and b/notes/pics/jc-volatile-barrier.png differ
diff --git a/notes/pics/jwt-refresh-timeline.png b/notes/pics/jwt-refresh-timeline.png
index 6b6c4ae1b..57803b2ec 100644
Binary files a/notes/pics/jwt-refresh-timeline.png and b/notes/pics/jwt-refresh-timeline.png differ
diff --git a/notes/pics/k8s-pod-create.png b/notes/pics/k8s-pod-create.png
index 332820823..7528e94cc 100644
Binary files a/notes/pics/k8s-pod-create.png and b/notes/pics/k8s-pod-create.png differ
diff --git a/notes/pics/k8s-traffic.png b/notes/pics/k8s-traffic.png
index 296437fe3..7d327ac89 100644
Binary files a/notes/pics/k8s-traffic.png and b/notes/pics/k8s-traffic.png differ
diff --git a/notes/pics/kafka-topic.png b/notes/pics/kafka-topic.png
index 127fa40b8..9c8de660b 100644
Binary files a/notes/pics/kafka-topic.png and b/notes/pics/kafka-topic.png differ
diff --git a/notes/pics/kv-cache.png b/notes/pics/kv-cache.png
index bad2456f9..2f048c414 100644
Binary files a/notes/pics/kv-cache.png and b/notes/pics/kv-cache.png differ
diff --git a/notes/pics/microservice-overview.png b/notes/pics/microservice-overview.png
index 25c525d4c..e1ba74915 100644
Binary files a/notes/pics/microservice-overview.png and b/notes/pics/microservice-overview.png differ
diff --git a/notes/pics/mysql-replication.png b/notes/pics/mysql-replication.png
index 218b47e27..a98592fb4 100644
Binary files a/notes/pics/mysql-replication.png and b/notes/pics/mysql-replication.png differ
diff --git a/notes/pics/process-states.png b/notes/pics/process-states.png
index 2e589e471..3aa847fae 100644
Binary files a/notes/pics/process-states.png and b/notes/pics/process-states.png differ
diff --git a/notes/pics/redis-cluster.png b/notes/pics/redis-cluster.png
index eba379fbb..03cfa6fd4 100644
Binary files a/notes/pics/redis-cluster.png and b/notes/pics/redis-cluster.png differ
diff --git a/notes/pics/redis-sentinel-failover.png b/notes/pics/redis-sentinel-failover.png
index 250604ebf..52cdbe56b 100644
Binary files a/notes/pics/redis-sentinel-failover.png and b/notes/pics/redis-sentinel-failover.png differ
diff --git a/notes/pics/snowflake.png b/notes/pics/snowflake.png
index 6ea1c76f1..199c35905 100644
Binary files a/notes/pics/snowflake.png and b/notes/pics/snowflake.png differ
diff --git a/notes/pics/tcp-sticky.png b/notes/pics/tcp-sticky.png
index c46938772..ad5ffcf36 100644
Binary files a/notes/pics/tcp-sticky.png and b/notes/pics/tcp-sticky.png differ
diff --git a/notes/pics/tls-handshake.png b/notes/pics/tls-handshake.png
index 23ced200e..ddb3ecc77 100644
Binary files a/notes/pics/tls-handshake.png and b/notes/pics/tls-handshake.png differ
diff --git a/notes/pics/url-timeline.png b/notes/pics/url-timeline.png
index 3409d6f8e..428d14bb7 100644
Binary files a/notes/pics/url-timeline.png and b/notes/pics/url-timeline.png differ
diff --git "a/notes/\345\210\206\345\270\203\345\274\217.md" "b/notes/\345\210\206\345\270\203\345\274\217.md"
index 01dfa3ddb..900bc1837 100644
--- "a/notes/\345\210\206\345\270\203\345\274\217.md"
+++ "b/notes/\345\210\206\345\270\203\345\274\217.md"
@@ -85,7 +85,11 @@ SELECT * FROM distributed_lock WHERE name = 'order:1024' FOR UPDATE;
COMMIT;
```
-其它客户端执行同一条语句时阻塞,超过 innodb_lock_wait_timeout(默认 50 秒)返回错误;加上 NOWAIT 时拿不到锁立即返回错误,相当于非阻塞锁。持有锁的进程崩溃后连接断开,事务回滚,锁立即释放:kill -9 持有锁的进程后,等待中的客户端马上拿到了锁(MySQL 8.0 实测,见[附录 A7](#a7-mysql-行锁实现的分布式锁))。需要注意的是,持有锁的机器宕机或网络中断时,MySQL 收不到断开的通知,要等这个空闲连接超过 wait_timeout(默认 8 小时)才回滚。可以为持有锁的连接调小 wait_timeout,但要大于持锁期间连接空闲的最长时间(例如调用远程服务),否则业务还没执行完锁就被释放了。代价是持有锁期间一直占用一个数据库连接和一个未提交的事务,并发高时会耗尽连接池。
+其它客户端执行同一条语句时阻塞,超过 innodb_lock_wait_timeout(默认 50 秒)返回错误;加上 NOWAIT 时拿不到锁立即返回错误,相当于非阻塞锁。持有锁的进程崩溃后连接断开,事务回滚,锁立即释放:kill -9 持有锁的进程后,等待中的客户端马上拿到了锁(MySQL 8.0 实测,见[附录 A7](#a7-mysql-行锁实现的分布式锁))。
+
+需要注意的是,持有锁的机器宕机或网络中断时,MySQL 收不到断开的通知,要等这个空闲连接超过 wait_timeout(默认 8 小时)才回滚。可以为持有锁的连接调小 wait_timeout,但要大于持锁期间连接空闲的最长时间(例如调用远程服务),否则业务还没执行完锁就被释放了。
+
+代价是持有锁期间一直占用一个数据库连接和一个未提交的事务,并发高时会耗尽连接池。
### Redis 的 SET NX 指令
@@ -157,7 +161,7 @@ RedLock 不要求各节点的时钟同步,但假设各节点的时钟走速大
UPDATE inventory SET stock = 97, fence = 8 WHERE sku_id = 10086 AND fence <= 8;
```
-RedLock 使用的随机值不是递增的,算法本身无法提供栅栏令牌;ZooKeeper 则可以使用加锁时创建的有序节点的序号或者事务 ID(zxid),它们都是单调递增的。
+RedLock 使用的随机值不是递增的,算法本身无法提供栅栏令牌;ZooKeeper 则可以使用加锁时创建的节点的事务 ID(czxid),它在整个集群中单调递增。有序节点的序号不适合作令牌:序号的计数器属于父节点,Curator 把锁的父节点建成容器节点,锁空闲后父节点会被删除,序号又从 0 开始。
Redisson 已把 RedLock 标为弃用,改为推荐 RLock 加上主从同步检查;需要令牌时用 RFencedLock,它在加锁时对一个计数键执行 INCR,返回值作为令牌。这个计数键和锁在同一个主节点上,主从复制是异步的,故障转移后计数可能回退,令牌不再严格递增,所以它同样不能完全排除旧持有者的写入。
@@ -636,7 +640,7 @@ gRPC 选择 HTTP/2 是因为它的 [多路复用](HTTP.md#二进制分帧层):
- 每次调用都要设置超时时间,否则下游变慢时,调用方的线程会一直等待,最终被耗尽;
- 调用方超时不代表服务方没有执行:请求可能在网络中延迟,或者在服务方的线程池中排队、遇到 GC 停顿,这些时间都计入了调用方的耗时,却不在服务方记录的处理时间里,于是调用方超时了,服务方却执行成功了;
-- 因此只有幂等的接口才能自动重试,例如查询;扣库存这类写操作要带上请求 ID,由服务方去重(见 [幂等](#九幂等)),否则重试会扣减两次;
+- 因此只有幂等的接口才能自动重试,例如查询;扣库存这类写操作要带上请求 ID,由服务方去重(见 [幂等](#九幂等)),否则重试会扣减两次。需要注意的是,有的框架默认就会重试,例如 Dubbo 默认的集群容错方式是 failover,失败或超时后换一台服务器重试 2 次,一次调用最多执行 3 次,写操作要改为 failfast 或者带上请求 ID;
- 调用链较长时,下游的超时时间应该比上游短。gRPC 会把截止时间(deadline)随请求传给服务方,超时后调用方收到 DEADLINE_EXCEEDED,服务方的框架也会取消这次调用,但正在执行的业务代码不会被中断,耗时的处理要自己检查是否已被取消;服务方调用下游时沿用这次请求的上下文,Java 和 Go 的 gRPC 会自动把剩余的时间传下去。
@@ -932,7 +936,7 @@ RLock lock = redisson.getLock("lock:order:1024");
### A4 重复提交与唯一索引
-MySQL 8.0.44,Python 3 + pymysql。两张订单表结构相同,只是 t3_orders_uk 在 request_id 上有唯一索引。每轮用 10 个线程同时提交同一个 request_id,先查后插的写法先 SELECT,没有记录再 INSERT;唯一索引的写法直接 INSERT,捕获 1062 错误:
+MySQL 8.0.44,Python 3 + pymysql。两张订单表结构相同,只是 orders_uk 在 request_id 上有唯一索引。每轮用 10 个线程同时提交同一个 request_id,先查后插的写法先 SELECT,没有记录再 INSERT;唯一索引的写法直接 INSERT,捕获 1062 错误:
```text
先查后插 第 1 轮:同一个 request_id 并发提交 10 次,创建订单 2 个
@@ -952,15 +956,15 @@ MySQL 8.0.44,Python 3 + pymysql。两张订单表结构相同,只是 t3_orde
### A5 定时任务的重复执行与加锁
-Redis 7.4.1,MySQL 8.0.44,Python 3 + redis-py、pymysql。h3_users 中有 100 个用户,两个进程 A、B 模拟账单服务的两个实例,在同一时刻触发(B 的时钟慢 300 ms 时,B 晚 0.3 秒触发)。任务查询全部用户,每个用户向 h3_bill_sms 插入一条短信记录:
+Redis 7.4.1,MySQL 8.0.44,Python 3 + redis-py、pymysql。users 中有 100 个用户,两个进程 A、B 模拟账单服务的两个实例,在同一时刻触发(B 的时钟慢 300 ms 时,B 晚 0.3 秒触发)。任务查询全部用户,每个用户向 bill_sms 插入一条短信记录:
```python
if mode == 'release': # 固定的键,执行完删除
if not r.set('lock:job:bill', name, nx=True, px=60000): skip()
elif mode == 'period': # 键中带账单月份,执行完不删除
if not r.set('lock:job:bill:2026-11', name, nx=True, ex=3600): skip()
-for uid in select_ids(): # SELECT id FROM h3_users
- insert_sms(uid, '2026-11', name) # INSERT INTO h3_bill_sms (user_id, month, instance) ...
+for uid in select_ids(): # SELECT id FROM users
+ insert_sms(uid, '2026-11', name) # INSERT INTO bill_sms (user_id, month, instance) ...
if mode == 'release' and r.get('lock:job:bill') == name.encode():
r.delete('lock:job:bill')
```
@@ -988,11 +992,11 @@ if mode == 'release' and r.get('lock:job:bill') == name.encode():
### A6 分片与按分片重试
-环境同 A5。h3_users 中有 1 万个用户,每发送一条短信先等待 1 ms,模拟调用短信服务。发送前先向 h3_bill 插入 (user_id, month),它是主键,插入失败说明已经发送过,跳过这个用户:
+环境同 A5。users 中有 1 万个用户,每发送一条短信先等待 1 ms,模拟调用短信服务。发送前先向 bill_sent 插入 (user_id, month),它是主键,插入失败说明已经发送过,跳过这个用户:
```python
-for uid in select_ids(total, index): # SELECT id FROM h3_users WHERE id % total = index
- if execute('INSERT IGNORE INTO h3_bill (user_id, month) VALUES (%s, %s)', (uid, '2026-11')) == 0:
+for uid in select_ids(total, index): # SELECT id FROM users WHERE id % total = index
+ if execute('INSERT IGNORE INTO bill_sent (user_id, month) VALUES (%s, %s)', (uid, '2026-11')) == 0:
continue
time.sleep(0.001)
insert_sms(uid, '2026-11', name)
diff --git "a/notes/\345\244\247\346\250\241\345\236\213\345\272\224\347\224\250.md" "b/notes/\345\244\247\346\250\241\345\236\213\345\272\224\347\224\250.md"
index ae72e83d0..381e9a276 100644
--- "a/notes/\345\244\247\346\250\241\345\236\213\345\272\224\347\224\250.md"
+++ "b/notes/\345\244\247\346\250\241\345\236\213\345\272\224\347\224\250.md"
@@ -184,7 +184,7 @@ MoE 与参数量相同的 Dense 模型相比:
-1. 用“饿”的 Q 与每个 Token 的 K 做点积得到相关性分数,再除以 √d,d 是向量的维度;
+1. 用“饿”的 Q 与每个 Token 的 K 做点积得到相关性分数,再除以 √d,d 是 Q、K 向量的维度,多头注意力中是每个头的维度(Qwen2.5-7B 为 3584 ÷ 28 = 128);
2. 对分数做 softmax 得到权重,权重之和为 1,其中“小猫”的权重最大,为 0.70;
3. 按权重对所有 Token 的 V 求和,得到“饿”这个位置的输出,它的大部分信息来自“小猫”。
@@ -468,7 +468,7 @@ QLoRA 进一步把冻结的原始参数量化为 4 位存储,计算时临时
- 双重量化:把缩放系数本身再量化一次,平均每个参数再节省约 0.37 位;
- 分页优化器:显存不足时把优化器状态临时换出到内存。
-Qwen2.5-7B 的权重在 4 位下约 4 GB,比 BF16 的 15.2 GB 少了 11 GB 左右;论文中 65B 的模型可以在一张 48 GB 的显卡上微调。
+Qwen2.5-7B 的权重在 4 位下约 5.5 GB,比 BF16 的 15.2 GB 少了 10 GB 左右,其中约 2.2 GB 是保持 BF16 的嵌入层和输出层(估算);论文中 65B 的模型可以在一张 48 GB 的显卡上微调。
全参数微调与 LoRA 的取舍:在代码和数学上的对比实验中,LoRA 在目标领域的效果明显不如全参数微调,但更好地保留了原有的能力;全参数微调学到的变化量的秩是常见 LoRA 配置的 10~100 倍。因此:
@@ -497,7 +497,7 @@ LoRA 微调的效果不好时,按以下顺序检查:
### 量化
-量化(Quantization)指的是用更少的位数存储模型的参数,例如把 BF16(16 位)的权重转为 INT8 或 INT4。Qwen2.5-7B 的权重在 BF16 下约 15.2 GB,INT8 约 7.6 GB,INT4 约 3.8 GB(本节的 GB 按 109 字节计算,按 230 字节计算时 BF16 权重约 14.2 GB)。
+量化(Quantization)指的是用更少的位数存储模型的参数,例如把 BF16(16 位)的权重转为 INT8 或 INT4。Qwen2.5-7B 的权重在 BF16 下约 15.2 GB,INT8 约 7.6 GB,INT4 约 3.8 GB(本节的 GB 按 109 字节计算,按 230 字节计算时 BF16 权重约 14.2 GB)。这是全部参数都按同样的位数折算的值。嵌入层和输出层通常不量化,Qwen2.5-7B 的这两层不共享权重,约 10.9 亿个参数,所以官方发布的 INT4(AWQ)模型权重实际约 5.6 GB。
量化不仅省显存,还能加快生成。这是因为解码阶段每生成一个 Token,都要把全部权重从显存读一遍,而计算量很小,速度受显存带宽限制,权重越小读得越快。推理服务的整体显存怎样估算,见[显存:权重与 KV Cache](#3-显存权重与-kv-cache)。
@@ -555,7 +555,7 @@ KV Cache 也可以量化。Qwen2.5-7B 每个 Token 的 KV Cache 在 BF16 下占
#### 2. 为什么会产生
-根本原因在于训练目标:模型学的是生成通顺的下一个 Token,而不是核对事实,不知道的内容也会被“合理地”补全。评测也在鼓励这种行为。多数评测只按答对的比例打分,回答“不知道”得 0 分,猜一个答案还有机会得分,所以倾向于猜测的模型在评测上分数更高,训练和挑选模型时也就偏向了猜测,而不是承认不确定。
+根本原因在于训练目标:模型学的是生成通顺的下一个 Token,而不是核对事实,不知道的内容也会被“合理地”补全。评测也在鼓励这种行为。多数评测只按答对的比例打分,回答“不知道”得 0 分,猜一个答案还有机会得分,所以倾向于猜测的模型在评测上分数更高,训练和挑选模型时也就偏向了猜测,而不是承认不确定。自建评测集时要把答对、答错和回答不知道分开统计,答错扣的分比不回答更多。
其它原因:
@@ -603,7 +603,7 @@ Agent 的幻觉除了回答错误,还会出现在工具调用的过程中。
- 结果核对:最终回答中的时间、金额、单号等关键信息,用代码检查是否出现在本轮的工具结果中;“已退款”这类操作结论,以退款工具是否真的返回成功为准,由代码生成确认消息;
- 多 Agent:子 Agent 之间传递结构化的结果和来源,关键结论回到原始数据核对,避免一个子 Agent 的错误被其它 Agent 当成事实继续使用。
-幻觉无法完全消除,工程上的目标是降低出现的概率,并让错误能被发现。
+模型不可能答对所有问题,但不确定时可以回答“不知道”,而不是猜一个答案。工程上的目标是降低自信地答错的概率,并让错误能被发现。
### 推理模型
@@ -1021,6 +1021,10 @@ embedding 由 text 经 Embedding 模型计算得到,bge-small-zh-v1.5 输出 5
文档修改后,按文档 ID 和内容的哈希值找出变化的文档,删除它的全部旧切片再写入新切片,不需要重建整个索引;只追加新切片会让旧内容继续被召回。只是权限或版本状态变化时,更新切片上的元数据即可,不必重新计算向量。文档的原文和元数据一般以关系数据库为准,向量库按切片 ID 幂等写入,失败时重试,并定期对账两边的数据。
+哈希只能发现被修改的文档,整篇源文件被删除时,要靠文档系统的删除事件,或者定期拿源系统的全部文档 ID 与库中比对,删掉多出来的切片。
+
+删旧切片和写新切片不是一次完成的,期间查询可能缺少这篇文档或新旧内容并存;在意这一点时,先写入带新版本号的切片,再把元数据中的当前版本切过去,最后删除旧切片。另外,Milvus 默认的一致性级别是 Bounded,刚写入的数据可能要稍等一会才能查到。
+
更换 Embedding 模型或切片方式时,所有向量都要重新计算。这时在新的集合中建好索引并评测,再把查询使用的别名(Alias)从旧集合切到新集合,切换过程中查询不中断,出问题时切回去。Milvus 和 Elasticsearch 都支持别名。换了模型时,查询向量也要用新模型计算,所以查询用的模型和集合作为一组配置同时切换、同时回滚。
#### 3. 大规模数据
@@ -1221,7 +1225,7 @@ GraphRAG 在建索引时让模型从每个切片中抽取实体和关系,组
### Agent 是什么 🔥🔥
-普通的大模型调用是一问一答:模型根据输入生成一段文本。它无法查询数据库、调用接口,也就拿不到训练数据以外的信息,更不能替用户执行操作。
+普通的大模型调用是一问一答:模型根据输入生成一段文本。它无法自行查询数据库、调用接口,只能依据训练中学到的知识和输入中给出的资料,更不能替用户执行操作。
Agent(智能体)指的是让大模型通过调用工具来完成任务的系统。模型负责决定下一步做什么,应用程序负责执行,并把结果交还给模型,如此循环,直到任务完成。例如用户问“订单 1024 到哪了”:
@@ -1576,6 +1580,8 @@ EXPIRE session:7f3a:state 86400
模型正确地选择了更新 m1,但在新记忆中写了一个编造的日期。所以模型只负责判断做哪种操作、新的内容是什么,用户 ID、时间这类字段由代码填写。
+需要注意的是,这是 Mem0 论文和早期版本的做法。Mem0 开源版后来改为自动写入只做新增:一次模型调用提取新事实,旧记忆只用来去重,搬家后新旧两条地址都保留,更正和删除由应用调用 update、delete 完成(以官方最新版本为准)。
+
新旧信息冲突时,按可信程度和适用范围判断:用户本次明确说的优先于记忆中的,用户亲口说的优先于模型推断的,来源相同时以新的为准;只对这一次有效的说法不覆盖长期记忆,例如“这次说详细一点”不会改变“喜欢简短的回答”;订单、账户这类数据以业务系统为准,本来就不写进记忆。仍然无法判断时,在对话中向用户确认。
被更新或删除的旧记忆不直接物理删除,而是标记失效时间,出错时可以追溯和恢复。错误或者过时的记忆被反复检索出来、影响回答,称为记忆污染。除了上面的规则,还可以在放入上下文的记忆前注明“来自长期记忆,可能已经过时,与用户本次的说法冲突时以用户为准”,并给每条记忆带上时间,让模型自己判断。
@@ -1701,7 +1707,8 @@ MCP 中有三种角色:
"params": {"name": "read_file", "arguments": {"path": "README.md"}}}
{"jsonrpc": "2.0", "id": 2,
- "result": {"content": [{"type": "text", "text": "# CS-Notes ..."}], "isError": false}}
+ "result": {"resultType": "complete",
+ "content": [{"type": "text", "text": "# CS-Notes ..."}], "isError": false}}
```
调用之前,双方还要确认协议版本和各自支持的能力,例如服务端是否提供工具、工具列表变化时是否发通知。2025-11-25 及以前的版本在连接建立时用 initialize 握手协商一次;2026-07-28 版去掉了握手,每个请求在 _meta 中带上协议版本和客户端能力,服务端逐个请求判断是否支持,客户端也可以先用 server/discover 查询服务端支持的版本和能力。
@@ -1735,7 +1742,7 @@ MCP 与相关概念的区别:
- 普通的 HTTP 接口也能封装成工具,但每个接口的认证方式、参数格式和文档各不相同,需要逐个适配;MCP 统一了发现和调用的方式,工具描述本身就是写给模型看的;
- 能执行命令的 Agent 可以直接调用 git、kubectl 这类命令行工具(CLI)。模型在训练中见过大量常用命令,不需要事先把定义放进上下文,需要时运行 --help 查看,输出还可以先用管道过滤,只有结果进入上下文;代价是需要能执行命令的环境,权限只能靠沙箱和命令白名单限制,输出是给人看的文本,没有统一的 Schema。MCP 适合需要统一授权、远程访问或者结构化结果的场景,以及没有命令行环境的应用,例如聊天客户端接入公司的工单系统。
-一个 Agent 接入多个服务端时,主机为每个服务端建立一个客户端,把各服务端的工具合并后交给模型。工具名只要求在一个服务端内唯一,合并时可能重名,所以主机通常加上服务端名作前缀,例如 Claude Code 中的工具名形如 mcp__服务端名__工具名。
+一个 Agent 接入多个服务端时,主机为每个服务端建立一个客户端,把各服务端的工具合并后交给模型。规范只建议工具名在一个服务端内唯一,不同服务端之间可能重名,所以主机通常加上服务端名作前缀(服务端自报的名称也不保证唯一,Claude Code 用的是用户配置中的名称),例如 Claude Code 中的工具名形如 mcp__服务端名__工具名。
MCP 也有代价。每个服务端的工具定义都会进入上下文,Anthropic 的文档中举的例子是同时接入 GitHub、Slack、Sentry、Grafana、Splunk 五个服务端,工具定义就占约 5.5 万个 Token;可用的工具超过 30~50 个后,模型选对工具的能力也会明显下降。解决方法是按需加载:上下文中只放一个搜索工具和最常用的几个工具,模型先搜索,再把找到的工具的完整定义加入上下文;也可以由应用先根据问题检索出相关的工具,只把这几个交给模型。Anthropic 还介绍过一种做法:把 MCP 工具生成为代码文件,Agent 按需读取工具的定义、编写代码调用,中间结果留在执行环境中,不经过模型,例子中的 Token 消耗从 15 万降到 2000。
@@ -1833,7 +1840,7 @@ Skill 越来越多时,目录中每个 Skill 的描述只占约 100 个 Token
- 主管分派:主管 Agent 决定把哪些子任务交给哪个 Agent,最后汇总,也就是上面的主从模式。子任务由主管根据输入临时决定,适合子任务互相独立、可以并行的情况;
- 流水线:几个 Agent 按固定的顺序执行,上一个的输出是下一个的输入,例如提取商品参数、撰写文案、合规审核。它本质上是节点恰好为 Agent 的工作流,审核不通过可以退回上一步,但要限制退回的次数;
-- 讨论:几个 Agent 先各自给出答案,再看过彼此的答案和理由后修改,几轮之后投票或者由裁决者给出结论。研究表明这种方式能提高算术和事实问答的准确率,代价是调用次数等于 Agent 数乘以轮数,适合出错代价高、答案可以互相检验的判断;
+- 讨论:几个 Agent 先各自给出答案,再看过彼此的答案和理由后修改,几轮之后投票或者由裁决者给出结论。研究表明这种方式能提高算术和事实问答的准确率,但后来的对照实验发现,提升大部分来自最后的多数投票,只让几个 Agent 各答一次再投票就能得到大部分收益;它的代价是调用次数等于 Agent 数乘以轮数,适合出错代价高、答案可以互相检验的判断;
- 转交(Handoff):没有固定的主管,当前 Agent 判断问题不归自己处理时,把控制权和需要的上下文(完整历史、筛选后的消息或摘要)交给另一个 Agent,由它接着处理并直接回复用户。例如客服的分诊 Agent 调用 transfer_to_refund_agent 工具,之后由退款 Agent 接手对话。与主管分派的区别是接手的 Agent 处理完不会像工具调用那样自动返回原来的 Agent,需要时再转交回去,适合按业务领域划分、由专门的 Agent 直接面对用户的场景。
Agent 之间传递信息的方式有三种:主管分派时通过工具调用的参数和返回值;同一个应用内通过共享的状态对象,例如 LangGraph 中各节点读写的状态,见[开发框架](#开发框架);跨服务、跨团队时通过消息队列或者下文的 A2A 协议。通过消息队列传递时,同一条消息可能被投递多次,接收方要用任务 ID 这类业务键去重,见[接收端的可靠性](消息队列.md#接收端的可靠性)。
@@ -1930,7 +1937,7 @@ Harness 的每个部分都对应一个模型自己做不到或做不稳的问题
| 钩子与校验 | 在固定时机执行确定的检查,不通过就把错误送回模型继续 |
| 评测与可观测 | 记录每一步的输入输出和 Token,改动 Harness 后用同一批任务回归,见 [可靠性与评测](#可靠性与评测) |
-工具的返回很长时,Harness 通常只把开头和结尾,或者最新的几条放进上下文,完整结果写到文件中,模型需要时再读取;Claude Code 对 MCP 工具的返回默认限制为 25,000 个 Token,超过 5 万字符的文本结果写入文件,只把路径交给模型。
+工具的返回很长时,Harness 通常只把开头和结尾,或者最新的几条放进上下文,完整结果写到文件中,模型需要时再读取;Claude Code 对 MCP 工具的返回默认限制为 25,000 个 Token,超过 5 万字符的文本结果写入文件,只把路径交给模型(2026 年 10 月的官方文档)。
#### 2. 引导与反馈
@@ -2065,7 +2072,9 @@ String answer = ChatClient.create(chatModel)
框架根据方法签名生成参数的 JSON Schema,模型返回工具调用后,由框架执行方法、把结果发回模型,直到得到最终回答。工具抛出运行时异常时,默认把异常信息作为工具结果发回模型,让它修正或者向用户说明。需要注意的是,编译时要保留参数名(javac 的 -parameters 选项,继承 spring-boot-starter-parent 时默认开启),否则 Schema 中的参数名是 arg0(Spring AI 2.0.2 实测,见[附录 A9](#a9-spring-ai-的工具调用循环))。
-对话记忆由 ChatMemory 接口管理:默认的 MessageWindowChatMemory 保留最近的 20 条消息,系统消息始终保留;消息存放在 ChatMemoryRepository 中,默认在内存里,也可以换成 JDBC、Cassandra 等实现。使用时给 ChatClient 加上 MessageChatMemoryAdvisor,每次调用传入会话 ID,它在调用前按会话 ID 取出历史放进请求,调用后保存新的消息。需要注意的是,工具调用的请求和结果不会写入记忆,下一轮对话时模型看不到上一轮查过什么(Spring AI 2.0.2 实测,见[附录 A28](#a28-spring-ai-的对话记忆与工具调用)),需要时由应用自己保存,见[短期记忆](#1-短期记忆)。
+对话记忆由 ChatMemory 接口管理:默认的 MessageWindowChatMemory 保留最近的 20 条消息,系统消息始终保留;消息存放在 ChatMemoryRepository 中,默认在内存里,也可以换成 JDBC、Cassandra 等实现。使用时给 ChatClient 加上 MessageChatMemoryAdvisor,每次调用传入会话 ID,它在调用前按会话 ID 取出历史放进请求,调用后保存新的消息。需要注意的是,默认情况下工具调用的请求和结果不会写入记忆,下一轮对话时模型看不到上一轮查过什么(Spring AI 2.0.2 实测,见[附录 A28](#a28-spring-ai-的对话记忆与工具调用))。这是因为 MessageChatMemoryAdvisor 默认排在工具调用循环之外,只保存最终的用户消息和回答。
+
+需要保存工具记录时,可以把 MessageChatMemoryAdvisor 的 order 设为大于 ToolCallingAdvisor 的值;Spring AI 2.0 的内置仓库中只有内存、Redis、Neo4j 能保存工具消息,JDBC 不能(以官方最新文档为准),用 JDBC 时由应用自己保存,见[短期记忆](#1-短期记忆)。
Spring AI 主要解决模型接入和单个 Agent 的工具调用。Java 中另一个常用的框架是 LangChain4j,它不依赖 Spring,可以集成到 Spring Boot 或 Quarkus 中,特点是 AI Services:声明一个接口,框架生成实现这个接口的代理对象,与 Spring Data JPA 的 Repository 类似。两者的功能相近,Spring 项目用 Spring AI 与已有的配置、依赖注入结合得更自然。需要状态图编排时,可以用仿照 LangGraph 的 LangGraph4j,它可以与 LangChain4j 或 Spring AI 一起使用。
@@ -2399,7 +2408,7 @@ batch 从 1 增大到 64,总吞吐从 22 Token/s 提高到 214 Token/s,约 9
所以在线对话服务要在延迟和吞吐之间取舍:先定下延迟的目标,例如 TPOT 不超过 50 毫秒,再选能满足目标的最大并发数;离线的批量任务不在乎单个请求的延迟,批次尽量大。
-需要注意的是,同一个请求在不同大小的批次中,浮点运算的顺序可能不同,结果可能出现细微的差别(这次在笔记本上的测试中,各个 batch 大小下的贪心输出一致)。对可复现性要求高时,vLLM 提供批次不变(Batch Invariance)的模式,代价是速度可能变慢。
+需要注意的是,同一个请求在不同大小的批次中,浮点运算的顺序可能不同,结果可能出现细微的差别(附录 A21 的实验中,各个 batch 大小下的贪心输出一致)。对可复现性要求高时,vLLM 提供批次不变(Batch Invariance)的模式,代价是速度可能变慢。
#### 2. 连续批处理
@@ -3788,6 +3797,7 @@ for r, j in enumerate(B[i]): sc[j] += w / (61 + r)
- [Fast Inference from Transformers via Speculative Decoding](https://arxiv.org/abs/2211.17192)
- [Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism](https://arxiv.org/abs/1909.08053)
- [SGLang: Efficient Execution of Structured Language Model Programs](https://arxiv.org/abs/2312.07104)
+- [Debate or Vote: Which Yields Better Decisions in Multi-Agent Large Language Models?](https://arxiv.org/abs/2508.17536)
- [SGLang: Server Arguments](https://docs.sglang.io/docs/advanced_features/server_arguments)
- [vLLM: Parallelism and Scaling](https://docs.vllm.ai/en/latest/serving/parallelism_scaling/)
- [vLLM: Metrics](https://docs.vllm.ai/en/latest/design/metrics/)
diff --git "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md" "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md"
index dd3f69eff..4cfee70b5 100644
--- "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md"
+++ "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md"
@@ -82,6 +82,9 @@
* [第 65 天:Kubernetes](#第-65-天kubernetes)
* [第 66 天:系统设计题的思路与即时通讯](#第-66-天系统设计题的思路与即时通讯)
* [第 67 天:订单与支付](#第-67-天订单与支付)
+ * [十五、大模型应用](#十五大模型应用)
+ * [第 68 天:RAG 与 Agent](#第-68-天rag-与-agent)
+ * [第 69 天:MCP、Skills 与 AI 编程助手](#第-69-天mcpskills-与-ai-编程助手)
@@ -151,40 +154,43 @@
| | **计算机网络** | | |
| [36](#第-36-天tcp-的连接管理) | TCP 的连接管理 | 计算机网络 - 传输层:UDP 和 TCP 的特点~TCP 的四次挥手 | 101. 对称二叉树 |
| [37](#第-37-天可靠传输与拥塞控制) | 可靠传输与拥塞控制 | 计算机网络 - 传输层:TCP 可靠传输~在 UDP 上实现可靠传输 | 104. 二叉树的最大深度 |
-| [38](#第-38-天http-基础) | HTTP 基础 | HTTP:HTTP 基础概念~HTTP 首部 | 102. 二叉树的层序遍历 |
-| [39](#第-39-天连接管理与-cookie) | 连接管理与 Cookie | HTTP:连接管理~Cookie | 103. 二叉树的锯齿形层序遍历 |
-| [40](#第-40-天缓存websocket-与跨域) | 缓存、WebSocket 与跨域 | HTTP:HTTP 缓存~跨域 | 236. 二叉树的最近公共祖先 |
-| [41](#第-41-天https) | HTTPS | HTTP:HTTPS | 199. 二叉树的右视图 |
-| [42](#第-42-天http-的版本get-与-postdns) | HTTP 的版本、GET 与 POST、DNS | HTTP:HTTP/1.1 新特性~GET 和 POST 比较;计算机网络 - 应用层:域名系统 | 105. 从前序与中序遍历序列构造二叉树 |
-| [43](#第-43-天从输入-url-到显示页面) | 从输入 URL 到显示页面 | 计算机网络 - 应用层:Web 页面请求过程;攻击技术:跨站脚本攻击~SQL 注入攻击 | 98. 验证二叉搜索树 |
+| [38](#第-38-天http-基础) | HTTP 基础 | HTTP:HTTP 基础概念~HTTP 首部 | 543. 二叉树的直径 |
+| [39](#第-39-天连接管理与-cookie) | 连接管理与 Cookie | HTTP:连接管理~Cookie | 102. 二叉树的层序遍历 |
+| [40](#第-40-天缓存websocket-与跨域) | 缓存、WebSocket 与跨域 | HTTP:HTTP 缓存~跨域 | 103. 二叉树的锯齿形层序遍历 |
+| [41](#第-41-天https) | HTTPS | HTTP:HTTPS | 236. 二叉树的最近公共祖先 |
+| [42](#第-42-天http-的版本get-与-postdns) | HTTP 的版本、GET 与 POST、DNS | HTTP:HTTP/1.1 新特性~GET 和 POST 比较;计算机网络 - 应用层:域名系统 | 199. 二叉树的右视图 |
+| [43](#第-43-天从输入-url-到显示页面) | 从输入 URL 到显示页面 | 计算机网络 - 应用层:Web 页面请求过程;攻击技术:跨站脚本攻击~SQL 注入攻击 | 105. 从前序与中序遍历序列构造二叉树 |
| | **操作系统** | | |
-| [44](#第-44-天进程与线程) | 进程与线程 | 计算机操作系统 - 进程管理:进程与线程~进程状态的切换 | 200. 岛屿数量 |
-| [45](#第-45-天系统调用进程同步与通信) | 系统调用、进程同步与通信 | 计算机操作系统 - 概述:系统调用;计算机操作系统 - 进程管理:进程调度算法~进程通信 | 46. 全排列 |
-| [46](#第-46-天死锁) | 死锁 | 计算机操作系统 - 死锁:必要条件~程序中的死锁 | 93. 复原 IP 地址 |
-| [47](#第-47-天内存管理) | 内存管理 | 计算机操作系统 - 内存管理:虚拟内存~分页与分段的比较 | 22. 括号生成 |
-| [48](#第-48-天io-多路复用) | I/O 多路复用 | Socket:I/O 模型~I/O 复用 | 78. 子集 |
+| [44](#第-44-天进程与线程) | 进程与线程 | 计算机操作系统 - 进程管理:进程与线程~进程状态的切换 | 98. 验证二叉搜索树 |
+| [45](#第-45-天系统调用进程同步与通信) | 系统调用、进程同步与通信 | 计算机操作系统 - 概述:系统调用;计算机操作系统 - 进程管理:进程调度算法~进程通信 | 200. 岛屿数量 |
+| [46](#第-46-天死锁) | 死锁 | 计算机操作系统 - 死锁:必要条件~程序中的死锁 | 46. 全排列 |
+| [47](#第-47-天内存管理) | 内存管理 | 计算机操作系统 - 内存管理:虚拟内存~分页与分段的比较 | 93. 复原 IP 地址 |
+| [48](#第-48-天io-多路复用) | I/O 多路复用 | Socket:I/O 模型~I/O 复用 | 22. 括号生成 |
| | **Spring 与设计模式** | | |
-| [49](#第-49-天ioc-与-bean) | IoC 与 Bean | Spring:IoC 与 Bean | 39. 组合总和 |
-| [50](#第-50-天aop-与事务) | AOP 与事务 | Spring:AOP~事务 | 121. 买卖股票的最佳时机 |
-| [51](#第-51-天spring-bootspring-mvc-与-mybatis) | Spring Boot、Spring MVC 与 MyBatis | Spring:Spring Boot 自动配置~Spring 中的设计模式 | 70. 爬楼梯 |
-| [52](#第-52-天设计模式) | 设计模式 | 设计模式 - 目录:设计模式前言;设计模式 - 单例:单例(Singleton);设计模式 - 策略:策略(Strategy);面向对象思想:设计原则 | 53. 最大子数组和 |
+| [49](#第-49-天ioc-与-bean) | IoC 与 Bean | Spring:IoC 与 Bean | 78. 子集 |
+| [50](#第-50-天aop-与事务) | AOP 与事务 | Spring:AOP~事务 | 39. 组合总和 |
+| [51](#第-51-天spring-bootspring-mvc-与-mybatis) | Spring Boot、Spring MVC 与 MyBatis | Spring:Spring Boot 自动配置~Spring 中的设计模式 | 121. 买卖股票的最佳时机 |
+| [52](#第-52-天设计模式) | 设计模式 | 设计模式 - 目录:设计模式前言;设计模式 - 单例:单例(Singleton);设计模式 - 策略:策略(Strategy);面向对象思想:设计原则 | 70. 爬楼梯 |
| | **分布式与消息队列** | | |
-| [53](#第-53-天微服务) | 微服务 | 微服务:从单体到微服务~网关 | 5. 最长回文子串 |
-| [54](#第-54-天分布式锁) | 分布式锁 | 分布式:分布式锁 | 300. 最长递增子序列 |
-| [55](#第-55-天分布式事务与-cap) | 分布式事务与 CAP | 分布式:分布式事务~BASE | 1143. 最长公共子序列 |
-| [56](#第-56-天rpc分布式-id-与幂等) | RPC、分布式 ID 与幂等 | 分布式:RPC~幂等 | 322. 零钱兑换 |
-| [57](#第-57-天消息队列的作用与可靠性) | 消息队列的作用与可靠性 | 消息队列:消息模型~消息队列的可靠性 | 64. 最小路径和 |
-| [58](#第-58-天顺序积压与延迟消息) | 顺序、积压与延迟消息 | 消息队列:顺序性~延迟消息与死信队列 | 198. 打家劫舍 |
-| [59](#第-59-天kafka) | Kafka | 消息队列:Kafka | 42. 接雨水 |
+| [53](#第-53-天微服务) | 微服务 | 微服务:从单体到微服务~网关 | 53. 最大子数组和 |
+| [54](#第-54-天分布式锁) | 分布式锁 | 分布式:分布式锁 | 5. 最长回文子串 |
+| [55](#第-55-天分布式事务与-cap) | 分布式事务与 CAP | 分布式:分布式事务~BASE | 300. 最长递增子序列 |
+| [56](#第-56-天rpc分布式-id-与幂等) | RPC、分布式 ID 与幂等 | 分布式:RPC~幂等 | 1143. 最长公共子序列 |
+| [57](#第-57-天消息队列的作用与可靠性) | 消息队列的作用与可靠性 | 消息队列:消息模型~消息队列的可靠性 | 322. 零钱兑换 |
+| [58](#第-58-天顺序积压与延迟消息) | 顺序、积压与延迟消息 | 消息队列:顺序性~延迟消息与死信队列 | 64. 最小路径和 |
+| [59](#第-59-天kafka) | Kafka | 消息队列:Kafka | 62. 不同路径 |
| | **系统设计** | | |
-| [60](#第-60-天性能与可用性) | 性能与可用性 | 系统设计基础:性能~安全性 | 239. 滑动窗口最大值 |
-| [61](#第-61-天秒杀与抢红包) | 秒杀与抢红包 | 系统设计基础:秒杀系统 | 76. 最小覆盖子串 |
-| [62](#第-62-天海量数据短链与-feed-流) | 海量数据、短链与 Feed 流 | 系统设计基础:海量数据处理~Feed 流 | 25. K 个一组翻转链表 |
-| [63](#第-63-天高并发系统的设计) | 高并发系统的设计 | 系统设计基础:高并发系统的设计 | 23. 合并 K 个升序链表 |
-| [64](#第-64-天docker) | Docker | Docker:解决的问题~镜像与容器 | 32. 最长有效括号 |
-| [65](#第-65-天kubernetes) | Kubernetes | Docker:Kubernetes | 4. 寻找两个正序数组的中位数 |
-| [66](#第-66-天系统设计题的思路与即时通讯) | 系统设计题的思路与即时通讯 | 系统设计基础:系统设计题的思路~即时通讯 | 124. 二叉树中的最大路径和 |
-| [67](#第-67-天订单与支付) | 订单与支付 | 系统设计基础:订单与支付 | 72. 编辑距离 |
+| [60](#第-60-天性能与可用性) | 性能与可用性 | 系统设计基础:性能~安全性 | 198. 打家劫舍 |
+| [61](#第-61-天秒杀与抢红包) | 秒杀与抢红包 | 系统设计基础:秒杀系统 | 42. 接雨水 |
+| [62](#第-62-天海量数据短链与-feed-流) | 海量数据、短链与 Feed 流 | 系统设计基础:海量数据处理~Feed 流 | 239. 滑动窗口最大值 |
+| [63](#第-63-天高并发系统的设计) | 高并发系统的设计 | 系统设计基础:高并发系统的设计 | 76. 最小覆盖子串 |
+| [64](#第-64-天docker) | Docker | Docker:解决的问题~镜像与容器 | 25. K 个一组翻转链表 |
+| [65](#第-65-天kubernetes) | Kubernetes | Docker:Kubernetes | 23. 合并 K 个升序链表 |
+| [66](#第-66-天系统设计题的思路与即时通讯) | 系统设计题的思路与即时通讯 | 系统设计基础:系统设计题的思路~即时通讯 | 32. 最长有效括号 |
+| [67](#第-67-天订单与支付) | 订单与支付 | 系统设计基础:订单与支付 | 4. 寻找两个正序数组的中位数 |
+| | **大模型应用** | | |
+| [68](#第-68-天rag-与-agent) | RAG 与 Agent | 大模型应用:RAG 的流程;大模型应用:Agent 是什么~Agent 与工作流 | 124. 二叉树中的最大路径和 |
+| [69](#第-69-天mcpskills-与-ai-编程助手) | MCP、Skills 与 AI 编程助手 | 大模型应用:MCP~Skills;大模型应用:AI 编程助手 | 72. 编辑距离 |
## 四、Java 基础
@@ -275,7 +281,7 @@ static 一节的初始化顺序可以对照代码自己推一遍。equals() 和
### 第 4 天:继承与多态
-先读面向对象的三大特性,再读 Java 中的实现:访问权限、抽象类与接口、重写与重载。多态一节说明了运行时怎样找到被调用的方法。
+先读面向对象的三大特性,再读 Java 中的实现:访问权限、抽象类与接口、重写与重载。多态一节说明了运行时怎样找到被调用的方法。重写一节的三条限制对照第一个示例理解;后面较长的示例重点看编译时选定方法、运行时确定实现的两步过程。
阅读:《面向对象思想》的[三大特性](面向对象思想.md#一三大特性);《Java 基础》的[继承](Java%20基础.md#六继承)。
@@ -319,7 +325,7 @@ static 一节的初始化顺序可以对照代码自己推一遍。equals() 和
1. 类加载时 JVM 根据 .class 文件创建 Class 对象,反射通过它在运行时取得类的信息,类名可以到运行时才确定;用 Field 读写字段,Method 的 invoke() 调用方法,Constructor 的 newInstance() 创建对象;应用:Spring 创建 Bean 并注入、MyBatis 写入结果字段、JUnit 调用 @Test 方法、JDK 动态代理;缺点是有性能开销(框架会缓存 Method 对象),类名和方法名写成字符串、失去编译期检查,JDK 16 起默认不能反射访问 JDK 内部的私有成员,会破坏封装。
2. 会执行,finally 在离开 try 语句之前执行;返回值在执行 finally 之前已经保存,返回基本类型时 finally 修改变量不影响返回值,返回引用时能看到对对象的修改;finally 中的 return 会覆盖返回值并吞掉异常;调用 System.exit()、kill -9 或 JVM 崩溃时 finally 不执行。
-3. 泛型只在编译时检查类型,编译后类型参数替换为上界(没有上界时为 Object),取出元素处由编译器插入强制类型转换;运行时 List 和 List 是同一个类,所以类型参数不能是基本类型,不能 new T() 或用 instanceof List;类、字段、方法声明上的泛型信息保存在 Signature 属性中,可以通过反射读取。
+3. 泛型只在编译时检查类型,编译后类型参数替换为上界(没有上界时为 Object),取出元素处由编译器插入强制类型转换;运行时 List\ 和 List\ 是同一个类,所以类型参数不能是基本类型,不能 new T() 或用 instanceof List\;类、字段、方法声明上的泛型信息保存在 Signature 属性中,可以通过反射读取。
@@ -622,7 +628,7 @@ AQS 一节先看 state 和等待队列怎样实现 ReentrantLock 的加锁、重
### 第 15 天:ThreadLocal 与锁优化
-ThreadLocal 重点看它的数据结构、内存泄漏的原因,以及线程池复用线程时的数据串用。锁优化讲 JVM 怎样降低 synchronized 的开销,按轻量级锁、偏向锁、锁升级的过程读。
+ThreadLocal 重点看它的数据结构、内存泄漏的原因,以及线程池复用线程时的数据串用。向线程池传递 traceId 时,看懂 wrap() 中捕获、设置和恢复上下文的过程即可,TaskDecorator 和 TTL 的介绍可以略读。锁优化讲 JVM 怎样降低 synchronized 的开销,按轻量级锁、偏向锁、锁升级的过程读。
阅读:《Java 并发》从[无同步方案](Java%20并发.md#无同步方案)读到[多线程开发良好的实践](Java%20并发.md#十三多线程开发良好的实践)。
@@ -794,7 +800,7 @@ ThreadLocal 重点看它的数据结构、内存泄漏的原因,以及线程
参考要点
-1. 加载:按全限定名取得字节流,转换为方法区的运行时结构,生成 Class 对象;验证字节流符合规范、不危害虚拟机;准备:为类变量分配内存并设为零值(基本类型或 String、用常量表达式初始化的 static final 常量不经 () 直接取 ConstantValue 的值,《深入理解 Java 虚拟机》把这一步归在准备阶段,JVMS SE 21 写在初始化过程中);解析:把常量池中的符号引用替换为直接引用;初始化:执行 (),即类变量赋值和静态语句块,父类先于子类,多线程下只有一个线程执行。
+1. 加载:按全限定名取得字节流,转换为方法区的运行时结构,生成 Class 对象;验证字节流符合规范、不危害虚拟机;准备:为类变量分配内存并设为零值(基本类型或 String、用常量表达式初始化的 static final 常量不经 \() 直接取 ConstantValue 的值,《深入理解 Java 虚拟机》把这一步归在准备阶段,JVMS SE 21 写在初始化过程中);解析:把常量池中的符号引用替换为直接引用;初始化:执行 \(),即类变量赋值和静态语句块,父类先于子类,多线程下只有一个线程执行。
2. 类加载器先把加载请求交给父加载器,父加载器无法完成时才自己加载;好处是基础类统一,例如自己写的 java.lang.Object 不会替换启动类加载器加载的那个,程序中所有的 Object 都是同一个类;父加载器已经加载过的类不会重复加载,避免同一个类出现两个互不相等的 Class 对象。
3. 重写 loadClass(),先自己加载、找不到再交给父加载器;例子:JDBC 的 DriverManager 由平台类加载器加载,通过 ServiceLoader 用线程上下文类加载器加载 classpath 上的驱动;Tomcat 每个应用的 WebappClassLoader 先在本应用中查找,不同应用可以用同一个库的不同版本;热部署用新的类加载器加载修改后的类。
@@ -823,8 +829,8 @@ ThreadLocal 重点看它的数据结构、内存泄漏的原因,以及线程
参考要点
-1. 先看错误信息判断是哪块内存:Java heap space、GC overhead limit exceeded、Metaspace、direct buffer memory、unable to create native thread;元空间、直接内存、线程创建失败分别检查类的生成与卸载、堆外内存的释放、线程数和容器的限制,不能靠增大堆解决;Java heap space 时,启动时加 -XX:+HeapDumpOnOutOfMemoryError 保存堆转储,或用 jcmd GC.heap_dump 导出;用 MAT 按 Retained Heap 排序,查看大对象到 GC Roots 的引用链;引用链不该存在就是内存泄漏,改代码,否则增大堆或减少一次加载的数据。
-2. top 找到 CPU 占用高的 Java 进程;top -H -p 找到占用最高的线程 ID;jstack 导出线程栈,按 nid 找到这个线程(JDK 18 前 nid 是十六进制,要先用 printf "%x" 转换);查看它正在执行的代码,常见是死循环或频繁 GC。
+1. 先看错误信息判断是哪块内存:Java heap space、GC overhead limit exceeded、Metaspace、direct buffer memory、unable to create native thread;元空间、直接内存、线程创建失败分别检查类的生成与卸载、堆外内存的释放、线程数和容器的限制,不能靠增大堆解决;Java heap space 时,启动时加 -XX:+HeapDumpOnOutOfMemoryError 保存堆转储,或用 jcmd \ GC.heap_dump 导出;用 MAT 按 Retained Heap 排序,查看大对象到 GC Roots 的引用链;引用链不该存在就是内存泄漏,改代码,否则增大堆或减少一次加载的数据。
+2. top 找到 CPU 占用高的 Java 进程;top -H -p \ 找到占用最高的线程 ID;jstack \ 导出线程栈,按 nid 找到这个线程(JDK 18 前 nid 是十六进制,要先用 printf "%x" 转换);查看它正在执行的代码,常见是死循环或频繁 GC。
3. 先明确目标(在线服务看最长停顿,批处理看吞吐量),默认用 G1 并开启 GC 日志观察;-Xms 与 -Xmx 设为相同,-Xmx 要给元空间、线程栈、直接内存等堆外内存留出余量,不能等于容器的内存,按需设置 MaxGCPauseMillis、MaxMetaspaceSize、HeapDumpOnOutOfMemoryError;频繁 Full GC 时用 jstat -gcutil 看老年代使用率,降不下来查泄漏,能降下来就增大堆或调整新生代;GC 问题多数是代码造成的,只调参数解决不了。
@@ -1169,7 +1175,7 @@ redo log、undo log 和 binlog 分别用于崩溃恢复、回滚和复制。最
参考要点
1. 惰性删除:访问键时检查,过期就删除并当作不存在;定期删除:每秒 10 次,每次抽 20 个设置了过期时间的键,过期的超过 10% 就继续下一批,每次最多占 25% 的 CPU;内存达到 maxmemory 后按淘汰策略处理,有 volatile 和 allkeys 两个范围的 lru、lfu、random,以及 volatile-ttl 和默认的 noeviction(SET 这类可能增加内存的命令返回 OOM 错误,GET、DEL 照常执行),Redis 4.0 到 7.x 共 8 种,8.6 又增加了按最近修改时间淘汰的 allkeys-lrm、volatile-lrm;只作缓存时常用 allkeys-lru。
-2. RDB 由 fork 出的子进程把某一时刻的全部数据写入紧凑的二进制文件,适合备份、恢复快,但会丢失最后一次快照之后的数据,可能有几分钟;AOF 把写命令追加到文件,everysec 时约丢 1 秒,文件更大,需要定期重写;Redis 7.0 起 AOF 的基础文件默认用 RDB 格式(混合持久化);数据重要时两种都开启,重启时用 AOF 恢复。
+2. RDB 由 fork 出的子进程把某一时刻的全部数据写入紧凑的二进制文件,适合备份、恢复快,但会丢失最后一次快照之后的数据,可能有几分钟;AOF 把写命令追加到文件,everysec 时约丢 1 秒,文件更大,需要定期重写;开启 AOF 后,Redis 5.0 起默认使用混合持久化,重写生成的基础文件用 RDB 格式;数据重要时两种都开启,重启时用 AOF 恢复。
3. Redis 用一个线程执行命令,执行脚本期间不处理其它客户端的命令,整个脚本就像一条命令,可以根据前一条命令的结果决定后续操作,例如先 GET 再判断是否 INCR;这里的原子性是不被打断,不是出错回滚,中途出错时已执行的写命令不会回滚。
@@ -1366,7 +1372,7 @@ redo log、undo log 和 binlog 分别用于崩溃恢复、回滚和复制。最
预期:curl -v 中以 > 开头的是请求行(GET / HTTP/1.1)和请求首部,以 < 开头的是状态行(HTTP/1.1 200 OK)和响应首部,curl -I 只输出状态行和响应首部,没有响应体。
-算法(二叉树,中等):[102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/),题解见[剑指 Offer 32.2](32.2%20把二叉树打印成多行.md)。
+算法(二叉树,简单):[543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/),题解见[两节点的最长路径](Leetcode%20题解%20-%20树.md#3-两节点的最长路径)。
明天:第 39 天,连接管理与 Cookie(HTTP:连接管理~Cookie)。
@@ -1395,7 +1401,7 @@ redo log、undo log 和 binlog 分别用于崩溃恢复、回滚和复制。最
预期:登录后响应中有形如 Set-Cookie: <名称>=<值>; Path=/; Secure; HttpOnly; SameSite=Lax 的首部,保存登录状态的 Cookie 通常带 HttpOnly 和 Secure。
-算法(二叉树,中等):[103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/),题解见[剑指 Offer 32.3](32.3%20按之字形顺序打印二叉树.md)。
+算法(二叉树,中等):[102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/),题解见[剑指 Offer 32.2](32.2%20把二叉树打印成多行.md)。
明天:第 40 天,缓存、WebSocket 与跨域(HTTP:HTTP 缓存~跨域)。
@@ -1420,11 +1426,11 @@ HTTP 缓存分为强缓存和协商缓存。内容协商到通信数据转发几
-动手:用 curl -I 请求一个静态资源(例如 https://www.baidu.com/favicon.ico),记下响应中的 ETag,再用 -H 把它放进 If-None-Match 首部请求一次,观察 304 响应,并与[附录 A2](HTTP.md#a2-强缓存与协商缓存)中 b.js 的第 2 次请求对照。
+动手:用 curl -I 请求一个静态资源(例如 `https://www.baidu.com/favicon.ico`),记下响应中的 ETag,再用 -H 把它放进 If-None-Match 首部请求一次,观察 304 响应,并与[附录 A2](HTTP.md#a2-强缓存与协商缓存)中 b.js 的第 2 次请求对照。
预期:第一次请求返回 200 和 ETag,带上相同的 If-None-Match 再请求返回 304 Not Modified;把 ETag 改动一个字符再请求,返回 200。
-算法(二叉树,中等):[236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/),题解见[二叉树的最近公共祖先](Leetcode%20题解%20-%20树.md#5-二叉树的最近公共祖先)。
+算法(二叉树,中等):[103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/),题解见[剑指 Offer 32.3](32.3%20按之字形顺序打印二叉树.md)。
明天:第 41 天,HTTPS(HTTP:HTTPS)。
@@ -1453,7 +1459,7 @@ HTTPS 在 HTTP 和 TCP 之间加了一层 TLS。按加密、认证、握手的
预期:Protocol 一行为协商出的版本(如 TLSv1.2),Cipher 一行为密码套件(如 ECDHE-RSA-AES128-GCM-SHA256),证书链按 depth=2、1、0 列出根证书、中间证书和网站证书,Verify return code 为 0 (ok)。
-算法(二叉树,中等):[199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/)。
+算法(二叉树,中等):[236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/),题解见[二叉树的最近公共祖先](Leetcode%20题解%20-%20树.md#5-二叉树的最近公共祖先)。
明天:第 42 天,HTTP 的版本、GET 与 POST、DNS(HTTP:HTTP/1.1 新特性~GET 和 POST 比较;计算机网络 - 应用层:域名系统)。
@@ -1482,7 +1488,7 @@ HTTP/2 和 HTTP/3 分别解决了 HTTP/1.1 的哪些问题是常见的追问。G
预期:依次看到根服务器返回顶级域(如 com)服务器的 NS 记录,顶级域服务器返回该域名权威服务器的 NS 记录,权威服务器返回最终的 A 记录(或先返回 CNAME),每条记录都带 TTL。
-算法(二叉树,中等):[105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/),题解见[剑指 Offer 7](7.%20重建二叉树.md)。
+算法(二叉树,中等):[199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/)。
明天:第 43 天,从输入 URL 到显示页面(计算机网络 - 应用层:Web 页面请求过程;攻击技术:跨站脚本攻击~SQL 注入攻击)。
@@ -1507,7 +1513,7 @@ Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、
-算法(二叉树,中等):[98. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/)。
+算法(二叉树,中等):[105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/),题解见[剑指 Offer 7](7.%20重建二叉树.md)。
综合题:在浏览器输入 https://example.com 并回车,按 DNS、TCP、TLS、HTTP 的顺序说出每一步发送了什么,以及在哪些地方可能变慢。(见[Web 页面请求过程](计算机网络%20-%20应用层.md#web-页面请求过程)、[TCP 的三次握手](计算机网络%20-%20传输层.md#tcp-的三次握手)、[TLS 握手](HTTP.md#tls-握手))
@@ -1551,7 +1557,7 @@ Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、
预期:1 万个平台线程启动约 1.4 秒、RSS 增加约 290 MB,1 万个虚拟线程约 30 毫秒、RSS 增加约 40 MB(堆中每个约 2 KB),100 万个虚拟线程约 0.5 秒、堆增加约 690 MB。
-算法(回溯与搜索,中等):[200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/),题解见[矩阵中的连通分量数目](Leetcode%20题解%20-%20搜索.md#2-矩阵中的连通分量数目)。
+算法(二叉树,中等):[98. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/)。
明天:第 45 天,系统调用、进程同步与通信(计算机操作系统 - 概述:系统调用;计算机操作系统 - 进程管理:进程调度算法~进程通信)。
@@ -1580,7 +1586,7 @@ Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、
预期:普通函数调用约 1 ns,glibc 的 getppid() 约 167 ns、直接陷入约 165 ns,系统调用约为函数调用的近 180 倍,经 vDSO 的 clock_gettime 约 18 ns,绕过 vDSO 约 202 ns。
-算法(回溯与搜索,中等):[46. 全排列](https://leetcode.cn/problems/permutations/),题解见[排列](Leetcode%20题解%20-%20搜索.md#5-排列)。
+算法(回溯与搜索,中等):[200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/),题解见[矩阵中的连通分量数目](Leetcode%20题解%20-%20搜索.md#2-矩阵中的连通分量数目)。
明天:第 46 天,死锁(计算机操作系统 - 死锁:必要条件~程序中的死锁)。
@@ -1601,7 +1607,7 @@ Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、
1. 互斥:资源要么已分配给一个进程,要么可用;占有和等待:已持有资源的进程还能请求新资源;不可抢占:资源只能由持有者显式释放;环路等待:多个进程形成环,每个都在等下一个持有的资源。
2. 设计时破坏任一必要条件:一次申请全部资源(破坏占有和等待,利用率低)、申请失败就释放已有资源(破坏不可抢占,如 tryLock 超时)、资源编号后按顺序加锁(破坏环路等待);最常用的是按固定顺序加锁,代价最小,例如转账时总是先锁 id 小的账户。
-3. 用阻塞式的锁时,死锁线程几乎不耗 CPU(例如等待 synchronized 的线程处于 BLOCKED),现象是 CPU 低而请求全部卡住;用 jstack 或 jcmd Thread.print 打印线程栈,末尾会报告 Found one Java-level deadlock,列出各线程等待的锁和持有者;程序中也可以定时调用 ThreadMXBean.findDeadlockedThreads() 检测并告警,但它检测不到包含虚拟线程的死锁环和 Semaphore 这类不可重入锁的自死锁;JVM 不会自动解除死锁,卡在 synchronized 上的线程无法中断,通常只能重启,所以要靠固定加锁顺序或 tryLock 预防。
+3. 用阻塞式的锁时,死锁线程几乎不耗 CPU(例如等待 synchronized 的线程处于 BLOCKED),现象是 CPU 低而请求全部卡住;用 jstack 或 jcmd \ Thread.print 打印线程栈,末尾会报告 Found one Java-level deadlock,列出各线程等待的锁和持有者;程序中也可以定时调用 ThreadMXBean.findDeadlockedThreads() 检测并告警,但它检测不到包含虚拟线程的死锁环和 Semaphore 这类不可重入锁的自死锁;JVM 不会自动解除死锁,卡在 synchronized 上的线程无法中断,通常只能重启,所以要靠固定加锁顺序或 tryLock 预防。
@@ -1609,7 +1615,7 @@ Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、
预期:原写法每次都卡住(stuck),jstack 末尾报告 Found one Java-level deadlock:T1 等待的锁被 T2 持有,T2 等待的锁被 T1 持有,两者都是 BLOCKED,改为按 id 顺序加锁后都能完成,两个余额仍为 1000。
-算法(回溯与搜索,中等):[93. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/),题解见[IP 地址划分](Leetcode%20题解%20-%20搜索.md#2-ip-地址划分)。
+算法(回溯与搜索,中等):[46. 全排列](https://leetcode.cn/problems/permutations/),题解见[排列](Leetcode%20题解%20-%20搜索.md#5-排列)。
明天:第 47 天,内存管理(计算机操作系统 - 内存管理:虚拟内存~分页与分段的比较)。
@@ -1634,11 +1640,11 @@ Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、
-动手:运行[附录 A3](计算机操作系统%20-%20内存管理.md#a3-进程的地址空间布局)的代码,查看 /proc//maps 中代码段、堆和栈的位置。
+动手:运行[附录 A3](计算机操作系统%20-%20内存管理.md#a3-进程的地址空间布局)的代码,查看 /proc/\/maps 中代码段、堆和栈的位置。
预期:maps 中 r-xp 的可执行文件区域是代码段,之后 rw-p 的是数据段和 BSS,[heap] 紧随其后,1 MB 的 malloc 和共享库在中间的映射区,[stack] 在最高处,再运行一次起始地址都变,上下顺序不变。
-算法(回溯与搜索,中等):[22. 括号生成](https://leetcode.cn/problems/generate-parentheses/),题解见[括号生成](Leetcode%20题解%20-%20搜索.md#16-括号生成)。
+算法(回溯与搜索,中等):[93. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/),题解见[IP 地址划分](Leetcode%20题解%20-%20搜索.md#2-ip-地址划分)。
明天:第 48 天,I/O 多路复用(Socket:I/O 模型~I/O 复用)。
@@ -1667,7 +1673,7 @@ Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、
预期:LT 下 3 次 epoll_wait 依次读到 1024、1024、952 字节后第 4 次超时,ET 每次只读一次时第 2 次 epoll_wait 就超时、剩 1976 字节,ET 循环读时一次事件内读完 1024、1024、952 字节后返回 EAGAIN。
-算法(回溯与搜索,中等):[78. 子集](https://leetcode.cn/problems/subsets/),题解见[子集](Leetcode%20题解%20-%20搜索.md#11-子集)。
+算法(回溯与搜索,中等):[22. 括号生成](https://leetcode.cn/problems/generate-parentheses/),题解见[括号生成](Leetcode%20题解%20-%20搜索.md#16-括号生成)。
综合题:一个 Java 服务要同时维持 10 万个长连接:用每个连接一个线程的方式会遇到什么问题?用 epoll 或虚拟线程怎样解决?(见[上下文切换](计算机操作系统%20-%20进程管理.md#4-上下文切换)、[epoll](Socket.md#epoll)、[协程](计算机操作系统%20-%20进程管理.md#5-协程))
@@ -1710,7 +1716,7 @@ IoC 一节先说明为什么需要容器。Bean 的生命周期要能对照调
预期:构造函数的栈中 doCreateBean 调用 createBeanInstance、经 Constructor.newInstance 反射创建对象,Setter 的栈中 doCreateBean 调用 populateBean、经 postProcessProperties 和 Method.invoke 注入,构造函数先打印,即先实例化再注入。
-算法(回溯与搜索,中等):[39. 组合总和](https://leetcode.cn/problems/combination-sum/),题解见[组合求和](Leetcode%20题解%20-%20搜索.md#8-组合求和)。
+算法(回溯与搜索,中等):[78. 子集](https://leetcode.cn/problems/subsets/),题解见[子集](Leetcode%20题解%20-%20搜索.md#11-子集)。
明天:第 50 天,AOP 与事务(Spring:AOP~事务)。
@@ -1739,7 +1745,7 @@ AOP 基于动态代理,自调用时切面不生效的原因也在这里。事
预期:从外部调用 a(),b() 写入数据后抛出 RuntimeException,数据没有回滚,把 b() 移到另一个 Bean(或通过注入的自身代理调用)后,同样的异常使写入被回滚。
-算法(动态规划,简单):[121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/),题解见[买卖股票最大的收益](Leetcode%20题解%20-%20贪心思想.md#5-买卖股票最大的收益)。
+算法(回溯与搜索,中等):[39. 组合总和](https://leetcode.cn/problems/combination-sum/),题解见[组合求和](Leetcode%20题解%20-%20搜索.md#8-组合求和)。
明天:第 51 天,Spring Boot、Spring MVC 与 MyBatis(Spring:Spring Boot 自动配置~Spring 中的设计模式)。
@@ -1768,13 +1774,13 @@ AOP 基于动态代理,自调用时切面不生效的原因也在这里。事
预期:CONDITIONS EVALUATION REPORT 中,没有 JDBC 相关依赖时 DataSourceAutoConfiguration 在 Negative matches 中,原因是 @ConditionalOnClass 找不到所需的类,引入 spring-boot-starter-jdbc 和数据库驱动后出现在 Positive matches 中。
-算法(动态规划,简单):[70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/),题解见[爬楼梯](Leetcode%20题解%20-%20动态规划.md#1-爬楼梯)。
+算法(动态规划,简单):[121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/),题解见[买卖股票最大的收益](Leetcode%20题解%20-%20贪心思想.md#5-买卖股票最大的收益)。
明天:第 52 天,设计模式(设计模式 - 目录:设计模式前言;设计模式 - 单例:单例(Singleton);设计模式 - 策略:策略(Strategy);面向对象思想:设计原则)。
### 第 52 天:设计模式
-先看前言中下单流程用到的几种模式,再读单例和策略两篇。设计原则说明了这些模式背后的目标。
+先看前言中下单流程用到的几种模式,再读单例和策略两篇。单例的几种写法先比较线程安全和创建时机的差别,双重校验锁逐行细读,其余代码看懂结构即可;策略一篇重点看用策略替代 if-else 前后的对比。设计原则说明了这些模式背后的目标。
阅读:《设计模式 - 目录》的[设计模式前言](设计模式%20-%20目录.md#一前言);《设计模式 - 单例》的[单例(Singleton)](设计模式%20%20-%20单例.md#单例singleton);《设计模式 - 策略》的[策略(Strategy)](设计模式%20-%20策略.md#9-策略strategy);《面向对象思想》的[设计原则](面向对象思想.md#三设计原则)。选读:[工厂方法](设计模式%20-%20工厂方法.md)、[代理](设计模式%20-%20代理.md)、[装饰](设计模式%20-%20装饰.md)、[观察者](设计模式%20-%20观察者.md)、[模板方法](设计模式%20-%20模板方法.md)、[责任链](设计模式%20-%20责任链.md)。
@@ -1797,7 +1803,7 @@ AOP 基于动态代理,自调用时切面不生效的原因也在这里。事
预期:每种支付方式一个 PayStrategy 实现类,用 @Component("WECHAT") 这样的名称标注,注入的 Map 以 Bean 名称为 key,按支付方式取出策略调用、未知方式抛异常,结果与原 if-else 一致,新增支付方式只需加一个类。
-算法(动态规划,中等):[53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/),题解见[子数组最大的和](Leetcode%20题解%20-%20贪心思想.md#10-子数组最大的和)。
+算法(动态规划,简单):[70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/),题解见[爬楼梯](Leetcode%20题解%20-%20动态规划.md#1-爬楼梯)。
综合题:用户下单接口中,Controller 调用 Service,Service 带有 @Transactional 并调用了日志切面:从请求到达 DispatcherServlet 到事务提交,依次经过了哪些对象和代理?(见[请求的处理过程](Spring.md#请求的处理过程)、[动态代理](Spring.md#动态代理)、[事务的实现原理](Spring.md#事务的实现原理))
@@ -1837,7 +1843,7 @@ AOP 基于动态代理,自调用时切面不生效的原因也在这里。事
-算法(动态规划,中等):[5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/),题解见[最长回文子串](Leetcode%20题解%20-%20字符串.md#10-最长回文子串)。
+算法(动态规划,中等):[53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/),题解见[子数组最大的和](Leetcode%20题解%20-%20贪心思想.md#10-子数组最大的和)。
明天:第 54 天,分布式锁(分布式:分布式锁)。
@@ -1866,7 +1872,7 @@ AOP 基于动态代理,自调用时切面不生效的原因也在这里。事
预期:lock() 后 PTTL 约 30000 并逐渐减小,约每 10 秒被重置回 30 秒附近,持有 30 秒锁一直存在,lock(10s) 不续期、约 10 秒后键消失,进程崩溃后约 30 秒过期,其它客户端能拿到锁。
-算法(动态规划,中等):[300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/),题解见[最长递增子序列](Leetcode%20题解%20-%20动态规划.md#1-最长递增子序列)。
+算法(动态规划,中等):[5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/),题解见[最长回文子串](Leetcode%20题解%20-%20字符串.md#10-最长回文子串)。
明天:第 55 天,分布式事务与 CAP(分布式:分布式事务~BASE)。
@@ -1891,7 +1897,7 @@ AOP 基于动态代理,自调用时切面不生效的原因也在这里。事
-算法(动态规划,中等):[1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/),题解见[最长公共子序列](Leetcode%20题解%20-%20动态规划.md#1-最长公共子序列)。
+算法(动态规划,中等):[300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/),题解见[最长递增子序列](Leetcode%20题解%20-%20动态规划.md#1-最长递增子序列)。
明天:第 56 天,RPC、分布式 ID 与幂等(分布式:RPC~幂等)。
@@ -1920,7 +1926,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:每轮 10 个线程并发提交同一个 request_id,先查后插的写法每轮创建 2~3 个订单,直接 INSERT 依靠唯一索引时每轮只创建 1 个,其余 9 次报错 1062。
-算法(动态规划,中等):[322. 零钱兑换](https://leetcode.cn/problems/coin-change/),题解见[找零钱的最少硬币数](Leetcode%20题解%20-%20动态规划.md#4-找零钱的最少硬币数)。
+算法(动态规划,中等):[1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/),题解见[最长公共子序列](Leetcode%20题解%20-%20动态规划.md#1-最长公共子序列)。
明天:第 57 天,消息队列的作用与可靠性(消息队列:消息模型~消息队列的可靠性)。
@@ -1949,7 +1955,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:消费者完成 3 笔扣款后、自动提交偏移量(默认每 5 秒)之前被 kill -9,组的偏移量仍为空,约 45 秒后重启的消费者又收到这 3 条消息、重复扣款,去重方法是在同一个本地事务中先插入去重表(键如 order:1024:pay)再扣款,重复消息报 1062 后回滚并直接确认。
-算法(动态规划,中等):[64. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/),题解见[矩阵的最小路径和](Leetcode%20题解%20-%20动态规划.md#1-矩阵的最小路径和)。
+算法(动态规划,中等):[322. 零钱兑换](https://leetcode.cn/problems/coin-change/),题解见[找零钱的最少硬币数](Leetcode%20题解%20-%20动态规划.md#4-找零钱的最少硬币数)。
明天:第 58 天,顺序、积压与延迟消息(消息队列:顺序性~延迟消息与死信队列)。
@@ -1978,7 +1984,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:分区 0、1、2 分别积压 9237、9577、8186 条,LAG = LOG-END-OFFSET - CURRENT-OFFSET(日志末尾偏移量减去组已提交的偏移量),例如 11186 - 3000 = 8186。
-算法(动态规划,中等):[198. 打家劫舍](https://leetcode.cn/problems/house-robber/),题解见[强盗抢劫](Leetcode%20题解%20-%20动态规划.md#2-强盗抢劫)。
+算法(动态规划,中等):[64. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/),题解见[矩阵的最小路径和](Leetcode%20题解%20-%20动态规划.md#1-矩阵的最小路径和)。
明天:第 59 天,Kafka(消息队列:Kafka)。
@@ -2007,7 +2013,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:默认配置(批量)每秒约 5.7 万~6.8 万条、平均延迟 21~54 ms;batch.size=0、linger.ms=0 关闭批量后每秒只有约 6500~6800 条,平均延迟升到约 1.4 秒;原因是每条消息单独成为一个批次,网络请求、写日志和副本复制的次数大幅增加,发不出去的请求在生产者端排队,所以吞吐下降的同时延迟也升高。
-算法(双指针,困难):[42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/),题解见[接雨水](Leetcode%20题解%20-%20双指针.md#9-接雨水)。
+算法(动态规划,中等):[62. 不同路径](https://leetcode.cn/problems/unique-paths/),题解见[矩阵的总路径数](Leetcode%20题解%20-%20动态规划.md#2-矩阵的总路径数)。
综合题:下单后需要扣库存、发优惠券、发短信:哪些步骤放在同一个事务中,哪些通过消息队列异步完成?消息丢失或重复时分别会怎样?(见[异步处理](消息队列.md#异步处理)、[本地消息表](分布式.md#本地消息表)、[幂等](分布式.md#九幂等))
@@ -2051,7 +2057,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:新建后连续 acquire() 第 1 次不等待、之后每次等待约 0.2 秒,空闲 2 秒后前 6 次都不等待、第 7 次等待约 0.2 秒,令牌不足时 tryAcquire() 立即返回 false。
-算法(滑动窗口,困难):[239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/),题解见[剑指 Offer 59](59.%20滑动窗口的最大值.md)。
+算法(动态规划,中等):[198. 打家劫舍](https://leetcode.cn/problems/house-robber/),题解见[强盗抢劫](Leetcode%20题解%20-%20动态规划.md#2-强盗抢劫)。
明天:第 61 天,秒杀与抢红包(系统设计基础:秒杀系统)。
@@ -2080,7 +2086,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:Lua 脚本成功 100、库存不足 1800、重复购买 100、剩余库存 0,没有超卖;先 GET 再 DECR 时成功 106~114 次、剩余库存 -6~-14,出现超卖。
-算法(滑动窗口,困难):[76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/),题解见[最小覆盖子串](Leetcode%20题解%20-%20双指针.md#10-最小覆盖子串)。
+算法(双指针,困难):[42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/),题解见[接雨水](Leetcode%20题解%20-%20双指针.md#9-接雨水)。
明天:第 62 天,海量数据、短链与 Feed 流(系统设计基础:海量数据处理~Feed 流)。
@@ -2109,7 +2115,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:301 的响应被浏览器缓存,第二次访问 /a 时浏览器直接请求 /target,服务器只收到一次 /a,而 302 的 /b 两次都到达服务器,所以 301 会漏掉后续点击,需要统计点击量时用 302。
-算法(链表,困难):[25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/),题解见[K 个一组翻转链表](Leetcode%20题解%20-%20链表.md#11-k-个一组翻转链表)。
+算法(滑动窗口,困难):[239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/),题解见[剑指 Offer 59](59.%20滑动窗口的最大值.md)。
明天:第 63 天,高并发系统的设计(系统设计基础:高并发系统的设计)。
@@ -2134,7 +2140,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
-算法(链表,困难):[23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/),题解见[合并 K 个有序链表](Leetcode%20题解%20-%20链表.md#13-合并-k-个有序链表)。
+算法(滑动窗口,困难):[76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/),题解见[最小覆盖子串](Leetcode%20题解%20-%20双指针.md#10-最小覆盖子串)。
明天:第 64 天,Docker(Docker:解决的问题~镜像与容器)。
@@ -2163,7 +2169,7 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;
预期:多阶段构建的镜像只含 JRE 和 jar,约 236 MB,在 Maven 镜像中单阶段构建并运行的镜像约 680 MB,多出了 JDK、Maven 和下载的依赖。
-算法(栈和队列,困难):[32. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/),题解见[最长有效括号](Leetcode%20题解%20-%20动态规划.md#3-最长有效括号)。
+算法(链表,困难):[25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/),题解见[K 个一组翻转链表](Leetcode%20题解%20-%20链表.md#11-k-个一组翻转链表)。
明天:第 65 天,Kubernetes(Docker:Kubernetes)。
@@ -2188,7 +2194,7 @@ Kubernetes 重点看 Pod 的创建过程、Service 和 Ingress 的作用,以
-算法(二分查找,困难):[4. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/),题解见[两个有序数组的中位数](Leetcode%20题解%20-%20二分查找.md#7-两个有序数组的中位数)。
+算法(链表,困难):[23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/),题解见[合并 K 个有序链表](Leetcode%20题解%20-%20链表.md#13-合并-k-个有序链表)。
明天:第 66 天,系统设计题的思路与即时通讯(系统设计基础:系统设计题的思路~即时通讯)。
@@ -2217,7 +2223,7 @@ Kubernetes 重点看 Pod 的创建过程、Service 和 Ingress 的作用,以
预期:第二次 INSERT 报错 ERROR 1062 Duplicate entry '...' for key 'video_like.PRIMARY';INSERT IGNORE 不报错,影响 0 行;EXPLAIN 中 type 为 ref,按 user_id 等值查找,扫描的行数约等于这个用户点过赞的视频数。
-算法(二叉树,困难):[124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/)。
+算法(栈和队列,困难):[32. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/),题解见[最长有效括号](Leetcode%20题解%20-%20动态规划.md#3-最长有效括号)。
明天:第 67 天,订单与支付(系统设计基础:订单与支付)。
@@ -2246,7 +2252,7 @@ Kubernetes 重点看 Pod 的创建过程、Service 和 Ingress 的作用,以
预期:只改本地状态的写法,到期时把订单改为已关闭,但平台上的交易仍然可以支付,用户在关单前后付款时就出现平台已付、本地已关(300 轮中二百多轮),只能退款;先向平台关单、失败只等回调的写法,关单因用户已付款而失败后只等回调,回调又有 10% 丢失,订单就停在待支付(300 轮中二十多轮),要靠查单或对账补上;按三步的写法关单失败时查单,300 轮全部一致。
-算法(动态规划,困难):[72. 编辑距离](https://leetcode.cn/problems/edit-distance/),题解见[编辑距离](Leetcode%20题解%20-%20动态规划.md#2-编辑距离)。
+算法(二分查找,困难):[4. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/),题解见[两个有序数组的中位数](Leetcode%20题解%20-%20二分查找.md#7-两个有序数组的中位数)。
综合题:设计一个秒杀系统:从页面、网关、服务、缓存、消息队列到数据库,每一层分别做什么来扛住流量、防止超卖?出现故障时怎样降级?(见[秒杀系统](系统设计基础.md#六秒杀系统)、[读路径的逐层分流](系统设计基础.md#读路径的逐层分流)、[限流、降级与熔断](系统设计基础.md#限流降级与熔断))
@@ -2260,3 +2266,76 @@ Kubernetes 重点看 Pod 的创建过程、Service 和 Ingress 的作用,以
5. 降级:依赖持续失败时熔断,库存服务被熔断时详情页显示“有货”、下单以数据库为准,关闭评论等非核心功能,无法降级时快速失败提示稍后再试。
+
+明天:第 68 天,RAG 与 Agent(大模型应用:RAG 的流程;大模型应用:Agent 是什么~Agent 与工作流)。
+
+## 十五、大模型应用
+
+### 第 68 天:RAG 与 Agent
+
+后端面试中的大模型问题大多停留在概念上,这两天只读最常被问到的部分,想深入的读者转到大模型应用开发路线。RAG 只读流程一节,记住“先检索资料、再让模型按资料回答”这条主线;Agent 的四节要细读,重点是模型始终只输出文本、工具由应用代码执行,以及什么时候用工作流就够了。
+
+阅读:《大模型应用》的[RAG 的流程](大模型应用.md#流程);《大模型应用》从[Agent 是什么](大模型应用.md#agent-是什么)读到[Agent 与工作流](大模型应用.md#agent-与工作流)。选读:[混合检索](大模型应用.md#混合检索)、[重排](大模型应用.md#重排)。
+
+自测:
+
+1. RAG 的完整流程是什么?它为什么能减少幻觉?(见[RAG 的流程](大模型应用.md#流程))
+2. Agent 和普通的大模型调用有什么区别?模型是怎样调用工具的?(见[Agent 是什么](大模型应用.md#agent-是什么)、[工具调用](大模型应用.md#工具调用))
+3. 什么时候用工作流,什么时候用 Agent?(见[Agent 与工作流](大模型应用.md#agent-与工作流))
+
+
+参考要点
+
+1. 离线阶段解析文档、切成小段,用 Embedding 模型把每段转为向量存入向量数据库,同时建立关键词索引;在线阶段先改写问题,向量检索和关键词检索同时召回候选,合并后重排,把最相关的几段资料和问题一起放进提示词,生成带引用的回答;能减少幻觉,是因为模型从凭记忆回答变为根据给定的资料回答,每个结论都能按编号核对;但资料没找到或找错时模型仍会编造,所以要用重排分数和相似度阈值判断有没有答案,没有时改写问题再检索、向用户追问,或者如实回答没找到。
+2. 普通调用是一问一答,模型根据输入生成文本,不能自行查询数据库或执行操作,外部资料要由应用放进输入;Agent 由模型决定下一步调用哪个工具,应用代码执行后把结果交还模型,如此循环直到任务完成;工具调用的过程:请求中附上工具的名称、用途和参数的 JSON Schema,推理服务按对话模板把它们拼进提示词,模型生成一段特定格式的文本,推理服务解析成 tool_calls 返回,应用执行后用 tool 角色的消息把结果发回;模型始终只输出文本,可能写错参数或编造订单号,所以执行前要校验,循环要限制最大步数。
+3. 工作流的步骤和顺序由代码事先写好,可预测、成本可控,适合步骤固定的业务;Agent 的下一步由模型临时决定,适合事先确定不了步骤的开放任务,代价是成本更高、错误会逐步累积,每步成功率 95% 时连续 10 步都成功只有约 60%;判断标准是步骤和步骤之间的转移规则能否事先写清;实际系统常把两者组合:先用意图路由分流,退款这类涉及资金、步骤固定的流程写成工作流,开放的咨询交给 Agent。
+
+
+
+动手:参照[Agent 循环](大模型应用.md#agent-循环)中并发执行工具调用的片段和[附录 A25](大模型应用.md#a25-并发执行工具调用)的预期输出,在 JDK 21 中自行定义 ToolCall 记录、每次休眠 500 毫秒的模拟查询方法和线程池,分别串行、并发执行两个查询订单的调用,比较耗时。
+
+预期:串行执行约 1000 毫秒(两次各休眠 500 毫秒),并发执行约 520 毫秒(附录 A25 中为 519 毫秒);两条 tool 消息的 toolCallId 分别为 call_1 和 call_2,与模型给出的调用一一对应。
+
+算法(二叉树,困难):[124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/),题解见[二叉树中的最大路径和](Leetcode%20题解%20-%20树.md#15-二叉树中的最大路径和)。
+
+明天:第 69 天,MCP、Skills 与 AI 编程助手(大模型应用:MCP~Skills;大模型应用:AI 编程助手)。
+
+### 第 69 天:MCP、Skills 与 AI 编程助手
+
+MCP 和 Skills 常被拿来和工具调用比较:工具是模型能调用的函数,MCP 解决工具怎样接入应用,Skill 告诉 Agent 怎样完成一类任务。AI 编程助手一节把前一天的 Agent 循环落到日常开发上,后半部分的使用方法对应“你平时怎么用 AI 写代码”这类问题,回答时要结合自己的习惯。
+
+阅读:《大模型应用》从[MCP](大模型应用.md#mcp)读到[Skills](大模型应用.md#skills);《大模型应用》的[AI 编程助手](大模型应用.md#ai-编程助手)。选读:[Harness](大模型应用.md#harness)。
+
+自测:
+
+1. MCP 解决了什么问题?它和 Function Calling 有什么区别?(见[MCP](大模型应用.md#mcp))
+2. Skill 是什么?它和工具、MCP 有什么区别?(见[Skills](大模型应用.md#skills))
+3. 你平时怎样用 AI 编程工具?怎样保证生成的代码质量?(见[AI 编程助手](大模型应用.md#ai-编程助手))
+
+
+参考要点
+
+1. 同一个工具要接入多个 AI 应用时,原本要在每个应用里分别适配;MCP 定义了 AI 应用与外部工具、数据之间的统一接口,工具实现一次服务端就能被所有支持 MCP 的应用使用;三种角色是主机、客户端(一个客户端只连接一个服务端)和服务端,服务端提供工具、资源和提示词模板,本地用 stdio 通信,远程用 Streamable HTTP;Function Calling 是模型一侧的能力,模型根据工具描述给出调用,MCP 是应用与工具之间的协议,解决工具怎样被发现和调用,两者配合使用:主机把服务端的工具转换成工具定义交给模型,模型给出调用后再通过 MCP 发给服务端执行;代价是工具定义都会占用上下文,工具多时要按需加载。
+2. Skill 是一个包含 SKILL.md、脚本和参考资料的文件夹,教 Agent 完成某一类任务;核心是渐进式加载:平时只把名称和描述放进上下文(每个约 100 Token),模型判断与当前任务相关时才读入正文,正文引用的文档和脚本需要时才读取或执行;工具是可调用的函数,提供原子能力,MCP 解决工具怎样接入,Skill 提供做事的步骤和规范,执行时仍要调用工具;需要接入外部系统、要求权限控制的能力做成 MCP 服务端,组合已有工具的流程和规范写成 Skill。
+3. 给出可以验证的目标,例如让某个测试通过,修 bug 时先写一个能复现问题的测试;先让它阅读代码、给出方案,确认后再修改;任务拆小,一次只改一个功能,便于审查;生成的代码同样要做代码审查和测试,测试用例按需求写,由人检查边界;反复纠正的要求写进 CLAUDE.md、AGENTS.md 这类项目约定文件;AI 生成的代码占多大比例不是关键,开发者仍要负责需求、方案、审查和上线的结果,涉及资金和数据安全的修改每一处都要审查。
+
+
+
+动手:阅读[附录 A24](大模型应用.md#a24-mcp-服务端的最小实现)的输出,说明工具抛出 ToolError 和普通异常时模型分别收到什么,参数类型不符合 Schema 时工具函数会不会执行。
+
+预期:工具抛出 ToolError 时,错误信息作为 isError 为 true 的结果返回给模型,例如“Error executing tool get_order: 订单 9999 不存在,请向用户确认订单号”,模型可以据此向用户确认;换成普通的 ValueError 后,模型只收到 Error executing tool get_order,具体内容只在服务端的日志中;参数不符合 Schema 时(例如 order_id 传成数字 1024、缺少参数或参数名写错),SDK 先按 Schema 校验并返回 validation error,isError 为 true,工具函数不会执行。
+
+算法(动态规划,困难):[72. 编辑距离](https://leetcode.cn/problems/edit-distance/),题解见[编辑距离](Leetcode%20题解%20-%20动态规划.md#2-编辑距离)。
+
+综合题:要在订单系统中加一个客服助手,既能回答“订单到哪了”,也能办理退款,你会怎样设计?哪些事情交给模型,哪些必须由代码控制?(见[设计一个客服 Agent](大模型应用.md#设计一个客服-agent)、[Agent 与工作流](大模型应用.md#agent-与工作流)、[支付回调与重复支付](系统设计基础.md#支付回调与重复支付))
+
+
+答题思路
+
+1. 接入与权限:从登录会话中取出 user_id,工具都用它鉴权,不作为参数让模型填写,用户只能查自己的订单。
+2. 路由:退款、改地址这类涉及资金、步骤固定的请求交给工作流,咨询类交给 Agent,置信度低时反问澄清。
+3. Agent:一个 ReAct 循环,工具有查询订单、查询物流、检索知识库和创建工单,参数先校验再执行,限制最大步数。
+4. 退款:由代码按校验、生成申请、用户确认、执行的顺序处理,模型只负责从对话中提取订单号和原因;退款单号作为幂等键,超时重试沿用原单号,不会退两次。
+5. 上线:用历史会话做评测集,写操作检查数据库的最终状态;先只上线查询类,退款先由人工客服确认;主模型出错时切到备用模型,再不行退回固定答案并转人工。
+
+
diff --git "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \345\244\247\346\250\241\345\236\213\345\272\224\347\224\250\345\274\200\345\217\221.md" "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \345\244\247\346\250\241\345\236\213\345\272\224\347\224\250\345\274\200\345\217\221.md"
index d1f6c03ac..ed32fedf5 100644
--- "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \345\244\247\346\250\241\345\236\213\345\272\224\347\224\250\345\274\200\345\217\221.md"
+++ "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \345\244\247\346\250\241\345\236\213\345\272\224\347\224\250\345\274\200\345\217\221.md"
@@ -215,7 +215,7 @@ Token 是模型处理文本的单位,也是计费和上下文长度的单位
参考要点
-1. LoRA 冻结原矩阵 W,只训练小矩阵 B、A,h = Wx + (α / r)·BAx,B 初始化为 0,训练开始时与原模型相同;可训练参数很少(Qwen2.5-7B 全部线性层 r = 16 时占 0.53%),训练后合并进 W,推理无额外开销;QLoRA 再把冻结的权重量化为 4 位 NF4 存储、计算时还原为 BF16,7B 权重从 15.2 GB 降到约 4 GB。
+1. LoRA 冻结原矩阵 W,只训练小矩阵 B、A,h = Wx + (α / r)·BAx,B 初始化为 0,训练开始时与原模型相同;可训练参数很少(Qwen2.5-7B 全部线性层 r = 16 时占 0.53%),训练后合并进 W,推理无额外开销;QLoRA 再把冻结的权重量化为 4 位 NF4 存储、计算时还原为 BF16,7B 权重从 15.2 GB 降到约 5.5 GB(嵌入层和输出层仍为 BF16)。
2. 需要私有资料或经常更新的知识、要给出依据时用 RAG;要固定风格和格式、学会某种专业任务,提示词仍常出错时用微调;只是补少量规则和示例时用提示词;一般先提示词、再 RAG、最后微调,写进参数的知识难更新、无法给出来源。
3. 模型在新数据上训练后原有能力明显下降,例如只用客服对话微调后写代码变差,1B~7B 模型的实验中模型越大越严重;缓解:混入通用指令数据,用 LoRA、较小的学习率和较少的轮数并早停,训练前后都在通用评测集上评测。
@@ -580,7 +580,7 @@ Embedding 把文本变成向量,相似度检索靠它完成。向量索引回
参考要点
1. 短期记忆是当前会话的原始消息(含工具调用和结果)、摘要和任务状态,常以会话 ID 为键存在 Redis,请求时放入“摘要 + 最近几轮 + 任务状态”;长期记忆由后台提取或模型调用 save_memory 写入,只写用户明确说过、以后有用、业务系统查不到的事实;使用时按用户 ID 过滤,按相似度和时间检索最相关的几条。
-2. 写入前检索相似的旧记忆,由模型选择 ADD、UPDATE、DELETE 或 NOOP,用户 ID、时间等字段由代码填写;冲突时用户本次说的优先,亲口说的优先于推断,同一来源以新的为准,只对这一次有效的说法不覆盖,仍无法判断就问用户;旧记忆标记失效而不物理删除,并按时间衰减排序、定期清理。
+2. 写入前检索相似的旧记忆,由模型选择 ADD、UPDATE、DELETE 或 NOOP(Mem0 论文的做法,开源版后来改为只新增),用户 ID、时间等字段由代码填写;冲突时用户本次说的优先,亲口说的优先于推断,同一来源以新的为准,只对这一次有效的说法不覆盖,仍无法判断就问用户;旧记忆标记失效而不物理删除,并按时间衰减排序、定期清理。
3. 完整历史在上下文中时模型一般能理解指代,问题出在检索、压缩和多个候选;检索前结合最近几轮和任务状态改写成完整的问题,例如“它什么时候发货”改为“订单 1026 什么时候发货”;任务状态记录当前和提到过的订单,有多个候选时追问而不是猜。
diff --git "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\233\266\345\237\272\347\241\200.md" "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\233\266\345\237\272\347\241\200.md"
index faebfcb7e..71d657dd1 100644
--- "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\233\266\345\237\272\347\241\200.md"
+++ "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\233\266\345\237\272\347\241\200.md"
@@ -635,7 +635,7 @@ IP 地址不好记,而且服务器换了机器地址就会变,所以 App 中
DNS 是一个分布在全世界的查询系统。手机先问本地的 DNS 服务器,通常是家里的路由器,它一般把问题转给运营商的 DNS 服务器。运营商的服务器如果没有缓存答案,就从根服务器开始,依次询问负责 .com 的服务器、负责 example.com 的服务器,最终得到 IP 地址,并缓存一段时间供下次使用。
-用 dig 命令查询 www.baidu.com(macOS 实测,见[附录 A6](#a6-dns-与往返时间)):
+用 dig 命令查询 `www.baidu.com`(macOS 实测,见[附录 A6](#a6-dns-与往返时间)):
```bash
dig www.baidu.com
@@ -821,7 +821,7 @@ dns 0.002281 tcp 0.011717 tls 0.041322 first_byte 0.053568
-动手:执行 dig www.baidu.com 和 ping -c 4 www.baidu.com,找出解析得到的 IP 地址、记录的有效期和往返时间。
+动手:执行 `dig www.baidu.com` 和 `ping -c 4 www.baidu.com`,找出解析得到的 IP 地址、记录的有效期和往返时间。
预期:dig 的 ANSWER SECTION 中,www.baidu.com 是 www.a.shifen.com 的别名(CNAME),A 记录给出 IP 地址(笔记实测 183.2.172.177),有效期 600 秒;ping 显示往返时间,笔记在家庭宽带上实测平均约 9.7 ms,具体数字随网络和地区不同。
@@ -846,7 +846,7 @@ dns 0.002281 tcp 0.011717 tls 0.041322 first_byte 0.053568
-动手:在一个终端中执行 sudo tcpdump -nn -S 'tcp port 80' 开始抓包(sudo 会要求输入当前用户的登录密码,-S 显示原始序号),在另一个终端中执行 curl http://example.com,然后回到第一个终端按 Ctrl + C 停止,找出三次握手的 3 个报文,对照[TCP](#tcp)一节中的图核对序号和确认号的关系。
+动手:在一个终端中执行 sudo tcpdump -nn -S 'tcp port 80' 开始抓包(sudo 会要求输入当前用户的登录密码,-S 显示原始序号),在另一个终端中执行 `curl http://example.com`,然后回到第一个终端按 Ctrl + C 停止,找出三次握手的 3 个报文,对照[TCP](#tcp)一节中的图核对序号和确认号的关系。
预期:前 3 个报文依次是 SYN、SYN + ACK、ACK(tcpdump 中标为 [S]、[S.]、[.]):第 2 个报文的确认号等于第 1 个的序号加 1,第 3 个报文的确认号等于服务器的起始序号加 1;不加 -S 时 tcpdump 显示相对序号(例如 ack 1)。
diff --git "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\235\242\350\257\225\345\206\262\345\210\272.md" "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\235\242\350\257\225\345\206\262\345\210\272.md"
index a18f5ac23..bb023d319 100644
--- "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\235\242\350\257\225\345\206\262\345\210\272.md"
+++ "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - \351\235\242\350\257\225\345\206\262\345\210\272.md"
@@ -31,6 +31,12 @@
每天先不看笔记,把 3 个自测题各口述 1~2 分钟,再展开参考要点核对,答得不完整的读链接的小节;然后做两道算法题,每题限时 30 分钟。
+应聘前端、C++、客户端或测试开发岗位时,可以跳过第 1~4 天和第 12 天的 Spring 部分(Android 和以 Java 为主的测试开发保留第 1、4 天),把时间用在这些岗位更常问的小节:
+
+- 前端:[跨域](HTTP.md#跨域)、[WebSocket](HTTP.md#websocket)、[Web 页面请求过程](计算机网络%20-%20应用层.md#web-页面请求过程)、[跨站脚本攻击](攻击技术.md#一跨站脚本攻击);
+- C++ 和客户端:[系统调用](计算机操作系统%20-%20概述.md#系统调用)、[进程通信](计算机操作系统%20-%20进程管理.md#进程通信)、[虚拟内存](计算机操作系统%20-%20内存管理.md#虚拟内存);
+- 测试开发:Linux 的[线上问题排查](Linux.md#十一线上问题排查)。
+
## 二、每天的安排
| 天 | 主题 | 阅读 | 算法 |
diff --git "a/notes/\345\276\256\346\234\215\345\212\241.md" "b/notes/\345\276\256\346\234\215\345\212\241.md"
index 692d0f6d6..3f0890de7 100644
--- "a/notes/\345\276\256\346\234\215\345\212\241.md"
+++ "b/notes/\345\276\256\346\234\215\345\212\241.md"
@@ -222,7 +222,7 @@ Map result = inventoryClient.deduct(10086, 2);
@FeignClient 注解的接口没有实现类,却能直接注入使用,过程如下:
- 启动时,@EnableFeignClients 导入 FeignClientsRegistrar,它扫描所有带 @FeignClient 的接口,为每个接口注册一个 FeignClientFactoryBean 类型的 Bean 定义;
-- 创建 Bean 时,FeignClientFactoryBean 把服务名拼成 http://inventory-service,用 JDK 动态代理生成接口的实现,也就是上面的 $Proxy88;
+- 创建 Bean 时,FeignClientFactoryBean 把服务名拼成 `http://inventory-service`,用 JDK 动态代理生成接口的实现,也就是上面的 $Proxy88;
- 调用时,代理把方法和参数转换成 HTTP 请求,交给 FeignBlockingLoadBalancerClient,它通过 Spring Cloud LoadBalancer 选出一个实例,把 URL 中的服务名换成实例的地址后再发送。
@@ -310,7 +310,7 @@ public class RandomConfiguration {
停掉 Nacos 后:
- 已经运行的订单服务仍然可以调用库存服务,因为实例列表已经缓存在本地;
-- 新启动的订单服务默认无法启动:spring.cloud.nacos.discovery.fail-fast 默认为 true,注册失败时直接报错退出。把它设为 false 后可以启动,配置也能从本地快照读取,但调用库存服务时报错 No servers available for service: inventory-service,因为新进程从没有拿到过实例列表(见[附录 A3](#a3-注册中心宕机))。
+- 新启动的订单服务默认无法启动:spring.cloud.nacos.discovery.fail-fast 默认为 true,注册失败时直接报错退出。把它设为 false 后可以启动,配置也能从本地快照读取,但调用库存服务时报错 No servers available for service: inventory-service,因为新进程启动时默认不读取磁盘上的实例缓存(spring.cloud.nacos.discovery.naming-load-cache-at-start 默认为 false),又连不上 Nacos(见[附录 A3](#a3-注册中心宕机))。
所以注册中心宕机不会立刻影响线上的调用,但扩容和发布都无法进行,生产环境中 Nacos 要以集群方式部署。
@@ -410,7 +410,7 @@ Spring Cloud Alibaba 中常用 Sentinel 实现:把接口或者一段代码定
AT 与 XA 两阶段提交的区别在于锁持有的时间:XA 的第一阶段只准备、不提交,行锁一直持有到第二阶段;AT 的第一阶段就提交了本地事务、释放行锁,只靠全局锁阻止其它全局事务修改这些行,所以并发更高。代价有两个:
-- 其它事务在第二阶段之前就能读到 98,即默认的全局隔离级别是读未提交,需要读已提交时用 SELECT ... FOR UPDATE 等待全局锁;
+- 其它事务在第二阶段之前就能读到 98,即默认的全局隔离级别是读未提交,需要读已提交时,在 @GlobalTransactional 或 @GlobalLock 的方法中用 SELECT ... FOR UPDATE 等待全局锁,不在这两种上下文中的 FOR UPDATE 不会经过 Seata 的检查;
- 全局锁只对 Seata 管理的事务有效,其它代码直接改了这些行时,回滚前发现当前值既不等于后镜像也不等于前镜像,Seata 就拒绝自动回滚,只能人工处理。
## 八、链路追踪
diff --git "a/notes/\346\266\210\346\201\257\351\230\237\345\210\227.md" "b/notes/\346\266\210\346\201\257\351\230\237\345\210\227.md"
index ebaec52c6..3299f3e45 100644
--- "a/notes/\346\266\210\346\201\257\351\230\237\345\210\227.md"
+++ "b/notes/\346\266\210\346\201\257\351\230\237\345\210\227.md"
@@ -119,6 +119,8 @@ Kafka、RocketMQ 等通过消费者组把两种模型结合起来:一条消息
需要注意的是,RabbitMQ 对无法路由到任何队列的消息同样返回 ack,只开启确认而不设置 mandatory 时,发错路由键的消息会被直接丢弃;设置了 mandatory,这类消息会先退回给生产者。
+RocketMQ 5.x 的主从部署中(未开启 Controller 模式,其它复制参数保持默认),只把 brokerRole 设为 SYNC_MASTER 不会等待从节点:需要确认的副本数由 inSyncReplicas 决定,默认为 1,即主节点写入后就返回。一主一从要同步复制时,还要把 totalReplicas 和 inSyncReplicas 都设为 2。
+
即使这些配置都打开,消息仍可能因为程序缺陷、误操作等原因丢失或者处理失败,关键业务还要定期核对上下游的数据(对账),发现缺失时补发。
### 发送端的可靠性
@@ -162,7 +164,7 @@ COMMIT;
要保证同一业务的消息有序,需要让发送端把这些消息按业务 Key(例如订单号)路由到同一个队列,并且接收端对同一个队列只用一个线程顺序消费。
-发送端也要按业务顺序发送。生产者的重试也可能打乱顺序:多个批次同时在途时,“已支付”的批次失败重试,后面“已发货”的批次先写入成功。Kafka 默认开启的幂等生产者用序号避免了这种乱序,关闭幂等时要把 max.in.flight.requests.per.connection 设为 1。
+发送端也要按业务顺序发送。生产者的重试也可能打乱顺序:多个批次同时在途时,“已支付”的批次失败重试,后面“已发货”的批次先写入成功。Kafka 默认开启的幂等生产者用序号避免了这种乱序,关闭幂等时要把 max.in.flight.requests.per.connection 设为 1。需要注意的是,没有显式设置 enable.idempotence 时,只要把 acks 设为 all 以外的值(例如 acks=1),幂等就会被自动关闭(只在 INFO 日志中提示一句),而 max.in.flight.requests.per.connection 仍是默认的 5,多个批次同时在途时重试乱序又会出现;retries=0 同样会关闭幂等,但这时不再重试。显式设为 true 时,冲突的配置会在启动时报错。
@@ -216,7 +218,7 @@ RabbitMQ 的队列默认没有长度上限,积压的消息占用内存和磁
各消息队列的支持情况:
- RocketMQ 4.x 只支持 18 个固定的延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,消息先存入内部的定时主题,每个级别一个队列,同一队列中先写入的先到期,只需检查队头;
-- RocketMQ 5.x 改用时间轮,支持指定任意的投递时间,默认精度为 1 秒;最长时长由配置决定,开源 5.5.1 中经 Proxy 发送的默认 1 天、Broker 默认 3 天,超出时拒绝写入(以官方最新文档为准);
+- RocketMQ 5.x 改用时间轮,支持指定任意的投递时间,默认精度为 1 秒;最长时长由配置决定,开源 5.x 中 gRPC 客户端经 Proxy 发送时默认最长 1 天(Proxy 的 maxDelayTimeMills),Remoting 客户端默认最长 3 天(Broker 的 timerMaxDelaySec),超出时发送失败,不会立即投递(官方文档写的是最长 24 小时、超出立即投递,与开源实现不同);
- RabbitMQ 常用的做法是给消息设置过期时间(TTL),过期后转入死信交换机,由死信队列的消费者处理。过期的消息到达队头才会被转出,前面 30 分钟的消息会挡住后面已到期的 1 分钟的消息,所以每种延迟时长单独建一个队列;4.4 起仲裁队列也支持延迟投递(以官方最新文档为准);
- Kafka 不支持延迟消息,需要自己实现,例如按延迟时长分成几个主题,消费者读到未到期的消息就暂停等待。
@@ -291,13 +293,15 @@ Kafka 中的消息按主题(Topic)分类,每个主题分成多个分区(
Kafka 4.2 起提供共享组(Share Group):一个分区的消息可以分给多个消费者,消费者数可以超过分区数,每条消息单独确认,超时未确认就重新投递,代价是不再保证顺序。例如 1 个分区的主题,3 个共享消费者各处理了 40 条消息(Kafka 4.4 实测,见[附录 A6](#a6-共享组))。
-消费者加入、退出或者分区数变化时,组内会重新分配分区,称为重平衡(Rebalance)。默认的 classic 协议下所有消费者先交出全部分区,整个组暂停;Kafka 4.0 起的新协议(group.protocol=consumer)只暂停需要移动的分区。例如 6 个分区的组加入第二个消费者时,第一个消费者在两种协议下分别交出 6 个和 3 个分区(Kafka 4.4 实测,见[附录 A7](#a7-重平衡时交出的分区))。
+消费者加入、退出或者分区数变化时,组内会重新分配分区,称为重平衡(Rebalance)。默认的 classic 协议使用 RangeAssignor 分配,所有消费者先交出全部分区,整个组暂停;classic 下改用 CooperativeStickyAssignor,或者使用 Kafka 4.0 起的新协议(group.protocol=consumer,由 broker 计算分配),都只暂停需要移动的分区。例如 6 个分区的组加入第二个消费者时,第一个消费者在两种协议下分别交出 6 个和 3 个分区(Kafka 4.4 实测,见[附录 A7](#a7-重平衡时交出的分区))。
重平衡还可能造成重复消费。一个常见的原因是处理一批消息的时间超过了 max.poll.interval.ms(默认 5 分钟),消费者被认为已经失效,它的分区被分给其他消费者,这时它再提交偏移量会失败。可以调小 max.poll.records 或调大 max.poll.interval.ms;正常的重平衡可以在 ConsumerRebalanceListener 的 onPartitionsRevoked 中先同步提交已处理完的偏移量,再交出分区。
### 副本与可靠性
-每个分区有多个副本,分布在不同的 broker 上,其中一个是 Leader,负责处理读写,其余的 Follower 不断从 Leader 拉取数据。与 Leader 保持同步的副本集合称为 ISR(In-Sync Replicas),Follower 超过 replica.lag.time.max.ms(默认 30 秒)没有追上 Leader 就会被移出 ISR。ISR 中所有副本都已写入的位置称为高水位(High Watermark, HW),消费者只能读到高水位之前的消息。Kafka 4.0 起完全移除了 ZooKeeper,Leader 选举等元数据由内置的 KRaft 控制器管理,它基于 [Raft](分布式.md#六raft) 协议实现。分区数据的复制没有用 Raft 的多数派:多数派要有 2f+1 个副本,才能在 f 个副本故障后保住已提交的消息并选出新 Leader;Kafka 的消息要写入 ISR 中的全部副本才算提交,提交时 ISR 有 f+1 个副本,损失其中 f 个仍能保住这条消息。代价是提交要等 ISR 中最慢的副本;故障后能否继续写入还取决于下文的 min.insync.replicas。
+每个分区有多个副本,分布在不同的 broker 上,其中一个是 Leader,负责处理读写,其余的 Follower 不断从 Leader 拉取数据。与 Leader 保持同步的副本集合称为 ISR(In-Sync Replicas),Follower 超过 replica.lag.time.max.ms(默认 30 秒)没有追上 Leader 就会被移出 ISR。ISR 中所有副本都已写入的位置称为高水位(High Watermark, HW),消费者只能读到高水位之前的消息。
+
+Kafka 4.0 起完全移除了 ZooKeeper,Leader 选举等元数据由内置的 KRaft 控制器管理,它基于 [Raft](分布式.md#六raft) 协议实现。分区数据的复制没有用 Raft 的多数派:多数派要有 2f+1 个副本,才能在 f 个副本故障后保住已提交的消息并选出新 Leader;Kafka 的消息要写入 ISR 中的全部副本才算提交,提交时 ISR 有 f+1 个副本,损失其中 f 个仍能保住这条消息。代价是提交要等 ISR 中最慢的副本;故障后能否继续写入还取决于下文的 min.insync.replicas。
例如创建 3 个分区、3 个副本的主题,然后用 kill -9 停掉 broker 3(3 个节点的 KRaft 集群实测):
@@ -324,7 +328,7 @@ Follower 落后几条消息时仍在 ISR 中,Leader 宕机后它可以成为
-acks=all 要配合 min.insync.replicas 使用,否则 ISR 只剩 Leader 一个时,acks=all 就退化成了 acks=1。min.insync.replicas 设为 2 时,ISR 少于 2 个副本,broker 就拒绝 acks=all 的写入。上面停掉 broker 3 之后 ISR 还有 2 个副本,写入正常;把 min.insync.replicas 改为 3 再写入,生产者重试几次后报错 NotEnoughReplicasException;改用 acks=1 时写入成功,但高水位不前进,停掉的 broker 回到 ISR 后消费者才能读到这条消息(Kafka 4.4 实测,见[附录 A8](#a8-mininsyncreplicas-与-acks))。常用的配置是 3 个副本、min.insync.replicas=2、acks=all,一个副本故障时既不丢消息,也不停止写入。
+acks=all 要配合 min.insync.replicas(默认为 1)使用,否则 ISR 只剩 Leader 一个时,acks=all 就退化成了 acks=1。min.insync.replicas 设为 2 时,ISR 少于 2 个副本,broker 就拒绝 acks=all 的写入。上面停掉 broker 3 之后 ISR 还有 2 个副本,写入正常;把 min.insync.replicas 改为 3 再写入,生产者重试几次后报错 NotEnoughReplicasException;改用 acks=1 时写入成功,但高水位不前进,停掉的 broker 回到 ISR 后消费者才能读到这条消息(Kafka 4.4 实测,见[附录 A8](#a8-mininsyncreplicas-与-acks))。常用的配置是 3 个副本、min.insync.replicas=2、acks=all,一个副本故障时既不丢消息,也不停止写入。
生产者没有收到确认就会重试,这可能让同一条消息写入两次。Kafka 3.0 起默认开启幂等生产者(3.0.0、3.1.0 因缺陷未生效):broker 给每个生产者分配一个 ID,生产者给发往每个分区的消息编上序号,broker 发现序号重复就丢弃。后文查看 .log 文件时,每个批次都带有 producerId: 1001 和 baseSequence 字段。幂等只保证单个分区内、生产者自身重试时不重复,应用自己重发的消息不能去重;要让写入多个分区的消息同时成功或失败,需要使用事务,消费者设置 isolation.level=read_committed 后只读取已提交事务中的消息。
diff --git "a/notes/\347\256\227\346\263\225 - \346\216\222\345\272\217.md" "b/notes/\347\256\227\346\263\225 - \346\216\222\345\272\217.md"
index da0d046e6..ed678c5a4 100644
--- "a/notes/\347\256\227\346\263\225 - \346\216\222\345\272\217.md"
+++ "b/notes/\347\256\227\346\263\225 - \346\216\222\345\272\217.md"
@@ -363,6 +363,7 @@ public class ThreeWayQuickSort> extends QuickSort {
```java
public T select(T[] nums, int k) {
+ shuffle(nums);
int l = 0, h = nums.length - 1;
while (h > l) {
int j = partition(nums, l, h);
@@ -397,6 +398,7 @@ public class Heap> {
private T[] heap;
private int N = 0;
+ // 容量固定为 maxN,插入超过 maxN 个元素时数组越界
public Heap(int maxN) {
this.heap = (T[]) new Comparable[maxN + 1];
}
@@ -471,6 +473,8 @@ public void insert(T v) {
```java
public T delMax() {
+ if (isEmpty())
+ throw new NoSuchElementException("堆为空");
T max = heap[1];
swap(1, N--);
heap[N + 1] = null;
diff --git "a/notes/\347\256\227\346\263\225 - \347\254\246\345\217\267\350\241\250.md" "b/notes/\347\256\227\346\263\225 - \347\254\246\345\217\267\350\241\250.md"
index 426e94ef3..68e2c9d24 100644
--- "a/notes/\347\256\227\346\263\225 - \347\254\246\345\217\267\350\241\250.md"
+++ "b/notes/\347\256\227\346\263\225 - \347\254\246\345\217\267\350\241\250.md"
@@ -216,7 +216,9 @@ public class BinarySearchOrderedST, Value> implement
values[index] = value;
return;
}
- // 否则在数组中插入新的节点,需要先将插入位置之后的元素都向后移动一个位置
+ // 否则在数组中插入新的节点:数组已满时先扩容为原来的两倍,再将插入位置之后的元素都向后移动一个位置
+ if (N == keys.length)
+ resize(Math.max(1, 2 * keys.length));
for (int j = N; j > index; j--) {
keys[j] = keys[j - 1];
values[j] = values[j - 1];
@@ -226,6 +228,11 @@ public class BinarySearchOrderedST, Value> implement
N++;
}
+ private void resize(int capacity) {
+ keys = Arrays.copyOf(keys, capacity);
+ values = Arrays.copyOf(values, capacity);
+ }
+
@Override
public Value get(Key key) {
int index = rank(key);
diff --git "a/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md" "b/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md"
index 8d30358b9..ddd909c11 100644
--- "a/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md"
+++ "b/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md"
@@ -302,7 +302,7 @@ return 0 -- 可以发送
同一个手机号 1 秒内请求 5 次,只有第 1 次发送了短信;把冷却时间改为 1 秒后连续请求 12 次,第 11、12 次因为当天的次数用完被拒绝;同一个 IP 给 30 个手机号发验证码,从第 21 个开始被拒绝(Redis 7.4 实测,见[附录 A20](#a20-接口防刷))。按小时、按天计数是固定窗口,窗口边界附近可能短时间内放过两倍的请求,需要更精确时改用滑动窗口,见 [限流算法](#1-限流算法)。
-验证码本身也要防止被猜中。6 位数字只有 100 万种,不限制输错次数时,在本机直接调用校验脚本逐个尝试,第 835770 次猜中,只用了 8 秒(见附录 A20);通过网络调用接口会慢一些,但按每秒 1000 次计算,平均约 8 分钟也能猜中。所以校验时:
+验证码本身也要防止被猜中。6 位数字只有 100 万种,不限制输错次数时,在服务器所在的机器上直接调用校验逻辑逐个尝试,第 835770 次猜中,只用了 8 秒(见附录 A20);通过网络调用接口会慢一些,但按每秒 1000 次计算,平均约 8 分钟也能猜中。所以校验时:
- 验证码保存在 Redis 中,5 分钟后过期,多个服务实例都能读到;
- 输对后立即删除,一个验证码只能用一次;
@@ -310,7 +310,7 @@ return 0 -- 可以发送
加上发送的限制,一个手机号每天最多能猜 50 次,猜中的概率只有 50 / 106 = 0.005%。
-开放给合作方的接口还要防止请求被篡改或者重放。重放指的是把截获的合法请求原样再发一次,例如把一次退款请求重复提交。做法是双方事先约定一个密钥,调用方把请求的关键字段和时间戳、随机数(nonce)拼在一起,用密钥计算 HMAC 签名:
+开放给合作方的接口还要防止请求被篡改或者重放。重放指的是把截获的合法请求原样再发一次,例如把一次退款请求重复提交。做法是双方事先约定一个密钥,调用方把请求的方法、路径、查询参数、请求体和时间戳、随机数(nonce)拼在一起(漏掉的部分可以被改掉而签名不变),用密钥计算 HMAC 签名:
```text
签名原文:POST\n/api/refund\n1793000000\nn-8f3a2c\n{"order": 1024, "amount": 3900}
@@ -321,9 +321,9 @@ return 0 -- 可以发送
- 时间戳与当前时间相差不超过 5 分钟,很久以前的请求直接拒绝;
- 用同样的密钥重新计算签名并比较,请求中任何一个字段被改过都对不上;
-- 执行 SET nonce:n-8f3a2c 1 NX EX 600 记录随机数,失败说明这个请求已经处理过。随机数保存的时间要覆盖时间戳允许的范围,否则过期后又能重放。
+- 执行 SET nonce:n-8f3a2c 1 NX EX 600 记录随机数,失败说明这个请求已经处理过。随机数保存的时间要覆盖时间戳允许的范围,前后各 5 分钟就保存 10 分钟,否则过期后又能重放。多个调用方时键名带上调用方的标识,避免随机数相互冲突。
-原样重放的请求因为随机数已经用过被拒绝,改掉金额的请求签名对不上,10 分钟后再重放则时间戳超出了范围(见附录 A20)。需要注意的是,签名只适合密钥保存在服务端的调用,例如开放平台和支付回调;写在 App 或网页中的密钥可以被逆向取出,只能增加刷接口的成本。
+原样重放的请求因为随机数已经用过被拒绝,改掉金额的请求签名对不上,10 分钟后再重放则时间戳超出了范围(见附录 A20)。随机数只能拦住原样的重放:调用方超时后换一个随机数重试,签名层看到的是新请求,退款这类操作仍要按业务单号做[幂等](分布式.md#九幂等)。需要注意的是,签名只适合密钥保存在服务端的调用,例如开放平台和支付回调;写在 App 或网页中的密钥可以被逆向取出,只能增加刷接口的成本。
识别出的刷接口来源,还可以在网关中加入黑名单,例如 SET ban:ip:203.0.113.7 1 EX 3600,一小时内直接拒绝它的请求。规模更大的系统会接入风控,根据设备指纹、操作轨迹等判断请求是否来自机器,可疑时要求验证码。图形和滑块验证码能被打码平台破解,同样只能提高成本,所以要和限频、黑名单一起使用。
@@ -496,7 +496,7 @@ WHERE id = 70001 AND user_id = 1001 AND status = 0 AND valid_end > NOW();
拆分的时机有两种:
- 预先拆分:发红包时就拆成 10 份,用 RPUSH 放入 Redis 列表,抢的时候 LPOP 一份。抢的路径只有一次弹出,金额在发的时候就已确定,可以随红包一起落库备查;代价是每一份都要占用内存,份数多时更明显;
-- 实时计算:Redis 中只保存剩余金额和剩余个数,抢的时候在 Lua 脚本中按二倍均值法算出本份金额,再更新这两个值。占用的内存固定,但每次领取都要在脚本中计算。Redis 7 中脚本的随机数生成器每次调用都会随机设置种子,可以直接调用 math.random。
+- 实时计算:Redis 中只保存剩余金额和剩余个数,抢的时候在 Lua 脚本中按二倍均值法算出本份金额,再更新这两个值。占用的内存固定,但每次领取都要在脚本中计算。Redis 7 中脚本可以直接调用 math.random。需要注意的是,随机数生成器只在启动时设置固定的初值,之后各次调用依次取同一个序列中的值,重启后序列从头开始(Redis 7.4 实测,见[附录 A26](#a26-脚本中的-mathrandom));对结果的不可预测性有要求时,由应用生成随机数作为参数传入。
两种做法用的是同一个拆分方法,金额的分布相同,在 Lua 脚本中执行时也都不会超发。红包的份数通常不多,预先拆分的实现更简单。
@@ -537,7 +537,7 @@ WHERE id = 8001 AND remain_num > 0 AND remain_amount >= 276;
应用在脚本执行成功之后、消息发出之前宕机时,这条领取只记录在 Redis 中。因此红包抢完或过期后,要用 Redis 中的领取记录与数据库对账,补发缺失的消息。例如每条消息都投递两次、其中一个用户的消息全部丢失:18 条消息插入 9 条、冲突 9 条,对账补发后剩余金额为 0、领取记录 10 条,合计正好 100 元(MySQL 8.0 实测,见[附录 A16](#a16-并发抢红包与异步落库))。
-红包一般有有效期,例如 24 小时。到期后先完成对账,再把数据库中的剩余金额退回给发送者。
+红包一般有有效期,例如 24 小时。到期后先在 Redis 中停止领取(例如删除金额列表,之后的脚本 LPOP 取不到金额,直接返回已抢完),再对账并补发缺失的消息,确认这些领取都已写入数据库后,把数据库中的剩余金额退回给发送者。顺序反过来时,对账之后仍可能有人抢到,退回的金额就多了。
## 七、海量数据处理
@@ -582,7 +582,7 @@ WHERE id = 8001 AND remain_num > 0 AND remain_amount >= 276;
## 八、短链系统
-短链系统把很长的网址换成很短的网址,例如把 https://shop.example.com/item/1024?from=campaign 换成 https://s.example/6LAze,常用于短信和社交媒体。用户访问短链时,服务返回一个跳转,浏览器再去访问原来的长链接。
+短链系统把很长的网址换成很短的网址,例如把 `https://shop.example.com/item/1024?from=campaign` 换成 `https://s.example/6LAze`,常用于短信和社交媒体。用户访问短链时,服务返回一个跳转,浏览器再去访问原来的长链接。
@@ -889,7 +889,7 @@ S3 的完成请求要求列出每个分片的分片号和 ETag,服务端按分
分片本身不是对象,只有完成请求之后才生成对象,分片也随之消失。因此合并是分片上传的最后一步。在 S3 中,合并由存储服务按分片号组合,应用不需要再下载、拼接并重新上传一次。
-MinIO 兼容 S3 的分片上传接口,它的限制与 S3 一致:分片号必须在 1 到 10000 之间,分片小于 5 MB 时返回 EntityTooSmall 错误。应用侧的流程与上文相同,区别在于分片由 MinIO 保存,完成时由 MinIO 组合成对象。如果希望大文件不经过应用服务器,也可以让客户端用预签名 URL 直接上传到对象存储,应用服务只保存上传状态。
+MinIO 兼容 S3 的分片上传接口,分片号和最小分片的限制与 S3 一致:分片号必须在 1 到 10000 之间,除最后一片外每片至少 5 MiB,否则完成上传时返回 EntityTooSmall 错误;单个分片最大 5 TiB,比 S3 的 5 GiB 大。应用侧的流程与上文相同,区别在于分片由 MinIO 保存,完成时由 MinIO 组合成对象。如果希望大文件不经过应用服务器,也可以让客户端用预签名 URL 直接上传到对象存储,应用服务只保存上传状态。
## 十二、权限系统
@@ -1030,7 +1030,8 @@ SELECT id, title FROM sys_order ORDER BY id;
常见做法是在 Mapper 或 DAO 层用注解标记需要过滤的查询,由 AOP 或 MyBatis 拦截器在执行前把范围条件追加到 WHERE 中,业务代码不需要关心范围。这种做法需要注意以下几点:
- 条件的值必须通过参数绑定传入,不能拼接字符串;
-- 多表关联或子查询中,条件要加在正确的表别名上,拦截器需要能正确解析 SQL;
+- 多表关联或子查询中,条件要加在正确的表别名上,原条件还要先加括号。例如 status = 1 OR status = 2 直接拼上 AND dept_id = 20,AND 的优先级高于 OR,其它部门 status = 1 的订单也会返回,所以拦截器要用 SQL 解析器改写,而不是拼接字符串;
+- MyBatis 拦截器要在 Executor.query 中、计算缓存键之前改写 SQL。在 StatementHandler.prepare 中改写时,缓存键仍按原 SQL 和参数计算,开启二级缓存后,另一个部门的用户会命中前一个用户的查询结果;
- 分页的总数(COUNT)也要带同一条件。张三的订单列表只有 2 条,不带条件的 COUNT(*) 却返回 4,分页会显示错误的总数;
- 修改和删除同样要带范围条件。张三修改订单 2 时,执行 UPDATE sys_order SET amount = 1 WHERE id = 2 AND creator_id = 1001,影响行数为 0,应用据此拒绝请求。这是为了防止不安全的直接对象引用(Insecure Direct Object Reference, IDOR),即用户只改动 URL 中的 ID 就访问到别人的数据;
- 部门有层级时,DEPT 的范围还应包括下级部门,可以在部门表中保存祖先路径,或使用闭包表;
@@ -1468,12 +1469,12 @@ OBJECT ENCODING inbox:0 -> listpack
### A4 MySQL 主键点查的并发扫描
-环境:MySQL 8.0.44,通过 unix socket 连接;Apple M1 Pro(10 核),macOS 15.8.1;客户端 mysqlslap 与服务端在同一台机器上。表 `t1_item` 有 10 万行,主键为 `id BIGINT`,表大小约 5.5 MB,小于 128 MB 的 InnoDB 缓冲池,预热后查询都在内存中完成。
+环境:MySQL 8.0.44,通过 unix socket 连接;Apple M1 Pro(10 核),macOS 15.8.1;客户端 mysqlslap 与服务端在同一台机器上。表 `item` 有 10 万行,主键为 `id BIGINT`,表大小约 5.5 MB,小于 128 MB 的 InnoDB 缓冲池,预热后查询都在内存中完成。
查询为 2000 条不同主键的语句循环执行,形如:
```sql
-SELECT name, price, stock FROM t1_item WHERE id = 48213;
+SELECT name, price, stock FROM item WHERE id = 48213;
```
命令(每个并发数执行 3 次,每次 10 万条查询):
@@ -1494,7 +1495,7 @@ mysqlslap --user=root --socket= --create-schema=demo \
| 64 | 0.557 | 179476 | 0.357 |
| 128 | 0.522 | 191510 | 0.668 |
-吞吐量为实际执行条数除以耗时。mysqlslap 的 `--number-of-queries` 指每个客户端约执行的条数([mysqlslap 手册](https://dev.mysql.com/doc/refman/8.0/en/mysqlslap.html)),64 和 128 个连接时取整后实际分别执行 99968 条。平均响应时间为耗时除以每个客户端的条数,即并发数 ÷ 吞吐量,包含客户端自身的开销。32 个连接的单次耗时在 0.557 至 0.927 秒之间波动。
+吞吐量为实际执行条数除以耗时。mysqlslap 的 `--number-of-queries` 是全部客户端合计的条数,按连接数向下取整平分(手册写作每个客户端的上限,与源码 mysqlslap.cc 中按连接数相除的实现不一致),64 和 128 个连接时实际分别执行 99968 条。平均响应时间为耗时除以每个客户端的条数,即并发数 ÷ 吞吐量,包含客户端自身的开销。32 个连接的单次耗时在 0.557 至 0.927 秒之间波动。
原始输出(并发数 16 的一段):
@@ -1509,10 +1510,10 @@ Benchmark
### A5 热点行与分散行的 UPDATE
-环境同[附录 A4](#a4-mysql-主键点查的并发扫描)。表 `t1_stock` 有 100 行(`sku_id` 为 1 至 100,`sold` 为计数)。热点负载为:
+环境同[附录 A4](#a4-mysql-主键点查的并发扫描)。表 `sku_stock` 有 100 行(`sku_id` 为 1 至 100,`sold` 为计数)。热点负载为:
```sql
-UPDATE t1_stock SET sold = sold + 1 WHERE sku_id = 1;
+UPDATE sku_stock SET sold = sold + 1 WHERE sku_id = 1;
```
分散负载为 2000 条语句,`sku_id` 为 1 至 100 之间的随机数。两者都是自动提交,每个并发数执行 20000 条,3 次平均。服务端的 `innodb_flush_log_at_trx_commit` 和 `sync_binlog` 均为默认值 1,每次提交都要刷盘。
@@ -1567,7 +1568,7 @@ print(params / 1e9, kv_per_token)
### A9 分片上传的最小实现与实测
-环境:Apple M1 Pro,macOS 15.8.1;Python 3.13.7,标准库 http.server 和 hashlib,pymysql 2.2.8;MySQL 8.0.44,demo 库中三张表 t2_upload、t2_part、t2_file。测试文件为 300 MiB(314572800 字节),由固定种子的伪随机数生成,分片 5 MiB,共 60 片。
+环境:Apple M1 Pro,macOS 15.8.1;Python 3.13.7,标准库 http.server 和 hashlib,pymysql 2.2.8;MySQL 8.0.44,demo 库中三张表 upload_task、upload_part、file_meta。测试文件为 300 MiB(314572800 字节),由固定种子的伪随机数生成,分片 5 MiB,共 60 片。
服务端的分片接收和合并(核心片段,省略建表、路由和部分错误分支):
@@ -1583,18 +1584,18 @@ def put_part(self, uid, n):
if not buf: # 客户端中途断开
break
f.write(buf); h.update(buf); md5.update(buf); got += len(buf)
- expect = q("SELECT expect_sha256 FROM t2_part WHERE upload_id=%s AND part_no=%s",
+ expect = q("SELECT expect_sha256 FROM upload_part WHERE upload_id=%s AND part_no=%s",
(uid, n), 'one')
if got != length or h.hexdigest() != expect['expect_sha256']:
os.remove(tmp)
return self.send(400 if got != length else 409, {'error': 'part rejected'})
os.replace(tmp, part_path(uid, n)) # 同号分片被覆盖
- q("UPDATE t2_part SET size=%s, etag=%s WHERE upload_id=%s AND part_no=%s",
+ q("UPDATE upload_part SET size=%s, etag=%s WHERE upload_id=%s AND part_no=%s",
(got, md5.hexdigest(), uid, n))
self.send(200, {'part': n, 'etag': md5.hexdigest()})
# 服务端:完成上传。条件更新保证只有一个请求进入合并,合并时流式计算整文件哈希
-if q("UPDATE t2_upload SET status='completing' WHERE id=%s AND status='uploading'", (uid,)) == 0:
+if q("UPDATE upload_task SET status='completing' WHERE id=%s AND status='uploading'", (uid,)) == 0:
return self.send(409, {'error': 'being completed or aborted'})
# 此处省略检查缺片、逐片核对哈希的代码
h = hashlib.sha256()
@@ -1747,10 +1748,10 @@ $ 客户端 status e355c1f1002e409d
upload directory exists after abort: False
== E1 数据库状态
-t2_upload by status: (('aborted', 1), ('done', 2))
-t2_file rows: 2
-t2_part (upload_id, rows, uploaded): ('762fa9bd16a44d31', 20, Decimal('20'))
-t2_part (upload_id, rows, uploaded): ('b80db1c51e31407b', 60, Decimal('60'))
+upload_task by status: (('aborted', 1), ('done', 2))
+file_meta rows: 2
+upload_part (upload_id, rows, uploaded): ('762fa9bd16a44d31', 20, Decimal('20'))
+upload_part (upload_id, rows, uploaded): ('b80db1c51e31407b', 60, Decimal('60'))
== 服务端日志
listening on 49696
@@ -1782,7 +1783,7 @@ SHA-256 扫描 300 MiB 实测 0.31 s,换算 2 GiB 约 2.12 s
### A11 权限判断与执行计划
-环境:MySQL 8.0.44,macOS(Apple M1 Pro),库 t3_rbac,客户端字符集 utf8mb4。判断查询与正文相同,五个用例只替换 user_id、请求方法和路径。原始输出如下:
+环境:MySQL 8.0.44,macOS(Apple M1 Pro),库 rbac,客户端字符集 utf8mb4。判断查询与正文相同,五个用例只替换 user_id、请求方法和路径。原始输出如下:
```text
+--------------------------------+---------+
@@ -1795,13 +1796,13 @@ SHA-256 扫描 300 MiB 实测 0.31 s,换算 2 GiB 约 2.12 s
| 1005 GET /api/orders (no role) | 0 |
+--------------------------------+---------+
-+----+-------------+-------+------------+--------+-----------------------+---------------+---------+--------------------------+------+----------+-------------+
-| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
-+----+-------------+-------+------------+--------+-----------------------+---------------+---------+--------------------------+------+----------+-------------+
-| 1 | SIMPLE | p | NULL | const | PRIMARY,uk_method_url | uk_method_url | 1056 | const,const | 1 | 100.00 | NULL |
-| 1 | SIMPLE | ur | NULL | ref | PRIMARY,idx_role_id | PRIMARY | 8 | const | 1 | 100.00 | Using index |
-| 1 | SIMPLE | rp | NULL | eq_ref | PRIMARY,idx_perm_id | PRIMARY | 16 | t3_rbac.ur.role_id,const | 1 | 100.00 | Using index |
-+----+-------------+-------+------------+--------+-----------------------+---------------+---------+--------------------------+------+----------+-------------+
++----+-------------+-------+------------+--------+-----------------------+---------------+---------+-----------------------+------+----------+-------------+
+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
++----+-------------+-------+------------+--------+-----------------------+---------------+---------+-----------------------+------+----------+-------------+
+| 1 | SIMPLE | p | NULL | const | PRIMARY,uk_method_url | uk_method_url | 1056 | const,const | 1 | 100.00 | NULL |
+| 1 | SIMPLE | ur | NULL | ref | PRIMARY,idx_role_id | PRIMARY | 8 | const | 1 | 100.00 | Using index |
+| 1 | SIMPLE | rp | NULL | eq_ref | PRIMARY,idx_perm_id | PRIMARY | 16 | rbac.ur.role_id,const | 1 | 100.00 | Using index |
++----+-------------+-------+------------+--------+-----------------------+---------------+---------+-----------------------+------+----------+-------------+
```
### A12 数据范围、分页总数与写操作
@@ -1947,7 +1948,7 @@ return 0
10400 ms 拒绝 剩余令牌 0.0
```
-内存用 MEMORY USAGE SAMPLES 0 测量:写入 1000 条记录的 ZSET(成员形如 req-1000000,分值为毫秒时间戳)为 102016 字节,多次运行在 101888~102384 字节之间;固定窗口的计数器为 56 字节,令牌桶的哈希表为 104 字节。
+内存用 MEMORY USAGE \ SAMPLES 0 测量:写入 1000 条记录的 ZSET(成员形如 req-1000000,分值为毫秒时间戳)为 102016 字节,多次运行在 101888~102384 字节之间;固定窗口的计数器为 56 字节,令牌桶的哈希表为 104 字节。
单个键的吞吐量用 redis-benchmark 测量(本机回环,50 个连接,每项 200000 次,同一个键),令牌桶脚本先用 SCRIPT LOAD 加载:
@@ -2258,40 +2259,40 @@ ignoreExceptions(BizException):状态 CLOSED,失败 0 次,失败率 -1%,
### A19 防止超卖的几种写法
-环境:MySQL 8.0.44(默认的可重复读),Python 3 + PyMySQL,Apple M1 Pro。表 d5_goods(id, stock, version) 中商品 1001 的库存为 100,d5_order 的主键为 (user_id, goods_id)。每种写法用 64 个线程,每个用户请求 1 次,每个线程使用自己的连接(自动提交):
+环境:MySQL 8.0.44(默认的可重复读),Python 3 + PyMySQL,Apple M1 Pro。表 goods(id, stock, version) 中商品 1001 的库存为 100,goods_order 的主键为 (user_id, goods_id)。每种写法用 64 个线程,每个用户请求 1 次,每个线程使用自己的连接(自动提交):
```python
def naive(uid): # 先查询,大于 0 再扣减
- cur.execute("SELECT stock FROM d5_goods WHERE id = 1001")
+ cur.execute("SELECT stock FROM goods WHERE id = 1001")
if cur.fetchone()[0] <= 0: return 'soldout'
- cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001")
- cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
+ cur.execute("UPDATE goods SET stock = stock - 1 WHERE id = 1001")
+ cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,))
return 'ok'
def pessimistic(uid): # SELECT ... FOR UPDATE
c.begin()
- cur.execute("SELECT stock FROM d5_goods WHERE id = 1001 FOR UPDATE")
+ cur.execute("SELECT stock FROM goods WHERE id = 1001 FOR UPDATE")
if cur.fetchone()[0] <= 0: c.rollback(); return 'soldout'
- cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001")
- cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
+ cur.execute("UPDATE goods SET stock = stock - 1 WHERE id = 1001")
+ cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,))
c.commit(); return 'ok'
def optimistic(uid, retries=0): # 版本号,冲突时最多重试 retries 次
for _ in range(retries + 1):
- cur.execute("SELECT stock, version FROM d5_goods WHERE id = 1001")
+ cur.execute("SELECT stock, version FROM goods WHERE id = 1001")
stock, ver = cur.fetchone()
if stock <= 0: return 'soldout'
- n = cur.execute("UPDATE d5_goods SET stock = stock - 1, version = version + 1 "
+ n = cur.execute("UPDATE goods SET stock = stock - 1, version = version + 1 "
"WHERE id = 1001 AND version = %s", (ver,))
if n == 1:
- cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
+ cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,))
return 'ok'
return 'conflict'
def conditional(uid): # 条件更新
- n = cur.execute("UPDATE d5_goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0")
+ n = cur.execute("UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0")
if n == 0: return 'soldout'
- cur.execute("INSERT INTO d5_order VALUES (%s, 1001)", (uid,))
+ cur.execute("INSERT INTO goods_order VALUES (%s, 1001)", (uid,))
return 'ok'
```
@@ -2683,6 +2684,22 @@ WHERE o.pay_txn IS NOT NULL AND o.channel = 'wxpay'
+--------------+----------+-------------+--------------+---------+------------------------+
```
+### A26 脚本中的 math.random
+
+环境:Redis 7.4.1,Apple M1 Pro,macOS,默认配置。连续调用三次同一个脚本,重启 Redis 后再调用三次:
+
+```bash
+for i in 1 2 3; do redis-cli eval "return math.random(1,1000000)" 0; done
+redis-cli shutdown nosave # 重新启动 redis-server 后再执行上一行
+```
+
+```text
+重启前:396465 840486 353337
+重启后:396465 840486 353337
+```
+
+三次调用的结果不同,说明不是每次调用都把种子重设为同一个值;重启后得到同样的序列,说明初值是固定的。源码中 math.random 被替换为 redisLrand48,只有脚本调用 math.randomseed 时才重设种子(src/rand.c、src/script_lua.c)。
+
## 参考资料
- [Redis: Scripting with Lua](https://redis.io/docs/latest/develop/programmability/eval-intro/)
diff --git "a/notes/\347\274\223\345\255\230.md" "b/notes/\347\274\223\345\255\230.md"
index 76234faac..39cb6a0fe 100644
--- "a/notes/\347\274\223\345\255\230.md"
+++ "b/notes/\347\274\223\345\255\230.md"
@@ -84,7 +84,7 @@ Cache cache = Caffeine.newBuilder()
Product p = cache.get("product:1024", key -> loadFromRedisOrDb(key));
```
-Caffeine 的淘汰算法 W-TinyLFU 同时考虑访问频率和最近访问时间:新数据先进入一个小的 LRU 窗口区,被窗口淘汰时再和主区将被淘汰的数据比较历史访问频率,频率高的留下,偶发的批量访问挤不掉热点数据,命中率通常高于 LRU。get 传入的加载函数对同一个键只会执行一次,其它并发的请求等待它的结果,避免缓存未命中时大量请求同时查询下游。
+Caffeine 的淘汰算法 W-TinyLFU 同时考虑访问频率和最近访问时间:新数据先进入一个小的 LRU 窗口区,被窗口淘汰时再和主区将被淘汰的数据比较历史访问频率,频率高的留下,偶发的批量访问挤不掉热点数据,命中率通常高于 LRU。get 传入的加载函数对同一个键只会执行一次,其它并发的请求等待它的结果,避免缓存未命中时大量请求同时查询下游。加载函数抛出异常或返回 null 时不写入缓存,仍未命中的请求会再次执行它。通过 Spring 的 @Cacheable 使用时,默认不合并并发的未命中,要设置 sync = true 才走这个加载函数。
### 数据库缓存
diff --git "a/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \346\255\273\351\224\201.md" "b/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \346\255\273\351\224\201.md"
index 985e63678..24a2f047f 100644
--- "a/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \346\255\273\351\224\201.md"
+++ "b/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237 - \346\255\273\351\224\201.md"
@@ -207,7 +207,11 @@ synchronized (first) {
两个线程各反向转账 10 万次,原来的写法每次运行都会卡住,按 id 顺序加锁后都能完成,两个账户的余额仍为 1000(JDK 21 实测,见[附录 A1](#a1-java-线程死锁))。使用 ReentrantLock 时还可以用 tryLock 设置超时,拿不到锁就释放已持有的锁再重试,破坏的是[不可抢占条件](#3-破坏不可抢占条件)。例如获取第二把锁等待 10 ms 仍失败时释放第一把锁,随机等待 1~4 ms 后重试,同样的两个线程在 30~60 ms 内完成(见[附录 A4](#a4-死锁线程的状态与-cpu-时间))。等待时间要随机,否则两个线程可能总是同时释放、同时重试,谁都拿不到两把锁,这称为活锁。
-需要注意的是,用阻塞式的锁时,死锁的线程处于阻塞状态,并不消耗 CPU;用自旋锁形成的死锁则会一直占用 CPU。上例卡住后,两个线程的状态都是 BLOCKED,之后 3 秒内各自消耗的 CPU 时间都不到 0.1 ms(JDK 21 实测,见[附录 A4](#a4-死锁线程的状态与-cpu-时间))。因此 CPU 占用很高时应先按 [CPU 占用过高](Java%20虚拟机.md#cpu-占用过高)排查死循环、频繁 GC 等问题;CPU 很低、请求却全部卡住,线程都停在加锁的地方,才更像死锁,用 jstack 或 jcmd 的 Thread.print 命令打印线程栈确认。JVM 不会像 InnoDB 那样选一个线程回滚,卡在 synchronized 上的线程也无法被中断,通常只能重启进程,因此要靠固定加锁顺序或 tryLock 提前预防。程序中也可以定时调用 ThreadMXBean 的 findDeadlockedThreads() 检测死锁并告警,但它检测不到包含虚拟线程的死锁环。
+需要注意的是,用阻塞式的锁时,死锁的线程处于阻塞状态,并不消耗 CPU;用自旋锁形成的死锁则会一直占用 CPU。上例卡住后,两个线程的状态都是 BLOCKED,之后 3 秒内各自消耗的 CPU 时间都不到 0.1 ms(JDK 21 实测,见[附录 A4](#a4-死锁线程的状态与-cpu-时间))。
+
+因此 CPU 占用很高时应先按 [CPU 占用过高](Java%20虚拟机.md#cpu-占用过高)排查死循环、频繁 GC 等问题;CPU 很低、请求却全部卡住,线程都停在加锁的地方,才更像死锁,用 jstack 或 jcmd 的 Thread.print 命令打印线程栈确认。
+
+JVM 不会像 InnoDB 那样选一个线程回滚,卡在 synchronized 上的线程也无法被中断,通常只能重启进程,因此要靠固定加锁顺序或 tryLock 提前预防。程序中也可以定时调用 ThreadMXBean 的 findDeadlockedThreads() 检测死锁并告警,但它检测不到包含虚拟线程的死锁环。
一个线程持有锁后再次申请同一把锁,等的是只有自己才会释放的锁,也会卡死,称为自死锁(self-deadlock)。synchronized 和 ReentrantLock 是可重入锁,同一线程再次加锁只把计数加 1;不可重入的锁会卡住,例如许可数为 1 的 Semaphore,同一线程连续 acquire 两次后一直处于 WAITING,findDeadlockedThreads() 也检不出来(JDK 21 实测,见[附录 A5](#a5-单个线程重复加锁))。
diff --git "a/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \345\272\224\347\224\250\345\261\202.md" "b/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \345\272\224\347\224\250\345\261\202.md"
index b881e8b3d..9e3dc6a5a 100644
--- "a/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \345\272\224\347\224\250\345\261\202.md"
+++ "b/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \345\272\224\347\224\250\345\261\202.md"
@@ -27,7 +27,7 @@
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
-域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。例如 com 是顶级域名,example.com 是二级域名,www.example.com 是三级域名;完整的写法是 www.example.com.,最右边的点代表根。
+域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。例如 com 是顶级域名,example.com 是二级域名,www.example.com 是三级域名;完整的写法是 `www.example.com.`,最右边的点代表根。
以解析 www.example.com 为例:主机先查浏览器缓存、操作系统缓存和 hosts 文件,没有命中就向本地 DNS 服务器查询。本地 DNS 服务器依次询问根服务器、com 顶级域服务器和 example.com 的权威服务器,前两者只返回下一级服务器的地址,权威服务器返回最终的 IP 地址。
@@ -35,7 +35,7 @@ DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转
主机到本地 DNS 服务器是递归查询,主机只等最终结果;本地 DNS 服务器到其它服务器是迭代查询,由它自己一步步问下去。每一级的结果都按 TTL 缓存,所以大多数查询在本地 DNS 服务器甚至主机上就能得到结果。
-TTL 是每条记录自带的缓存时间,单位为秒,由权威服务器给出,缓存中的记录每过一秒剩余时间减 1。例如把公共 DNS 223.5.5.5 作为本地 DNS 服务器,查询 www.jd.com(dig 实测,见[附录 A2](#a2-dns-的-ttl-与-cname)):
+TTL 是每条记录自带的缓存时间,单位为秒,由权威服务器给出,缓存中的记录每过一秒剩余时间减 1。例如把公共 DNS 223.5.5.5 作为本地 DNS 服务器,查询 `www.jd.com`(dig 实测,见[附录 A2](#a2-dns-的-ttl-与-cname)):
```text
www.jd.com. 74 IN CNAME www.jd.com.gslb.qianxun.com.
@@ -142,10 +142,10 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删
在浏览器中输入 https://www.example.com/ 并回车后,主要经过以下步骤:
-1. 浏览器解析 URL,得到协议 https、主机名 www.example.com、端口(省略时 https 为 443)和路径 /,各部分的含义见 [URL](HTTP.md#url),然后检查缓存,有未过期的缓存时直接使用,见 [缓存](HTTP.md#缓存);
-2. DNS 解析主机名 www.example.com,得到 IP 地址,路径和参数不参与解析;
+1. 浏览器解析 URL,得到协议 https、主机名 `www.example.com`、端口(省略时 https 为 443)和路径 /,各部分的含义见 [URL](HTTP.md#url),然后检查缓存,有未过期的缓存时直接使用,见 [缓存](HTTP.md#缓存);
+2. DNS 解析主机名 `www.example.com`,得到 IP 地址,路径和参数不参与解析;
3. 与服务器进行 TCP 三次握手,HTTPS 还要进行 TLS 握手;
-4. 发送 HTTP 请求。请求通常先到达负载均衡或[反向代理](集群.md#3-反向代理服务器)(例如 Nginx),再转发给某台应用服务器,应用服务器查询缓存或数据库后生成响应。如果响应是重定向,例如只输入 baidu.com 时浏览器补上协议后访问,得到 301 和 Location: https://www.baidu.com/(见[附录 A3](#a3-省略协议时的重定向)),浏览器对新地址重新走一遍前面的步骤;
+4. 发送 HTTP 请求。请求通常先到达负载均衡或[反向代理](集群.md#3-反向代理服务器)(例如 Nginx),再转发给某台应用服务器,应用服务器查询缓存或数据库后生成响应。如果响应是重定向,例如只输入 baidu.com 时浏览器补上协议后访问,得到 301 和 `Location: https://www.baidu.com/`(见[附录 A3](#a3-省略协议时的重定向)),浏览器对新地址重新走一遍前面的步骤;
5. 浏览器解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM,两者合成渲染树,再计算每个元素的位置和大小(布局),最后绘制到屏幕上。解析中遇到 CSS、JavaScript、图片等外部资源时另发请求,不带 async 或 defer 的普通脚本(type 不是 module)会暂停 HTML 的解析,直到脚本下载并执行完;
6. 连接保持一段时间供后续请求复用,空闲后关闭,见 [短连接与长连接](HTTP.md#1-短连接与长连接)。
diff --git "a/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\233\256\345\275\225.md" "b/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\233\256\345\275\225.md"
index 4e20a5851..b19080116 100644
--- "a/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\233\256\345\275\225.md"
+++ "b/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234 - \347\233\256\345\275\225.md"
@@ -15,7 +15,7 @@
- Kurose J F, Ross K W. 计算机网络:自顶向下方法(第 7 版)[M]. 机械工业出版社, 2018.
- Stevens W R. TCP/IP 详解 卷 1:协议 [M]. 机械工业出版社, 2006.
- [Active vs Passive FTP Mode: Which One is More Secure?](https://securitywing.com/active-vs-passive-ftp-mode/)
-- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined)
+- [Active and Passive FTP Transfers](https://support.solarwinds.com/SuccessCenter/s/article/Active-and-Passive-FTP-Transfers)
- [Traceroute](https://zh.wikipedia.org/wiki/Traceroute)
- [ping](https://zh.wikipedia.org/wiki/Ping)
- [How DHCP works and DHCP Interview Questions and Answers](http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html)
diff --git "a/notes/\350\256\276\350\256\241\346\250\241\345\274\217 - \345\215\225\344\276\213.md" "b/notes/\350\256\276\350\256\241\346\250\241\345\274\217 - \345\215\225\344\276\213.md"
index c466d6dfc..cc26937be 100644
--- "a/notes/\350\256\276\350\256\241\346\250\241\345\274\217 - \345\215\225\344\276\213.md"
+++ "b/notes/\350\256\276\350\256\241\346\250\241\345\274\217 - \345\215\225\344\276\213.md"
@@ -184,7 +184,9 @@ java.lang.IllegalArgumentException: Cannot reflectively create enum objects
该实现可以防止反射攻击。在其它实现中,调用私有构造函数的 setAccessible(true) 可以绕过访问检查,再调用 newInstance() 创建新的实例;要防止这种攻击,可以在构造函数中检查实例是否已经存在并抛出异常,但这只能挡住实例创建之后的反射调用:懒汉式在第一次调用 getUniqueInstance() 之前被反射创建时,检查不出来。对于枚举类型,Constructor.newInstance() 直接抛出 IllegalArgumentException,因此不会出现上述的反射攻击。
-该实现在多次序列化和反序列化之后,不会得到多个实例。而其它实现要实现 Serializable,反序列化时每次都会创建一个新对象,需要提供 readResolve() 方法返回已有的实例来替换它;transient 只决定字段是否参与序列化,不能保证实例唯一。
+该实现在多次序列化和反序列化之后,不会得到多个实例。这是因为枚举常量序列化时只写入它的名字,反序列化时用 Enum.valueOf() 按名字找回已有的常量,这个过程不能自定义。而其它实现要实现 Serializable,反序列化时每次都会创建一个新对象,需要提供 readResolve() 方法返回已有的实例来替换它;transient 只决定字段是否参与序列化,不能保证实例唯一。
+
+这几种写法中,《Effective Java》推荐单元素的枚举,它同时挡住了反射和反序列化;枚举不能继承其它类,单例需要继承某个父类时,用静态内部类实现。
### 示例
- 日志类;
@@ -206,3 +208,5 @@ Spring 默认的单例 Bean 与这里的单例不同:它指的是一个容器
- 弗里曼. Head First 设计模式 [M]. 中国电力出版社, 2007.
- Gamma E. 设计模式:可复用面向对象软件的基础 [M]. 机械工业出版社, 2007.
+- Bloch J. Effective Java(第 3 版) [M]. Addison-Wesley, 2018.
+- [Java Object Serialization Specification: Serialization of Enum Constants](https://docs.oracle.com/en/java/javase/21/docs/specs/serialization/serial-arch.html)
diff --git "a/notes/\351\233\206\347\276\244.md" "b/notes/\351\233\206\347\276\244.md"
index b2e5a47c8..4a90e2876 100644
--- "a/notes/\351\233\206\347\276\244.md"
+++ "b/notes/\351\233\206\347\276\244.md"
@@ -156,7 +156,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
优点:
-- DNS 能够根据地理位置进行域名解析,返回离用户最近的服务器 IP 地址。
+- DNS 能够根据查询来源的位置进行域名解析,返回离用户较近的服务器 IP 地址。需要注意的是,权威 DNS 看到的是用户所用的递归 DNS 服务器的地址,递归服务器支持 EDNS Client Subnet 时才能看到用户所在的网段。
缺点: