0%

网络 HTTP

从HTTP/1到HTTP/3:现代Web通信协议的演进

深入解析互联网基础设施的技术变革

HTTP是现代互联网通信的基石,从浏览器请求到Kubernetes集群内的微服务调用,几乎所有的网络交互都基于HTTP协议。每当浏览器加载页面、APP获取API响应、或后端服务相互查询时,几乎都是通过HTTP进行的。即使底层传输协议发生变化,这一点仍然成立——例如,gRPC将自己包装在HTTP/2之上,而主导后端设计的RESTful API只是建立在HTTP动词和状态码之上的约定。

基础认知:理解HTTP的生态角色

在深入探讨HTTP演进之前,我们必须理解HTTP运行在一个复杂的生态系统中。现代应用不仅仅是孤立地处理HTTP——它们还必须考虑数据库选择(SQL vs NoSQL)、CAP定理的权衡(一致性、可用性、分区容错性),以及HTTP所依赖的底层网络协议。

经典的客户端-服务器模型

alt text

这个简单的交换隐藏了巨大的复杂性。当你的浏览器请求/index.html时,它不仅仅是在获取一个文件——它参与了一个精心编排的复杂过程:

  • DNS解析
  • TCP连接建立
  • HTTP请求/响应循环
  • 连接管理
  • 缓存策略

HTTP/1.0:先驱者(1996年)

HTTP/1.0在当时是革命性的,但以今天的标准来看极其简单:

主要特征:

  • 无状态:每个请求都是独立的
  • 基于文本:人类可读的协议
  • 每请求一连接:每个资源都需要新的TCP连接
  • 无持久性:每次响应后连接都会关闭

问题所在:
想象加载一个包含50个资源(图片、CSS、JavaScript)的网页。HTTP/1.0需要建立50个独立的TCP连接。考虑到TCP的三次握手开销,这种效率是灾难性的。

1
2
3
请求1: [SYN] → [SYN-ACK] → [ACK] → [HTTP请求] → [响应] → [FIN]
请求2: [SYN] → [SYN-ACK] → [ACK] → [HTTP请求] → [响应] → [FIN]
... (再重复48次)

HTTP/1.1:主力军(1997年)

HTTP/1.1解决了HTTP/1.0的低效问题,成为互联网20多年来的主力协议。

主要改进:

1. 持久连接

1
Connection: keep-alive

HTTP/1.1允许在单个TCP连接上进行多个请求,而不是每次请求后都关闭连接。

2. 请求管道化
可以发送多个请求而无需等待响应,但响应仍必须按顺序处理。

3. Host头部

1
Host: example.com

启用虚拟主机——单个IP地址上的多个网站。

4. 分块传输编码

1
Transfer-Encoding: chunked

允许在不知道总内容长度的情况下流式传输响应。

队头阻塞(HOL Blocking)问题

尽管有这些改进,HTTP/1.1仍存在一个根本限制:队头阻塞。在管道化连接中,如果第一个响应延迟,所有后续响应都必须等待,即使它们已经准备好了。

1
2
3
请求A ────────────────────> [处理中...]
请求B ──> [就绪] [等待A]
请求C ──> [就绪] [等待A]

这与数据库理论相关。正如数据库在CAP定理中面临权衡(无法同时完美地拥有一致性、可用性和分区容错性),网络协议也面临自己的约束。HTTP/1.1选择了简单性和有序交付,而非并行性。

HTTP/2:游戏改变者(2015年)

HTTP/2代表了Web性能思维的根本转变。

革命性特性:

1. 二进制协议
与HTTP/1.1的基于文本格式不同,HTTP/2使用二进制帧,使其解析更高效、更不容易出错。

2. 多路复用
多个请求和响应可以在单个TCP连接上交错进行,不会相互阻塞。

1
2
3
4
连接1:
├── 流1: [请求A] ──> [响应A]
├── 流2: [请求B] ──> [响应B]
└── 流3: [请求C] ──> [响应C]

3. 服务器推送
服务器可以主动向客户端发送资源:

1
2
客户端请求: index.html
服务器推送: style.css, script.js, logo.png

4. 头部压缩(HPACK)
通过压缩HTTP头部(通常包含重复信息)来减少开销。

5. 流优先级
客户端可以指示哪些资源更重要:

1
Priority: weight=200, depends_on=stream_1

TCP瓶颈

然而,HTTP/2仍然依赖TCP,TCP在传输层有自己的队头阻塞。如果单个TCP数据包丢失,整个连接就会停滞,直到重传完成,影响所有HTTP/2流。

这反映了分布式数据库中的一致性挑战。正如最终一致性系统(如NoSQL文档存储)可以通过放松严格的一致性要求来提供更好的可用性,HTTP/3后来也会放松TCP的严格顺序保证。

HTTP/3:突破束缚(2020年)

HTTP/3通过完全放弃TCP而采用基于UDP的QUIC,代表了HTTP演进中最激进的变化。

QUIC:革命基础

QUIC(Quick UDP Internet Connections)解决了TCP的根本限制:

1. 减少连接建立
QUIC结合了传输和加密握手:

1
2
TCP + TLS: 3次往返
QUIC: 1次往返(后续连接为0-RTT)

2. 每流控制流量
与TCP的连接级流量控制不同,QUIC提供流级控制,消除了传输层队头阻塞。

3. 连接迁移
移动设备在WiFi和蜂窝网络之间切换时可以保持连接。

4. 内置加密
与基于TCP的HTTP/2不同,QUIC在设计上就包含加密——没有未加密的QUIC。

HTTP/3的革命性架构

HTTP/3从根本上改变了我们对Web通信的思考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HTTP/3协议栈:
┌─────────────────┐
│ 应用层 │ ← HTTP/3语义
├─────────────────┤
│ QUIC │ ← 传输+安全
├─────────────────┤
│ UDP │ ← 网络层
└─────────────────┘

传统协议栈:
┌─────────────────┐
│ 应用层 │ ← HTTP/1.1 或 HTTP/2
├─────────────────┤
│ TLS │ ← 安全层
├─────────────────┤
│ TCP │ ← 传输层
├─────────────────┤
│ IP │ ← 网络层
└─────────────────┘

演进时间线:可视化之旅

HTTP/1.1(1997年):顺序处理

1
2
3
4
5
6
7
8
9
客户端 ────────────> 服务器
│ TCP连接 │
│ ┌─────────────┐ │
│ │ 请求1 │──>│
│ │<── 响应 │ │
│ │ 请求2 │──>│
│ │<── 响应 │ │
│ └─────────────┘ │
│ 关闭连接 │

主要特性:

  • 持久连接和管道化
  • 使用TCP进行可靠传输
  • 应用层队头阻塞
  • 基于文本的协议

HTTP/2(2015年):多路复用革命

1
2
3
4
5
6
7
8
9
客户端 ────────────> 服务器
│ 单个TCP连接 │
│ ┌─────────────────────┐ │
│ │ 流1: 请求A │──>│
│ │ 流2: 请求B │──>│
│ │ 流3: 请求C │──>│
│ │<────── 响应 ────│ │
│ │ (多路复用) │ │
│ └─────────────────────┘ │

革命性变化:

  • 二进制帧层:高效解析和减少错误
  • 多路复用:同一TCP连接上的多个并发请求
  • 流优先级:关键资源获得优先级
  • 服务器推送:主动资源传递
  • 头部压缩(HPACK):减少开销

二进制帧的魔力:

1
2
3
4
5
6
HTTP消息(逻辑)
┌─────────────────┐
│ 请求头部 │ ──> 帧头部 <类型 = 头部>
├─────────────────┤ 帧主体 <压缩头部>
│ 请求主体 │ ──> 帧头部 <类型 = 数据>
└─────────────────┘ 帧主体 <实际数据>

HTTP/3(2019年):QUIC优势

1
2
3
4
5
6
7
8
9
10
客户端 ────────────> 服务器
│ 基于UDP的QUIC │
│ ┌─────────────────────┐ │
│ │ ╔═══════════════╗ │ │
│ │ ║ HTTP请求 ║ │ │
│ │ ╚═══════════════╝ │ │
│ │ ╔═══════════════╗ │ │
│ │ ║ HTTP响应 ║ │ │
│ │ ╚═══════════════╝ │ │
│ └─────────────────────┘ │

QUIC的颠覆性特性:

  • 无需正式连接建立
  • 基于UDP的最大灵活性
  • 每流流量控制消除传输层HOL阻塞
  • 0-RTT连接恢复
  • 连接迁移支持

性能对比:现实世界的影响

连接建立

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 + TLS:
[DNS] → [TCP SYN] → [TCP ACK] → [TLS握手] → [HTTP请求]
总计: ~3-4次往返

HTTP/2 + TLS:
[DNS] → [TCP SYN] → [TCP ACK] → [TLS握手] → [HTTP请求]
总计: ~3-4次往返(与HTTP/1.1相同)

HTTP/3 + QUIC:
[DNS] → [QUIC握手 + TLS + HTTP请求]
总计: ~1-2次往返(回访客户端为0-RTT)

队头阻塞分析

HTTP/1.1:应用层和传输层都有阻塞

1
2
3
请求A [████████████████] (慢)
请求B [██] (快,但在等待)
请求C [███] (快,但在等待)

HTTP/2:解决应用层,但TCP仍然阻塞

1
2
3
流A [████████████████] (数据包丢失影响所有)
流B [██] (被TCP重传阻塞)
流C [███] (被TCP重传阻塞)

HTTP/3:任何层都没有阻塞

1
2
3
流A [████████████████] (独立)
流B [██] (立即完成)
流C [███] (立即完成)

数据库类比:理解权衡

正如数据库面临CAP定理约束,HTTP协议也在做权衡:

HTTP/1.1:像ACID数据库

  • 强一致性:请求按顺序处理
  • 可靠性:TCP保证传输
  • 简单性:易于实现和调试
  • 性能代价:顺序处理限制吞吐量

HTTP/2:像带事务的NoSQL

  • 更好性能:多路复用增加吞吐量
  • 维持一致性:仍依赖TCP顺序
  • 增加复杂性:二进制协议、流管理
  • 部分解决方案:仍受TCP队头阻塞影响

HTTP/3:像最终一致性系统

  • 最大性能:独立流处理
  • 放松顺序:QUIC在适当时允许乱序传输
  • 高可用性:连接迁移、更快恢复
  • 复杂性权衡:更复杂的流控制和拥塞管理

现实世界的实现挑战

对开发者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// HTTP/1.1: 简单但低效
fetch('/api/data1').then(() =>
fetch('/api/data2').then(() =>
fetch('/api/data3')
)
);

// HTTP/2: 高效多路复用
Promise.all([
fetch('/api/data1'),
fetch('/api/data2'),
fetch('/api/data3')
]); // 所有请求在单连接上多路复用

// HTTP/3: 相同API,更好性能
// 无需代码更改 - 浏览器自动处理QUIC

对基础设施团队

  • 负载均衡器:必须支持QUIC转发
  • CDN:需要HTTP/3边缘服务器
  • 监控:QUIC连接的新指标
  • 调试:二进制协议需要专门工具

未来:下一步是什么?

当前采用情况(2024年)

  • HTTP/3:约30%的网站支持
  • 主要参与者:Google、Cloudflare、Facebook引领采用
  • 浏览器支持:所有现代浏览器都支持HTTP/3
  • 移动影响:在移动网络上获益最大

新兴模式

  1. 混合协议:同时使用HTTP/2和HTTP/3
  2. 边缘计算:HTTP/3的低延迟完美适合边缘部署
  3. 物联网集成:QUIC的效率有利于资源受限设备
  4. 实时应用:基于HTTP/3的WebRTC实现更好的流媒体

结论:理解性能权衡

从HTTP/1.1到HTTP/3的演进反映了分布式系统思维的广泛演进。正如我们从单体数据库转向具有最终一致性的微服务,HTTP也从简单的请求-响应模式演进为复杂的多路复用、加密、移动优化协议。

关键要点:

  1. HTTP/1.1仍然是基础——简单、可靠、通用支持
  2. HTTP/2解决了应用层多路复用但受TCP约束
  3. HTTP/3代表了对传输协议的根本重新思考

对系统架构师的建议:

  • 考虑为移动重点应用使用HTTP/3
  • 监控目标市场的采用率
  • 规划渐进式迁移策略
  • 理解HTTP/2在未来几年仍将相关

大局观:
理解HTTP不在于记忆状态码,而在于内化协议演进中固化的性能权衡。HTTP/1.0打开了大门。HTTP/1.1使其在规模上可用。HTTP/2通过在单个TCP连接上多路复用流来推动效率。而基于UDP上QUIC构建的HTTP/3,终于突破了数十年的旧约束。

在我们这个微秒级至关重要、移动连接变化无常的互联世界中,这些协议改进不仅仅是技术好奇心——它们是使现代Web体验成为可能的基础。


作为网络工程师和系统架构师,我们的工作不仅是实现这些协议,更要理解它们的基本权衡,并为每个特定挑战选择正确的工具。Web性能的未来不在于任何单一协议,而在于理解何时以及如何有效地利用每一个协议。

AI提效 Claude Sonnet 4+ ChatGPT
此系列,多多少少都会用到AI。

背景

刚购置了一台服务器,准备付尾款了,但是因为服务器是全新的,只装了几台虚拟机,显卡啥的都还没有用,所以在付钱之前需要先测试一下,看看表现。

跑了一整晚,第二天,看着没问题就删掉了虚拟机,忘了截图。

服务器配置

  1. CPU: 2*96
  2. 内存:384G
  3. 硬盘:5*3.4T
  4. 显卡:2*RTX5880

准备工作

  1. 开启显卡直通
  2. 停掉所有虚拟机
  3. 新建一台临时虚拟机
    1. 分配所有CPU+350GB内存+500G硬盘+2*RTX5880
    2. 安装ubuntu 24.04

安装显卡对应的驱动

1
2
3
sudo apt install ubuntu-drivers-common
ubuntu-drivers devices
sudo ubuntu-drivers autoinstall

安装烤鸡需要的工具

  1. gpu-burn
  2. stress-ng

系统测试脚本( GPU + CPU + 内存 + IO综合烤机测试)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/bin/bash

set -e

# 设置工具路径(修改为你实际的 gpu_burn 目录)
GPU_BURN_PATH="$HOME/gpu-burn/gpu_burn"

# 检查是否存在命令
command -v stress-ng >/dev/null 2>&1 || { echo >&2 "需要安装 stress-ng:sudo apt install stress-ng -y"; exit 1; }
command -v fio >/dev/null 2>&1 || { echo >&2 "需要安装 fio:sudo apt install fio -y"; exit 1; }

# 检查 GPU_BURN 是否存在
if [ ! -f "$GPU_BURN_PATH" ]; then
echo "找不到 gpu_burn,可在 https://github.com/wilicc/gpu-burn 下载并编译"
exit 1
fi

echo "开始 GPU + CPU + 内存 + IO 综合烤机测试"

# GPU 测试
"$GPU_BURN_PATH" 86400 &
GPU_PID=$!

# CPU 测试
stress-ng --cpu 0 --timeout 24h --metrics-brief &
CPU_PID=$!

# 内存测试
stress-ng --vm 4 --vm-bytes 80% --timeout 24h --metrics-brief &
MEM_PID=$!

# 磁盘I/O测试
fio --name=randwrite --ioengine=libaio --iodepth=16 --rw=randwrite --bs=4k --direct=1 --size=4G --numjobs=4 --runtime=86400 --group_reporting &
IO_PID=$!

echo "测试开始,PID: GPU=$GPU_PID, CPU=$CPU_PID, MEM=$MEM_PID, IO=$IO_PID"
wait


监控几种方案

  1. watch命令
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 每5秒刷新GPU状态
    watch -n 5 nvidia-smi

    # 每10秒刷新(减少刷新频率)
    watch -n 10 nvidia-smi

    # 只看关键信息
    watch -n 5 "nvidia-smi --query-gpu=name,temperature.gpu,power.draw,utilization.gpu,memory.used --format=csv"

  2. nvtop
    1
    2
    3
    4
    5
    6
    7
    bash# 安装nvtop - GPU实时监控工具
    sudo apt update
    sudo apt install nvtop -y

    # 运行nvtop
    nvtop

3.安装netdata + GPU集成插件

AI提效 Claude Sonnet 4
此系列都是基于AI辅助实现

国外的

1
2
# 能访问国外的一件搞定,比如我的Bandwagon
curl -fsSL https://get.docker.com | sudo bash

国内的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/bin/bash

# Docker和Docker Compose完整安装脚本 for Ubuntu
# 支持Ubuntu 20.04/22.04/24.04及更新版本

set -e # 遇到错误时退出

echo "================================"
echo "Docker和Docker Compose安装脚本"
echo "================================"

# 检查是否为root用户
if [ "$EUID" -eq 0 ]; then
echo "请不要使用root用户运行此脚本"
echo "请使用普通用户运行:./install_docker.sh"
exit 1
fi

# 检查系统版本
if ! command -v lsb_release &> /dev/null; then
sudo apt update
sudo apt install -y lsb-release
fi

UBUNTU_VERSION=$(lsb_release -rs)
echo "检测到Ubuntu版本: $UBUNTU_VERSION"

# 备份原有的sources.list
echo "1. 备份并配置APT镜像源..."
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup.$(date +%Y%m%d_%H%M%S)

# 配置阿里云APT镜像源
sudo tee /etc/apt/sources.list > /dev/null <<EOF
# 阿里云镜像源
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs) main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-security main restricted universe multiverse
EOF

# 更新系统
echo "2. 更新系统包..."
sudo apt update

# 安装必要依赖
echo "3. 安装必要依赖..."
sudo apt install -y \
apt-transport-https \
ca-certificates \
curl \
software-properties-common \
gnupg \
lsb-release

# 添加Docker官方GPG密钥(使用阿里云镜像)
echo "4. 添加Docker GPG密钥..."
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 添加Docker仓库(使用阿里云镜像)
echo "5. 添加Docker仓库..."
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 更新包索引
echo "6. 更新包索引..."
sudo apt update

# 安装Docker
echo "7. 安装Docker..."
sudo apt install -y docker-ce docker-ce-cli containerd.io

# 启动Docker服务并设置开机自启
echo "8. 启动Docker服务..."
sudo systemctl start docker
sudo systemctl enable docker

# 将当前用户添加到docker组
echo "9. 配置用户权限..."
sudo usermod -aG docker $USER

# 配置Docker国内镜像加速器
echo "10. 配置Docker镜像加速器..."
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<EOF
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://dockerhub.icu",
"https://docker.chenby.cn",
"https://dockerproxy.com"
],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
EOF

# 重启Docker服务以应用配置
echo "11. 重启Docker服务..."
sudo systemctl daemon-reload
sudo systemctl restart docker

# 安装Docker Compose
echo "12. 安装Docker Compose..."

# 检查Ubuntu版本,选择合适的安装方法
if [[ $(echo "$UBUNTU_VERSION >= 22.04" | bc -l) -eq 1 ]] 2>/dev/null || [[ "$UBUNTU_VERSION" == "22.04" ]] || [[ "$UBUNTU_VERSION" > "22.04" ]]; then
# Ubuntu 22.04及以上版本,使用官方插件
echo " 使用官方插件安装Docker Compose..."
sudo apt install -y docker-compose-plugin
COMPOSE_CMD="docker compose"
else
# Ubuntu 20.04及以下版本,使用二进制文件安装
echo " 使用二进制文件安装Docker Compose..."
COMPOSE_VERSION="v2.24.1"
sudo curl -L "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
COMPOSE_CMD="docker-compose"
fi

# 等待Docker服务完全启动
echo "13. 等待Docker服务启动..."
sleep 3

# 测试安装
echo "14. 测试安装..."
echo "Docker版本:"
docker --version

echo "Docker Compose版本:"
if [[ "$COMPOSE_CMD" == "docker compose" ]]; then
docker compose version
else
docker-compose --version
fi

echo "Docker服务状态:"
sudo systemctl is-active docker

echo ""
echo "================================"
echo "安装完成!"
echo "================================"
echo ""
echo "重要提示:"
echo "1. 请重新登录系统或运行 'newgrp docker' 来使用户组权限生效"
echo "2. 然后运行 'docker run hello-world' 来测试安装"
echo ""
if [[ "$COMPOSE_CMD" == "docker compose" ]]; then
echo "Docker Compose命令: docker compose (注意是空格)"
echo "如果习惯使用 docker-compose,可以创建别名:"
echo "echo 'alias docker-compose=\"docker compose\"' >> ~/.bashrc && source ~/.bashrc"
else
echo "Docker Compose命令: docker-compose"
fi
echo ""
echo "常用命令:"
echo " docker --version # 查看Docker版本"
echo " $COMPOSE_CMD version # 查看Docker Compose版本"
echo " docker images # 查看镜像列表"
echo " docker ps # 查看运行中的容器"
echo " docker run hello-world # 测试Docker安装"
echo ""
echo "镜像加速器已配置,拉取镜像速度会更快!"
echo ""
echo "如需卸载Docker,请运行:"
echo "sudo apt purge docker-ce docker-ce-cli containerd.io docker-compose-plugin"
echo "sudo rm -rf /var/lib/docker /etc/docker"

如何优化Spring Boot应用以处理每秒百万请求

在当今高流量的互联网环境中,应用程序需要处理的请求量正在迅速增长。作为Java开发者,我们经常面临如何扩展Spring Boot应用以处理大量并发请求的挑战。本文将探讨如何将Spring Boot应用从处理每秒5万请求优化到每秒100万请求的实用策略,并提供详细的代码示例和实施步骤。

面临的挑战

想象一下这样的场景:你的团队被告知应用需要在三个月内实现20倍的流量增长,从每秒5万请求提升到每秒100万请求,而且硬件预算有限。这听起来几乎是不可能完成的任务。然而,通过深入分析性能瓶颈和应用一系列优化技术,我们可以达成这一目标。

在开始优化之前,我们需要了解系统的现状:

  • 基于Spring Boot 2.x的传统MVC架构
  • 使用内嵌的Tomcat服务器
  • 采用JPA进行数据库访问
  • 典型的N层架构(Controller > Service > Repository)
  • 高峰期出现频繁的GC暂停和超时错误

关键性能指标

在开始优化过程之前,了解我们的目标至关重要。成功优化后的应用应该达到以下性能指标:

  • 最大吞吐量:每秒120万请求
  • 平均响应时间:85毫秒(优化前为350毫秒)
  • 95%响应时间:120毫秒(优化前为850毫秒)
  • 峰值CPU使用率:60-70%(优化前为85-95%)
  • 内存使用:可用堆的50%(优化前为75%)
  • 数据库查询:通过缓存减少70%

优化策略

1. 采用响应式编程模型(Spring WebFlux)

最有影响力的变化是采用Spring WebFlux进行响应式编程。这不仅仅是简单的替换,而是需要重新思考应用程序的结构。首先,更新项目依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 将spring-boot-starter-web替换为spring-boot-starter-webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.7.5</version>
</dependency>

<!-- 使用Netty作为服务器 -->
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>1.0.24</version>
</dependency>

<!-- 引入Reactor测试支持 -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>

然后,改变控制器实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 传统Spring MVC控制器
@RestController
@RequestMapping("/api/products")
public class ProductController {

@Autowired
private ProductService productService;

@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}

@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}
}

// 响应式WebFlux控制器
@RestController
@RequestMapping("/api/products")
public class ReactiveProductController {

@Autowired
private ReactiveProductService productService;

@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProduct(@PathVariable Long id) {
return productService.getProductById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}

@GetMapping
public Mono<ResponseEntity<List<Product>>> getAllProducts() {
return productService.getAllProducts()
.collectList()
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}

服务层也需要相应改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 传统的服务实现
@Service
public class ProductService {
@Autowired
private ProductRepository repository;

public Product getProductById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}

public List<Product> getAllProducts() {
return repository.findAll();
}
}

// 响应式服务实现
@Service
public class ReactiveProductService {
@Autowired
private ReactiveProductRepository repository;

public Mono<Product> getProductById(Long id) {
return repository.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
}

public Flux<Product> getAllProducts() {
return repository.findAll();
}
}

2. 优化数据库访问

数据库通常是系统的主要瓶颈。以下是一些优化数据库访问的详细策略:

2.1 采用响应式数据库驱动

将传统的JDBC驱动替换为响应式数据库驱动:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 添加R2DBC依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>

<!-- MySQL的R2DBC驱动 -->
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>0.8.2.RELEASE</version>
</dependency>

配置R2DBC连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class R2dbcConfig {

@Bean
public ConnectionFactory connectionFactory() {
return MySqlConnectionFactory.from(
MySqlConnectionConfiguration.builder()
.host("localhost")
.port(3306)
.username("root")
.password("password")
.database("reactive_demo")
.build()
);
}

@Bean
public R2dbcEntityTemplate r2dbcEntityTemplate(ConnectionFactory connectionFactory) {
return new R2dbcEntityTemplate(connectionFactory);
}
}

创建响应式Repository:

1
2
3
4
5
6
7
8
9
public interface ReactiveProductRepository extends ReactiveCrudRepository<Product, Long> {

Flux<Product> findByCategory(String category);

Mono<Product> findByName(String name);

@Query("SELECT * FROM products WHERE price > :minPrice")
Flux<Product> findExpensiveProducts(BigDecimal minPrice);
}

2.2 实施多级缓存策略

引入Redis缓存:

1
2
3
4
5
<!-- Redis响应式支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

配置Redis缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
@EnableRedisRepositories
public class RedisConfig {

@Bean
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}

@Bean
public ReactiveRedisTemplate<String, Product> reactiveRedisTemplate(
ReactiveRedisConnectionFactory factory) {

StringRedisSerializer keySerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Product> valueSerializer =
new Jackson2JsonRedisSerializer<>(Product.class);

RedisSerializationContext.RedisSerializationContextBuilder<String, Product> builder =
RedisSerializationContext.newSerializationContext(keySerializer);

RedisSerializationContext<String, Product> context =
builder.value(valueSerializer).build();

return new ReactiveRedisTemplate<>(factory, context);
}
}

在服务层实现缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Service
public class CachedProductService {

private static final String CACHE_KEY_PREFIX = "product:";
private static final Duration CACHE_TTL = Duration.ofMinutes(10);

@Autowired
private ReactiveProductRepository repository;

@Autowired
private ReactiveRedisTemplate<String, Product> redisTemplate;

public Mono<Product> getProductById(Long id) {
String cacheKey = CACHE_KEY_PREFIX + id;

// 先尝试从缓存获取
return redisTemplate.opsForValue().get(cacheKey)
.switchIfEmpty(
// 缓存未命中,从数据库加载
repository.findById(id)
.flatMap(product -> {
// 将结果存入缓存
return redisTemplate.opsForValue()
.set(cacheKey, product, CACHE_TTL)
.thenReturn(product);
})
);
}

public Flux<Product> getAllProducts() {
// 对于列表类型的数据,可以使用不同的缓存策略
return repository.findAll();
}

// 清除缓存的方法,在更新产品时调用
public Mono<Void> invalidateCache(Long id) {
String cacheKey = CACHE_KEY_PREFIX + id;
return redisTemplate.delete(cacheKey);
}
}

2.3 优化SQL查询

使用索引优化:

1
2
3
4
-- 为常用查询字段添加索引
CREATE INDEX idx_product_category ON products(category);
CREATE INDEX idx_product_price ON products(price);
CREATE INDEX idx_product_name ON products(name);

优化查询方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 避免使用count(*)
public Mono<Boolean> existsByCategory(String category) {
return repository.findFirstByCategory(category)
.map(product -> true)
.defaultIfEmpty(false);
}

// 使用分页减少数据传输量
public Flux<Product> getProductsByCategory(String category, int page, int size) {
return repository.findByCategory(category,
PageRequest.of(page, size, Sort.by("name").ascending()));
}

// 使用投影只返回需要的字段
@Query("SELECT id, name, price FROM products WHERE category = :category")
Flux<ProductSummary> findProductSummariesByCategory(String category);

2.4 实现读写分离

配置主从数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Configuration
public class DatabaseConfig {

@Bean
@Primary
@Qualifier("masterConnectionFactory")
public ConnectionFactory masterConnectionFactory() {
return MySqlConnectionFactory.from(
MySqlConnectionConfiguration.builder()
.host("master-db.example.com")
.port(3306)
.username("master_user")
.password("master_pass")
.database("products")
.build()
);
}

@Bean
@Qualifier("slaveConnectionFactory")
public ConnectionFactory slaveConnectionFactory() {
return MySqlConnectionFactory.from(
MySqlConnectionConfiguration.builder()
.host("slave-db.example.com")
.port(3306)
.username("read_user")
.password("read_pass")
.database("products")
.build()
);
}

@Bean
public TransactionManager transactionManager(
@Qualifier("masterConnectionFactory") ConnectionFactory connectionFactory) {
return new R2dbcTransactionManager(connectionFactory);
}
}

创建读写分离的Repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Repository
public class ReadWriteProductRepository {

private final R2dbcEntityTemplate masterTemplate;
private final R2dbcEntityTemplate slaveTemplate;

public ReadWriteProductRepository(
@Qualifier("masterConnectionFactory") ConnectionFactory masterFactory,
@Qualifier("slaveConnectionFactory") ConnectionFactory slaveFactory) {
this.masterTemplate = new R2dbcEntityTemplate(masterFactory);
this.slaveTemplate = new R2dbcEntityTemplate(slaveFactory);
}

// 写操作使用主库
@Transactional
public Mono<Product> save(Product product) {
return masterTemplate.insert(Product.class)
.into("products")
.using(product);
}

// 读操作使用从库
public Mono<Product> findById(Long id) {
return slaveTemplate.select(Product.class)
.from("products")
.matching(Query.query(Criteria.where("id").is(id)))
.one();
}

public Flux<Product> findAll() {
return slaveTemplate.select(Product.class)
.from("products")
.all();
}
}

3. 配置调优

性能提升往往来自于正确的配置调整。以下是详细的配置优化步骤:

3.1 调整Netty服务器参数

application.yml中配置Netty参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
webflux:
base-path: /api

netty:
connection:
timeout: 5000 # 连接超时时间(毫秒)
worker:
count: 16 # worker线程数量, 建议设置为CPU核心数的2倍
boss:
count: 2 # boss线程数量
buffer:
size: 32768 # 缓冲区大小(字节)

在Java代码中自定义Netty配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class NettyConfig {

@Bean
public NettyReactiveWebServerFactory nettyReactiveWebServerFactory() {
NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory();

factory.addServerCustomizers(server -> {
HttpServer httpServer = (HttpServer) server;

// 设置更大的接收缓冲区大小
httpServer.tcpConfiguration(tcpServer ->
tcpServer.option(ChannelOption.SO_RCVBUF, 128 * 1024)
.option(ChannelOption.SO_SNDBUF, 128 * 1024)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.TCP_NODELAY, true));

return httpServer;
});

return factory;
}
}

3.2 优化JVM堆和垃圾收集器

application.properties中添加JVM参数,或者直接在启动脚本中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 堆内存设置
-Xms4g
-Xmx4g

# 年轻代大小,建议为堆的1/3到1/2
-Xmn2g

# GC相关设置,使用G1收集器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2
-XX:InitiatingHeapOccupancyPercent=70

# 禁用类元数据的GC,防止Full GC
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# 优化内存分配
-XX:+AlwaysPreTouch
-XX:+DisableExplicitGC

# 开启GC日志记录
-Xlog:gc*:file=logs/gc-%t.log:time,uptime,level,tags:filecount=5,filesize=100m

3.3 连接池优化

配置R2DBC连接池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
public class ConnectionPoolConfig {

@Bean
public ConnectionFactory connectionFactory() {
ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration.builder()
.connectionFactory(createConnectionFactory())
.maxIdleTime(Duration.ofMinutes(30))
.maxLifeTime(Duration.ofHours(2))
.maxAcquireTime(Duration.ofSeconds(3))
.initialSize(10)
.maxSize(50) // 根据负载调整
.minIdle(10)
.build();

return new ConnectionPool(configuration);
}

private ConnectionFactory createConnectionFactory() {
return MySqlConnectionFactory.from(
MySqlConnectionConfiguration.builder()
.host("localhost")
.port(3306)
.username("root")
.password("password")
.database("reactive_demo")
.connectTimeout(Duration.ofSeconds(3))
.build()
);
}
}

配置Redis连接池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
public class RedisConnectionConfig {

@Bean
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofMillis(100)) // 命令超时
.shutdownTimeout(Duration.ZERO) // 关闭超时
.clientOptions(ClientOptions.builder()
.socketOptions(SocketOptions.builder()
.connectTimeout(Duration.ofMillis(100)) // 连接超时
.keepAlive(true)
.build())
.build())
.clientResources(DefaultClientResources.builder()
.ioThreadPoolSize(4) // IO线程池大小
.computationThreadPoolSize(4) // 计算线程池大小
.build())
.build();

RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration();
serverConfig.setHostName("localhost");
serverConfig.setPort(6379);

return new LettuceConnectionFactory(serverConfig, clientConfig);
}
}

3.4 HTTP客户端调优

配置WebClient高性能连接池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)))
.responseTimeout(Duration.ofSeconds(5))
.compress(true)
.wiretap(true) // 开发环境调试使用,生产环境应关闭
.keepAlive(true)
.poolResources(ConnectionProvider.builder("custom")
.maxConnections(500)
.pendingAcquireTimeout(Duration.ofMillis(5000))
.pendingAcquireMaxCount(1000)
.maxIdleTime(Duration.ofMillis(8000))
.build());

return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.codecs(configurer ->
configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build();
}
}

4. 混合架构方法

不是所有的端点都需要改为响应式的。下面是混合架构的详细实现:

4.1 定义混合架构路由

使用Spring的路由器函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
public class RouterConfig {

// 高流量端点使用响应式处理
@Bean
public RouterFunction<ServerResponse> highTrafficRoutes(
ReactiveProductHandler productHandler) {

return RouterFunctions
.route(GET("/api/products").and(accept(APPLICATION_JSON)),
productHandler::getAllProducts)
.andRoute(GET("/api/products/{id}").and(accept(APPLICATION_JSON)),
productHandler::getProduct)
.andRoute(GET("/api/products/category/{category}").and(accept(APPLICATION_JSON)),
productHandler::getProductsByCategory);
}

// 低流量的管理端点保留传统MVC
@Bean
public WebMvcConfigurer mvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/admin").setViewName("admin");
registry.addViewController("/dashboard").setViewName("dashboard");
}
};
}
}

4.2 响应式处理器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Component
public class ReactiveProductHandler {

private final ReactiveProductService productService;

public ReactiveProductHandler(ReactiveProductService productService) {
this.productService = productService;
}

public Mono<ServerResponse> getProduct(ServerRequest request) {
Long productId = Long.valueOf(request.pathVariable("id"));

return productService.getProductById(productId)
.flatMap(product ->
ServerResponse.ok()
.contentType(APPLICATION_JSON)
.bodyValue(product))
.switchIfEmpty(ServerResponse.notFound().build());
}

public Mono<ServerResponse> getAllProducts(ServerRequest request) {
return productService.getAllProducts()
.collectList()
.flatMap(products ->
ServerResponse.ok()
.contentType(APPLICATION_JSON)
.bodyValue(products))
.switchIfEmpty(ServerResponse.notFound().build());
}

public Mono<ServerResponse> getProductsByCategory(ServerRequest request) {
String category = request.pathVariable("category");

return productService.getProductsByCategory(category)
.collectList()
.flatMap(products ->
ServerResponse.ok()
.contentType(APPLICATION_JSON)
.bodyValue(products))
.switchIfEmpty(ServerResponse.notFound().build());
}
}

4.3 实现逐步迁移策略

创建适配器以支持新旧代码互操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class ProductServiceAdapter {

private final ReactiveProductService reactiveService;
private final LegacyProductService legacyService;

@Autowired
public ProductServiceAdapter(
ReactiveProductService reactiveService,
LegacyProductService legacyService) {
this.reactiveService = reactiveService;
this.legacyService = legacyService;
}

// 将响应式服务适配到传统服务
public Product getProductSync(Long id) {
return reactiveService.getProductById(id).block();
}

// 将传统服务适配到响应式服务
public Mono<Product> getProductReactive(Long id) {
return Mono.fromCallable(() -> legacyService.getProductById(id))
.subscribeOn(Schedulers.boundedElastic());
}

// 批量操作适配
public List<Product> getAllProductsSync() {
return reactiveService.getAllProducts().collectList().block();
}

public Flux<Product> getAllProductsReactive() {
return Flux.defer(() -> Flux.fromIterable(legacyService.getAllProducts()))
.subscribeOn(Schedulers.boundedElastic());
}
}

5. 水平扩展与Kubernetes

在优化应用程序之后,可以通过Kubernetes进行智能水平扩展。以下是详细的配置和实现:

5.1 创建Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM openjdk:17-jdk-slim

WORKDIR /app

COPY target/reactive-product-service-0.0.1-SNAPSHOT.jar app.jar

# 设置JVM参数
ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -Xms1g -Xmx1g -XX:+AlwaysPreTouch"

EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD curl -f http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

5.2 创建Kubernetes部署配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
namespace: ecommerce
spec:
replicas: 3
selector:
matchLabels:
app: product-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
app: product-service
spec:
containers:
- name: product-service
image: my-registry/product-service:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1000m"
memory: "2Gi"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: SERVER_PORT
value: "8080"
- name: JAVA_OPTS
value: "-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -Xms1g -Xmx1g -XX:+AlwaysPreTouch"

5.3 创建服务和入口配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: v1
kind: Service
metadata:
name: product-service
namespace: ecommerce
spec:
selector:
app: product-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: product-service-ingress
namespace: ecommerce
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /api/products
pathType: Prefix
backend:
service:
name: product-service
port:
number: 80

5.4 配置自动扩缩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: product-service-hpa
namespace: ecommerce
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: product-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 60

5.5 实现优雅降级机制

在应用程序中添加断路器模式:

1
2
3
4
5
6
7
8
9
10
11
<!-- 添加Resilience4j依赖 -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>1.7.1</version>
</dependency>

配置断路器:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ResilienceConfig {

@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slowCallRateThreshold(50)
.slowCallDurationThreshold(Duration.ofSeconds(1))
.permittedNumberOfCallsInHalfOpenState(10)
.minimumNumberOfCalls(20)
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.

参考

https://medium.com/javarevisited/how-i-optimized-a-spring-boot-application-to-handle-1m-requests-second-0cbb2f2823ed

AI 第五篇

这几天想起翻起来Google之前新发布的白皮书《Prompt Engineering》,学习下再简单用Claude提取下主要内容记录下。

掌握提示工程:打造AI高效交互的艺术与科学

在人工智能迅猛发展的今天,提示工程(Prompt Engineering)已成为连接人类意图与AI输出的关键桥梁。Google最新发布的白皮书《Prompt Engineering》为我们提供了全面而深入的指南,让我们一起探索如何通过精心设计的提示让大语言模型发挥最大潜力。

提示工程的本质

提示工程是设计高质量提示的过程,旨在引导大语言模型(LLM)产生准确、相关且有用的输出。正如白皮书中所强调的:

“你不需要成为数据科学家或机器学习工程师——每个人都可以编写提示。然而,设计最有效的提示可能很复杂。”

大语言模型本质上是一个预测引擎,它接收序列文本作为输入,然后基于训练数据预测下一个词元(token)应该是什么。当你编写提示时,你正在尝试设置LLM以预测正确的词元序列。

理解大语言模型的输出配置

在开始提示工程之前,了解如何配置LLM输出至关重要:

输出长度控制

控制模型生成的词元数量是一项重要配置。生成更多词元需要更多计算资源,导致能耗增加、响应时间潜在变慢,以及成本提高。限制输出长度对于某些提示技术特别重要,如ReAct(推理与行动),在这种情况下,LLM会在你想要的响应后继续生成无用词元。

采样控制机制

LLM不会正式预测单个词元,而是为每个可能的下一个词元分配概率。这些词元概率然后被采样以确定下一个生成的词元。

温度参数

温度控制词元选择中的随机性程度:

  • 低温度(接近0):适用于需要确定性响应的提示,选择概率最高的词元
  • 高温度(接近1或更高):产生更多样化或意外的结果,使所有词元被选中的可能性更加均等

Top-K和Top-P(核采样)

这两种设置限制预测的下一个词元只能来自概率最高的词元:

  • Top-K采样:从模型预测分布中选择K个最可能的词元
  • Top-P采样:选择累积概率不超过特定值(P)的最可能词元

综合配置时,一般起点建议:

  • 相对连贯且有创意的结果:温度0.2,top-P 0.95,top-K 30
  • 特别有创意的结果:温度0.9,top-P 0.99,top-K 40
  • 较少创意的结果:温度0.1,top-P 0.9,top-K 20
  • 只有一个正确答案的任务(如数学问题):温度0

提示技术详解

零样本提示(Zero-shot)

最简单的提示类型,仅提供任务描述和一些文本让LLM开始。适用于模型已经理解的简单任务。

示例

1
2
3
将电影评论分类为积极、中性或消极。
评论:"她"是一项揭示人工智能发展方向的研究,如果AI继续不受控制地发展,人类将走向何方。我希望有更多像这样的杰作。
情感:

单样本和少样本提示(One-shot & Few-shot)

通过在提示中提供一个或多个示例,帮助模型理解所需的输出格式或模式。这种方法特别适用于需要特定输出结构的任务。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
解析客户的披萨订单为有效的JSON:

例子1:
我想要一个小号披萨,配芝士、番茄酱和意大利香肠。
JSON响应:
{
"size": "small",
"type": "normal",
"ingredients": [["cheese", "tomato sauce", "pepperoni"]]
}

例子2:
我想要一个大号披萨,配番茄酱、罗勒和马苏里拉奶酪。
{
"size": "large",
"type": "normal",
"ingredients": [["tomato sauce", "basil", "mozzarella"]]
}

现在,我想要一个大号披萨,一半是芝士和马苏里拉奶酪,另一半是番茄酱、火腿和菠萝。
JSON响应:

少样本提示的示例数量取决于任务复杂性、示例质量和模型能力。一般建议至少使用3-5个示例,复杂任务可能需要更多。

系统、角色和上下文提示

这三种技术都用于引导LLM生成文本,但侧重点不同:

系统提示

设置语言模型的整体上下文和目的,定义模型应该做什么的”大局观”。

示例

1
2
3
将电影评论分类为积极、中性或消极。只返回大写的标签。
评论:"她"是一部揭示AI不受控制进化可能导致人类走向何方的令人不安的研究。它太令人不安以至于我无法观看完。
情感:

角色提示

为语言模型分配特定角色或身份,帮助模型生成与该角色一致的响应。

示例

1
2
3
我希望你扮演旅游指南的角色。我会告诉你我的位置,你将为我推荐3个附近可以参观的地方。在某些情况下,我还会告诉你我想参观的地点类型。
我的建议:"我在阿姆斯特丹,我只想参观博物馆。"
旅游建议:

上下文提示

提供与当前对话或任务相关的具体细节或背景信息,帮助模型理解要求的细微差别。

示例

1
2
上下文:你正在为一个关于80年代街机视频游戏的博客撰写文章。
建议3个可以撰写文章的主题,并简要描述这些文章应该包含什么内容。

退一步提示(Step-back)

先考虑与特定任务相关的一般问题,然后将答案用于后续提示,激活相关背景知识和推理过程。

示例

1
2
3
步骤1:基于流行的第一人称射击游戏,什么是5个虚构的关键设置有助于第一人称射击游戏中挑战和引人入胜的关卡故事线?

步骤2:利用上面的主题,为第一人称射击游戏编写一个新关卡的一段故事情节,既具挑战性又引人入胜。

思维链提示(Chain of Thought)

通过生成中间推理步骤来改善推理能力,特别适用于数学问题、逻辑推理等需要分步思考的任务。

示例

1
2
3
4
5
6
当我3岁时,我的伙伴是我年龄的3倍。现在,我20岁了。我的伙伴多大?让我们一步一步思考。

1. 当我3岁时,我的伙伴是3*3=9岁。
2. 这意味着我的伙伴比我大6岁。
3. 现在我20岁了,所以我的伙伴应该是20+6=26岁。
因此,我的伙伴现在26岁。

思维链提示可以与单样本或少样本结合使用,效果更佳:

1
2
3
4
5
问题:当我哥哥2岁时,我的年龄是他的两倍。现在我40岁了。我哥哥多大?让我们一步一步思考。
答案:当我哥哥2岁时,我是2*2=4岁。这意味着我比他大2岁。现在我40岁了,所以我哥哥是40-2=38岁。答案是38。

问题:当我3岁时,我的伙伴是我年龄的3倍。现在,我20岁了。我的伙伴多大?让我们一步一步思考。
答案:

自我一致性(Self-consistency)

结合抽样和多数投票,生成多样化的推理路径并选择最一致的答案。这种方法改进了LLM在各种任务中的准确性和回答一致性。

过程

  1. 使用相同提示多次,生成不同的推理路径
  2. 从每个生成的响应中提取答案
  3. 选择最常见的答案

思维树(Tree of Thoughts)

允许LLM同时探索多个不同的推理路径,通过在树的不同节点分支来解决问题。特别适合需要探索的复杂任务。

推理与行动(ReAct)

结合自然语言推理与执行外部工具操作的能力,使LLM能够解决复杂任务。例如,查询网络获取信息,然后基于结果继续推理。

Python示例

1
2
3
4
5
6
7
8
9
10
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.llms import VertexAI

prompt = "Metallica乐队成员有多少个孩子?"
llm = VertexAI(temperature=0.1)
tools = load_tools(["serpapi"], llm=llm)
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
agent.run(prompt)

自动提示工程(Automatic Prompt Engineering)

利用LLM自身生成更多提示,评估它们,并可能修改好的提示,然后重复这个过程:

  1. 编写生成输出变体的提示
  2. 根据选定指标评估所有指令候选
  3. 选择评估分数最高的指令候选

代码提示技巧

编写代码提示

LLM可以帮助开发人员加速编写代码过程,实现各种自动化任务:

示例

1
用Bash编写一个代码片段,询问文件夹名称。然后将该文件夹的内容重命名,在文件名前加上"draft"。

解释代码提示

LLM也可以帮助理解他人的代码:

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
解释以下Bash代码:
#!/bin/bash
echo "Enter the folder name: "
read folder_name
if [ ! -d "$folder_name" ]; then
echo "Folder does not exist."
exit 1
fi
files=( "$folder_name"/* )
for file in "${files[@]}"; do
new_file_name="draft_$(basename "$file")"
mv "$file" "$new_file_name"
done
echo "Files renamed successfully."

翻译代码提示

将代码从一种语言翻译到另一种语言:

示例

1
2
将以下Bash代码翻译为Python片段:
[Bash代码]

调试和审查代码提示

修复代码中的错误并提供改进建议:

示例

1
2
3
4
5
6
7
8
以下Python代码报错:
Traceback (most recent call last):
File "/Users/leeboonstra/Documents/test_folder/rename_files.py", line 7, in <module>
text = toUpperCase(prefix)
NameError: name 'toUpperCase' is not defined

调试错误并解释如何改进代码。
[Python代码]

提示工程最佳实践

提供示例

在提示中提供示例是最重要的最佳实践之一,它作为强大的教学工具,展示期望的输出或类似响应,提高模型输出的准确性、风格和语调。

设计简单

提示应简洁、清晰、易于理解。如果对你来说已经令人困惑,对模型来说也可能如此。避免使用复杂语言和提供不必要的信息。

示例改进

1
2
3
4
5
改进前:
我现在正在纽约参观,想了解更多关于好地方的信息。我带着两个3岁的孩子。我们假期应该去哪里?

改进后:
作为旅游指南,描述纽约曼哈顿适合带3岁孩子参观的好地方。

明确输出要求

具体说明所需的输出形式。简洁的指令可能无法充分引导LLM或过于笼统。

示例

1
生成一篇关于5大视频游戏主机的3段落博客文章。博客文章应该信息丰富且引人入胜,并以对话风格撰写。

使用指令而非约束

指令和约束在提示中用于引导LLM输出,但研究表明,专注于积极指令比严重依赖约束更有效:

  • 指令:直接传达期望的结果
  • 约束:设置响应的限制或边界

示例

1
2
3
4
5
推荐做法:
生成一段关于5大视频游戏主机的博客文章。只讨论主机、制造公司、发布年份和总销量。

避免做法:
生成一段关于5大视频游戏主机的博客文章。不要列出视频游戏名称。

控制最大令牌长度

通过配置或在提示中明确要求特定长度来控制生成的LLM响应长度:

1
用推特长度的消息解释量子物理学。

在提示中使用变量

使用变量使提示更加动态和可重用,特别是在集成到应用程序中时:

1
2
3
4
5
变量:
{city} = "阿姆斯特丹"

提示:
你是一名旅游指南。告诉我关于这个城市的一个事实:{city}

实验输入格式和写作风格

不同的模型、配置、提示格式、词汇选择可能产生不同结果。因此,尝试不同的提示属性如风格、词汇选择和提示类型(零样本、少样本、系统提示)非常重要。

例如,关于Sega Dreamcast的提示可以表述为:

  • 问题:Sega Dreamcast是什么,为什么它是如此革命性的主机?
  • 陈述:Sega Dreamcast是世嘉在1999年发布的第六代视频游戏主机…
  • 指令:写一段描述Sega Dreamcast主机并解释为什么它如此革命性的段落。

分类任务中混合类别

对于分类任务的少样本提示,确保混合可能的响应类别,避免模型只是记住示例顺序而非学习每个类别的关键特征。

适应模型更新

随时了解模型架构变化、添加的数据和新功能。尝试新版模型并调整提示以更好地利用新特性。

尝试不同输出格式

考虑实验输出格式,对于非创意任务(如提取、选择、解析、排序、排名或分类数据),尝试使用JSON或XML等结构化格式。

JSON输出优势

  • 始终以相同风格返回
  • 专注于您想要接收的数据
  • 降低幻觉风险
  • 使其关系感知
  • 获得数据类型
  • 可以排序

处理JSON输出的特殊考虑

JSON修复

虽然以JSON格式返回数据提供了众多优势,但也存在一些缺点。JSON的结构化特性虽然有利于解析和在应用程序中使用,但需要比纯文本更多的词元,导致处理时间增加和成本提高。此外,JSON的冗长可能轻易消耗整个输出窗口,特别是当生成由于词元限制而突然中断时,这通常会导致缺少关键的闭合大括号或方括号,使输出无法使用。

幸运的是,json-repair库(可在PyPI上获取)在这些情况下非常宝贵。此库智能地尝试自动修复不完整或格式错误的JSON对象。

使用JSON模式

JSON模式定义了JSON输入的预期结构和数据类型。通过提供模式,您为LLM提供了一个清晰的数据蓝图,帮助它专注于相关信息并减少误解输入的风险。此外,模式可以帮助建立不同数据片段之间的关系,甚至通过包含特定格式的日期或时间戳字段使LLM”时间感知”。

与其他提示工程师合作实验

如果您需要尝试制定良好的提示,找多人进行尝试可能会有所帮助。当每个人都遵循最佳实践时,您会看到不同提示尝试之间的性能差异。

思维链最佳实践

  • 在推理之后放置答案是必需的,因为推理的生成会改变模型在预测最终答案时获得的词元
  • 使用思维链和自我一致性时,需要能够从提示中提取最终答案,与推理分开
  • 对于思维链提示,将温度设置为0
  • 思维链提示基于贪婪解码,根据语言模型分配的最高概率预测序列中的下一个词。通常,在使用推理得出最终答案时,可能只有一个正确答案,因此温度应始终设置为0

记录提示尝试

详细记录您的提示尝试至关重要,这样您可以随着时间的推移了解什么有效,什么无效。建议使用谷歌表格,包含以下字段:

  • 名称
  • 目标
  • 模型
  • 温度
  • 令牌限制
  • Top-K
  • Top-P
  • 提示
  • 输出

除了这些字段外,跟踪提示版本(迭代)、结果是否OK/NOT OK/SOMETIMES OK的字段,以及反馈字段也很有帮助。

结语

提示工程是一门艺术,也是一门科学,需要持续的实践和改进。通过掌握本白皮书中详述的各种技术和最佳实践,您可以有效地引导大语言模型产生准确、相关且有用的输出,充分发挥AI的潜力,无论是用于个人项目还是企业级应用。

记住,提示工程是一个迭代过程。设计并测试不同的提示,分析并记录结果。根据模型的表现优化您的提示。不断实验,直到达到所需的输出。当您更换模型或模型配置时,回顾并继续使用先前使用的提示进行实验。

通过这种方法,您将能够与AI系统建立更有效、更精确的交互,释放其真正的潜力。

AI 第四篇

Cursor Memory Bank 指南:让 AI 拥有持久的项目记忆

在与 AI 协作编程,尤其是进行大型项目或跨多个会话工作时,我们常常会遇到一个挑战:AI(如 Cursor)的”记忆”通常是短暂的,它可能在两次交互之间忘记之前的上下文、项目目标或技术决策。为了解决这个问题,社区借鉴了 Cline 的 Memory Bank 概念,并将其适配到了 Cursor 中,旨在为 AI 提供一个持久化的”项目记忆库”。

本文将介绍 Memory Bank 的由来、原理、作用以及如何在 Cursor 中配置和使用它。

以下为论坛帖子截图,展示了 Memory Bank 和 Plan/Act 模式的设置步骤:

[图像描述:一张论坛帖子的截图,标题为 “How to add Cline Memory Bank feature to your cursor”。内容分为三部分:1. 添加 Plan/Act 模式到 cursor agent,包含创建 .cursor/rules/core.mdc 文件和规则代码;2. 添加 Memory Bank 到 cursor agent,引用 Cline 文档并包含创建 .cursor/rules/memory-bank.mdc 文件和规则代码,展示了 Memory Bank 的文件结构图;3. 设置 Memory Bank,包括创建 memory-bank/ 文件夹和要求 Cursor agent 初始化。]

1. Memory Bank 的由来与原理

Memory Bank 的概念受到 Cline Memory Bank 的启发,其核心思想是解决 AI 在不同会话间记忆重置的问题。

原理:

  • 应对记忆重置: Cursor 作为一个 AI 助手,其内部状态(记忆)在会话结束后或重新启动时通常会丢失。Memory Bank 通过外部文件的形式,为 AI 提供了一个持久化的信息存储。
  • 结构化文档: 它不是随意的信息堆砌,而是由一组结构化的 Markdown 文件组成,形成一个清晰的信息层级。这有助于 AI 系统性地理解项目。
  • 强制读取: 最关键的一点是,配置规则后,Cursor 被要求在每次开始新任务时,都必须首先阅读 Memory Bank 中的所有核心文件。这确保了 AI 在开始工作前能获取到必要的项目背景和当前状态,就像人类开发者回顾项目文档一样。
  • 信息层级: Memory Bank 文件之间存在依赖关系,例如 projectbrief.md 是基础,定义了项目核心目标,其他文件在此基础上展开。
1
2
3
4
5
6
7
8
9
10
flowchart TD
PB[projectbrief.md] --> PC[productContext.md]
PB --> SP[systemPatterns.md]
PB --> TC[techContext.md]

PC --> AC[activeContext.md]
SP --> AC
TC --> AC

AC --> P[progress.md]

2. Memory Bank 是做什么的?

Memory Bank 的主要目标是成为 Cursor 理解和参与项目的 唯一可靠信息源。它解决了 AI 短期记忆的问题,确保开发过程的连续性和一致性。具体作用包括:

  • 提供项目背景: 定义项目的核心需求、目标、要解决的问题 (projectbrief.md, productContext.md)。
  • 记录技术决策: 保存系统架构、关键技术选型、设计模式、技术栈、依赖和环境设置 (systemPatterns.md, techContext.md)。
  • 追踪当前状态: 记录当前的工作焦点、最近的变更、下一步计划、已知问题和项目进展 (activeContext.md, progress.md)。
  • 维护项目文档: 成为项目事实上的”活文档”,随着项目的进展而更新。
  • 提高 AI 效率: 通过提供完整的上下文,减少 AI 的猜测和重复提问,使其能更准确、高效地执行任务。
  • 组织复杂信息: 可以创建额外的文件或子目录来组织特定功能、API、测试策略等复杂信息。

核心文件及其职责:

  1. projectbrief.md: 项目基础,定义核心需求和目标。
  2. productContext.md: 项目存在的意义,解决的问题,用户体验目标。
  3. activeContext.md: 当前工作焦点,最近变更,下一步计划。
  4. systemPatterns.md: 系统架构,关键技术决策,设计模式。
  5. techContext.md: 使用的技术,开发设置,技术限制,依赖项。
  6. progress.md: 已完成功能,待办事项,当前状态,已知问题。

3. Memory Bank 如何配合 Cursor 使用?

在 Cursor 中启用和使用 Memory Bank 需要以下步骤:

  1. 创建规则文件 (.cursor/rules/memory-bank.mdc):

    • 在你的项目根目录下创建 .cursor/rules 文件夹(如果尚不存在)。
    • rules 文件夹内创建一个名为 core.mdc 的文件。
    • rules 文件夹内创建一个名为 memory-bank.mdc 的文件。
      • 提示: 有些系统可能不允许直接创建以 . 开头的文件或 .mdc 后缀的文件,可以先创建为 memory-bank.md,粘贴内容后,再重命名为 memory-bank.mdc
    • 将以下规则内容粘贴到 core.mdc 文件中:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      ---
      description:
      globs:
      alwaysApply: true
      ---
      ## Core Rules

      You have two modes of operation:

      1. Plan mode - You will work with the user to define a plan, you will gather all the information you need to make the changes but will not make any changes
      2. Act mode - You will make changes to the codebase based on the plan

      - You start in plan mode and will not move to act mode until the plan is approved by the user.
      - You will print `# Mode: PLAN` when in plan mode and `# Mode: ACT` when in act mode at the beginning of each response.
      - Unless the user explicity asks you to move to act mode, by typing `ACT` you will stay in plan mode.
      - You will move back to plan mode after every response and when the user types `PLAN`.
      - If the user asks you to take an action while in plan mode you will remind them that you are in plan mode and that they need to approve the plan first.
      - When in plan mode always output the full updated plan in every response.
    • 将以下规则内容粘贴到 memory-bank.mdc 文件中:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      ---
      description:
      globs:
      alwaysApply: true
      ---
      # Cursor's Memory Bank

      I am Cursor, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.

      ## Memory Bank Structure

      The Memory Bank consists of required core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:

      \```mermaid
      flowchart TD
      PB[projectbrief.md] --> PC[productContext.md]
      PB --> SP[systemPatterns.md]
      PB --> TC[techContext.md]

      PC --> AC[activeContext.md]
      SP --> AC
      TC --> AC

      AC --> P[progress.md]
      \```

      ### Core Files (Required)
      1. `projectbrief.md`
      - Foundation document that shapes all other files
      - Created at project start if it doesn't exist
      - Defines core requirements and goals
      - Source of truth for project scope

      2. `productContext.md`
      - Why this project exists
      - Problems it solves
      - How it should work
      - User experience goals

      3. `activeContext.md`
      - Current work focus
      - Recent changes
      - Next steps
      - Active decisions and considerations

      4. `systemPatterns.md`
      - System architecture
      - Key technical decisions
      - Design patterns in use
      - Component relationships

      5. `techContext.md`
      - Technologies used
      - Development setup
      - Technical constraints
      - Dependencies

      6. `progress.md`
      - What works
      - What's left to build
      - Current status
      - Known issues

      ### Additional Context
      Create additional files/folders within memory-bank/ when they help organize:
      - Complex feature documentation
      - Integration specifications
      - API documentation
      - Testing strategies
      - Deployment procedures

      ## Core Workflows

      ### Plan Mode
      \```mermaid
      flowchart TD
      Start[Start] --> ReadFiles[Read Memory Bank]
      ReadFiles --> CheckFiles{Files Complete?}

      CheckFiles -->|No| Plan[Create Plan]
      Plan --> Document[Document in Chat]

      CheckFiles -->|Yes| Verify[Verify Context]
      Verify --> Strategy[Develop Strategy]
      Strategy --> Present[Present Approach]
      \```

      ### Act Mode
      \```mermaid
      flowchart TD
      Start[Start] --> Context[Check Memory Bank]
      Context --> Update[Update Documentation]
      Update --> Rules[Update .cursor/rules if needed]
      Rules --> Execute[Execute Task]
      Execute --> Document[Document Changes]
      \```

      ## Documentation Updates

      Memory Bank updates occur when:
      1. Discovering new project patterns
      2. After implementing significant changes
      3. When user requests with **update memory bank** (MUST review ALL files)
      4. When context needs clarification

      \```mermaid
      flowchart TD
      Start[Update Process]

      subgraph Process
      P1[Review ALL Files]
      P2[Document Current State]
      P3[Clarify Next Steps]
      P4[Update .cursor/rules]

      P1 --> P2 --> P3 --> P4
      end

      Start --> Process
      \```

      Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.

      ## Project Intelligence (.cursor/rules)

      The .cursor/rules file is my learning journal for each project. It captures important patterns, preferences, and project intelligence that help me work more effectively. As I work with you and the project, I'll discover and document key insights that aren't obvious from the code alone.

      \```mermaid
      flowchart TD
      Start{Discover New Pattern}

      subgraph Learn [Learning Process]
      D1[Identify Pattern]
      D2[Validate with User]
      D3[Document in .cursor/rules]
      end

      subgraph Apply [Usage]
      A1[Read .cursor/rules]
      A2[Apply Learned Patterns]
      A3[Improve Future Work]
      end

      Start --> Learn
      Learn --> Apply
      \```

      ### What to Capture
      - Critical implementation paths
      - User preferences and workflow
      - Project-specific patterns
      - Known challenges
      - Evolution of project decisions
      - Tool usage patterns

      The format is flexible - focus on capturing valuable insights that help me work more effectively with you and the project. Think of .cursor/rules as a living document that grows smarter as we work together.

      REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.

  2. 创建 Memory Bank 目录 (memory-bank/):

    • 在你的项目根目录下创建一个名为 memory-bank 的文件夹。
  3. 初始化 Memory Bank 文件:

    • memory-bank/ 文件夹内,创建上述提到的核心 Markdown 文件:
      • projectbrief.md
      • productContext.md
      • activeContext.md
      • systemPatterns.md
      • techContext.md
      • progress.md
    • 关键一步: 在 Cursor 聊天中,向 AI 发出指令:”initialize memory bank“。Cursor 会尝试根据当前项目理解(可能需要你提供信息)来填充这些文件的初始内容。之后你需要检查并完善这些内容。
  4. 维护和更新:

    • 时机: 当发现新的项目模式、完成重要功能或架构变更后、需要澄清上下文时,或者你觉得 AI 的理解有偏差时,都应该更新 Memory Bank。
    • 方式:
      • 可以直接编辑 memory-bank/ 中的 Markdown 文件。
      • 可以明确指示 Cursor:”update memory bank“。根据规则,它会重新审视所有 Memory Bank 文件,并根据当前状态和你的指示进行更新(尤其关注 activeContext.mdprogress.md)。
  5. (可选)配合 Plan/Act 模式:

    • Memory Bank 可以与 Plan/Act 模式 结合使用(需要额外配置 core.mdc 规则)。在 Plan 模式下,Cursor 会先读取 Memory Bank 来制定计划;在 Act 模式执行任务后,它可能会被指示更新 Memory Bank(尤其是 progress.mdactiveContext.md)。

部分效果

alt text

总结

Cursor Memory Bank 提供了一种有效的机制,通过持久化的结构化文档来弥补 AI 短暂记忆的不足。通过强制 AI 在每次任务开始时读取这些信息,可以显著提高其对项目上下文的理解,从而提升协作效率和代码质量。虽然需要投入一些精力来初始化和维护,但对于复杂或长期的项目来说,这种投入往往是值得的。

参考来源:

  1. How to add Cline Memory Bank feature to your cursor - Cursor Community Forum
  2. cline-memory-bank

面试知识梳理 第一篇

背景

最近找工作,有些大数据岗位我想投,但是奈何之前的工作内容大数据不是主业,大数据经验不够看,我最早要追溯到15年当时spark+hive,然后17年的storm+hbase,到最近的flink+ck,我觉得我努把力看能不能够一够大数据相关的岗位。

基础环境准备

把我给媳妇儿配的打LOL的电脑,偷偷拿来用一用,当成小型服务器,反正性能对LOL来说,很过剩了,不影响。

我之前鼓捣其它技术的时候就在电脑上装了虚拟机,所以也不折腾了,直接装个ubuntu,然后装个docker+docker compose,就差不多了。

docker镜像源

单独说下,因为docker默认用的国外的镜像源所以安装后几乎是不可用的,这时候需要配置国内的镜像。
要注意验证镜像源,比如通过curl等命令,看是否能正常访问是否能免验证访问,我就是被阿里云的镜像加速器耽搁了小半小时,就是按照官方的配置始终403,最后才发现,原理阿里前几个月更新了协议,大概意思是,不再支持外部直接用加速镜像,而是支持阿里云本身的产品使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 验证镜像源
curl 镜像源
# 2. 添加镜像源
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://xxxx"
]
}
EOF

# 3. 使其生效
sudo systemctl daemon-reload
sudo systemctl restart docker

# 4. 查看镜像是否修改成功
docker info

# 5. 拉取镜像验证
docker pull xxx

安装CK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 1. 获取ck镜像
docker pull clickhouse/clickhouse-server
# 2. 添加ck需要的目录
mkdir -p /data/clickhouse/data /data/clickhouse/config /data/clickhouse/logs

# 3. ck的配置
cat > /data/clickhouse/config/config.xml << EOF
<?xml version="1.0"?>
<yandex>
<logger>
<level>information</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
</logger>

<http_port>8123</http_port>
<tcp_port>9000</tcp_port>
<interserver_http_port>9009</interserver_http_port>

<listen_host>0.0.0.0</listen_host>

<max_connections>4096</max_connections>
<keep_alive_timeout>10</keep_alive_timeout>
<max_concurrent_queries>100</max_concurrent_queries>
<uncompressed_cache_size>8589934592</uncompressed_cache_size>
<mark_cache_size>5368709120</mark_cache_size>

<path>/var/lib/clickhouse/</path>
<tmp_path>/var/lib/clickhouse/tmp/</tmp_path>

<user_directories>
<users_xml>
<path>/etc/clickhouse-server/users.xml</path>
</users_xml>
</user_directories>

<timezone>UTC</timezone>
</yandex>
EOF

# 4. ck用户管理
cat > /data/clickhouse/config/users.xml << EOF
<?xml version="1.0"?>
<yandex>
<users>
<default>
<password>yourpassword</password>
<networks>
<ip>::/0</ip>
</networks>
<profile>default</profile>
<quota>default</quota>
</default>
</users>

<profiles>
<default>
<max_memory_usage>10000000000</max_memory_usage>
<use_uncompressed_cache>0</use_uncompressed_cache>
<load_balancing>random</load_balancing>
</default>
</profiles>

<quotas>
<default>
<interval>
<duration>3600</duration>
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</yandex>
EOF

# 5.运行容器

docker run -d \
--name clickhouse-server \
--ulimit nofile=262144:262144 \
-p 8123:8123 \
-p 9000:9000 \
-p 9009:9009 \
-v /data/clickhouse/data:/var/lib/clickhouse \
-v /data/clickhouse/config/config.xml:/etc/clickhouse-server/config.xml \
-v /data/clickhouse/config/users.xml:/etc/clickhouse-server/users.xml \
-v /data/clickhouse/logs:/var/log/clickhouse-server \
--restart=always \
clickhouse/clickhouse-server:latest

# 6. 测试是否可用(内部)
docker exec -it clickhouse-server clickhouse-client --password yourpassword

# 7.暴露到外部可访问,由于不想每次run都写一长串,也为了后续方便管理其它容器,把docker compose装上
apt update
apt install -y docker-compose
# 8.compose文件编写,别忘了暴露environment
nano /data/clickhouse/docker-compose.yml

version: '3'
services:
clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: clickhouse-server
restart: always
ports:
- "8123:8123"
- "9000:9000"
- "9009:9009"
volumes:
- /data/clickhouse/data:/var/lib/clickhouse
- /data/clickhouse/config/config.xml:/etc/clickhouse-server/config.xml
- /data/clickhouse/config/users.xml:/etc/clickhouse-server/users.xml
- /data/clickhouse/logs:/var/log/clickhouse-server
environment:
- CLICKHOUSE_USER=default
- CLICKHOUSE_PASSWORD=xxxx
ulimits:
nofile:
soft: 262144
hard: 262144
# 删除ck容器后重启
cd /data/clickhouse
docker-compose up -d
# 9. 看是否正常返回
curl "http://xx:8123/?user=default&password=xx&query=SELECT%201"


还有待续….

参考

https://www.coderjia.cn/archives/dba3f94c-a021-468a-8ac6-e840f85867ea
https://hub.docker.com/r/clickhouse/clickhouse-server/

AI 第四篇

引言

最近准备面试嘛,看到好些JD里,特别是关于大模型的JD,里面有个技能要求Prompt Engineering。刚好我也有兴趣,平时也是claude.ai和chatgpt、deepseek的重度用户,美元都花了好些,问题问的多了,慢慢的知道问题描述的准确性与预期的答案相关性确实很大。确实感觉Prompt Engineering(提示词工程)已经成为一项重要技能。无论你是开发者、内容创作者还是普通用户,掌握这项技能都能帮助你更有效地与AI交流,获得更满意的结果。刚好借此JD机会,更深入的学习下怎么才能写好Prompt。

什么是Prompt Engineering?

基本概念解释

Prompt Engineering是指设计和优化输入到AI模型(如ChatGPT、Claude等)的提示词的过程,目的是引导AI生成更准确、更符合预期的输出内容。

简单来说,就像我们与人交流时,清晰表达自己的需求会得到更好的回应一样,与AI的交流也需要”说人话”,而Prompt Engineering就是学习如何更好地”对AI说话”的艺术。

为什么Prompt Engineering很重要?

  • 节省时间:好的提示词能直接获得理想结果,减少反复尝试的时间
  • 提高质量:精心设计的提示词能显著提升AI输出的质量和准确度
  • 解锁潜能:掌握高级技巧后,你可以让AI完成更复杂的任务

Prompt Engineering的基础知识

提示词的基本结构

一个好的提示词通常包含以下几个要素:

  1. 明确的指令:清楚地告诉AI你想要它做什么
  2. 上下文信息:提供必要的背景知识
  3. 输入数据:需要AI处理的具体内容
  4. 输出格式:期望AI如何组织和呈现结果

简单例子对比

不好的提示词

1
写一篇关于AI的科普文章

好的提示词

1
2
请写一篇800字左右的科普文章,主题是"人工智能的发展历程",适合完全不动技术的人阅读,
包含三个主要发展阶段,使用生动的比喻和案例解释专业概念,并在结尾提出对未来的展望。

提升Prompt效果的核心技巧

1. 清晰具体

越具体的提示词越能得到准确的回答。包括具体描述:

  • 所需输出的长度(字数/段落数)
  • 目标受众(专业水平/年龄段)
  • 风格(正式/轻松/创意)
  • 结构(要点/段落/表格)

例子

1
请用简单的语言向我10岁女儿解释光合作用,不超过200字,使用至少2个生活中的比喻,避免使用专业术语。

2. 提供示例(少样本学习)

通过提供几个输入-输出的示例,可以更好地引导AI理解你的期望。

例子

1
2
3
4
5
6
7
8
9
10
请按照以下格式将这些句子翻译成英文:

中文:我喜欢吃苹果。
英文:I like to eat apples.

中文:明天我要去北京旅游。
英文:I will travel to Beijing tomorrow.

中文:这本书很有趣,我想推荐给你。
英文:

3. 角色设定

让AI扮演特定角色,能使回答更符合特定专业或风格需求。

例子

1
请你扮演一位经验丰富的营销专家,分析我的产品定位问题,并提供改进建议。我的产品是一款...

4. 分步骤思考

引导AI一步步思考问题,可以获得更准确的结果,特别是对于复杂问题。

例子

1
2
3
请帮我解决这个数学问题,在回答前,请先分析问题,列出已知条件,然后逐步推导求解过程,最后给出结论。

问题:一个圆柱形水箱,底面积为3平方米,高为2米。现在水箱中有水,深度为1.5米。如果以每分钟0.1立方米的速度向水箱中注水,需要多少分钟才能将水箱装满?

5. 指定输出格式

明确要求特定的输出格式,使结果更易于使用。

例子

1
2
3
4
5
6
7
8
请分析这家公司的优势和劣势,并以下面的JSON格式输出结果:

{
"公司名称": "XX科技",
"优势": ["优势1", "优势2", "优势3"],
"劣势": ["劣势1", "劣势2", "劣势3"],
"改进建议": ["建议1", "建议2", "建议3"]
}

进阶技巧

链式思维(Chain-of-Thought)

引导AI展示其思考过程,对于复杂推理特别有效。

例子

1
问题:小明有12个苹果,他给了小红3个,又给了小李他手中苹果数量的一半,最后他还剩下多少个苹果?请一步一步地思考,解释每一步的计算过程和原因。

思维树(Tree of Thoughts)

引导AI探索多种可能性和解决方案路径。

例子

1
请用思维树的方式分析我创业的三个不同选择(开咖啡店、做在线教育、开发APP),每个选择探索三个可能的发展路径,考虑不同条件下的结果,然后总结最优选择。

自我评估和修正

让AI评估自己的输出并进行改进。

例子

1
请写一篇关于气候变化的短文,然后评估这篇文章的优缺点,并基于评估给出一个改进版本。

常见应用场景实战

内容创作

写作辅助

例子

1
请为我的科技博客生成一篇文章大纲,主题是"5G技术如何改变我们的生活"。大纲应包含引言、3-5个主要部分、每部分2-3个小节,以及结论。每个小节都需要有简短描述。

创意生成

例子

1
我正在设计一个以"海洋保护"为主题的儿童故事书。请创作5个可能的故事情节,每个情节包含主角描述、基本冲突和教育意义。

数据分析与处理

例子

1
2
我有一组销售数据,包含产品名称、月份和销售额。请帮我分析这些数据,找出销售趋势,并提出改进建议。数据如下:
[数据内容]

代码辅助

例子

1
请编写一个Python函数,用于分析文本情感倾向。函数应接受一段文本作为输入,返回积极、消极或中性的评价以及置信度分数。请包含必要的注释和简单的使用示例。

常见问题与解决方法

如何处理AI回答过于笼统或离题?

  • 解决方法:增加具体细节,使用引导性问题,明确输出要求
  • 例子:原提示”谈谈人工智能的未来”可改为”请从就业、教育和伦理三个方面,具体分析人工智能在未来10年可能带来的社会变革。每个方面请提供至少两个具体的预测和可能的应对策略。”

如何避免AI生成的内容过于冗长?

  • 解决方法:明确字数限制,要求简洁回答,指定重点内容
  • 例子:”请用不超过300字,总结量子计算的核心原理,重点解释量子比特和量子纠缠这两个概念。”

如何使AI生成更创新性的内容?

  • 解决方法:明确要求原创思路,设置情景约束,激励思维发散
  • 例子:”请提出5种从未出现过的智能家居产品创意,每种产品都需要融合至少两种现有技术,并解决一个特定的家庭难题。”

免费学习资源

  1. 免费课程与教程

    • OpenAI的Prompt Engineering指南 (官网免费提供)
    • 李宏毅教授的”Large Language Model”课程 (YouTube完整课程)
    • Khan Academy的AI基础知识 (免费教育平台)
    • Hugging Face的NLP教程 (官方文档免费)
    • Coursera上的免费AI课程 (可以免费旁听)
  2. 免费电子书与指南

    • 《Prompt Engineering Guide》by Lilian Weng (在线免费阅读)
    • Dair.ai的Prompt Engineering Guide (GitHub上免费)
    • Github上的awesome-chatgpt-prompts开源仓库
    • OpenAI官方的最佳实践指南
  3. 免费在线社区与资源

    • GitHub上的Prompt Engineering资源库
    • Reddit的r/PromptEngineering社区
    • Discord的公开AI社区讨论组
  4. 免费网站

结语

Prompt Engineering不仅是一项技术技能,更是一门艺术。通过不断实践和调整,你会发现与AI交流的效率和质量都会显著提升。记住,最好的学习方式是实践——从今天开始尝试这些技巧,记录效果,持续改进。

面试 第一篇

源自ByteByteGo

HTTPS工作原理:三个关键步骤解

根据图中内容,HTTPS(Hypertext Transfer Protocol Secure)的工作原理可以分为三个关键步骤:

服务器证书检查(Server Certificate Check)

这是建立安全连接的第一步:

  • 客户端(浏览器)向服务器发送”HELLO”消息
  • 服务器回应”HELLO”
  • 服务器发送自己的证书给客户端
  • 客户端向证书颁发机构(CA)发送请求,确认此证书是否有效
  • CA回应”YES”,表示证书合法

这一步确保了用户正在与合法网站通信,而不是某个冒充者。证书颁发机构作为可信第三方,保证了服务器的身份。

密钥交换(Key Exchange)

验证服务器身份后,需要建立加密通信:

  • 客户端从服务器证书中提取服务器的公钥
  • 客户端创建一个会话密钥
  • 客户端告知服务器它支持的密码套件(”I know A,B,C,D cipher suites”)
  • 服务器选择一个密码套件(”OK, Let’s use C”)
  • 客户端使用服务器的公钥和选定的密码套件加密会话密钥
  • 服务器使用自己的私钥解密,获得会话密钥

此时,服务器也拥有了会话密钥,为后续加密通信做好准备。

加密通信隧道(Encrypted Tunnel)

在完成前两步后:

  • 客户端和服务器双方都拥有相同的会话密钥
  • 客户端发送的数据使用会话密钥加密
  • 服务器接收数据后使用相同的会话密钥解密
  • 服务器发送给客户端的数据也使用相同的会话密钥加密
  • 客户端接收后使用会话密钥解密

这形成了一个安全的加密通信隧道,即使数据在传输过程中被拦截,没有会话密钥的第三方也无法解密内容,保证了数据传输的安全性。

PS:从输入URL到页面显示的完整过程

  1. URL解析:浏览器解析URL,确定协议(HTTP/HTTPS)、域名和路径
  2. DNS解析:浏览器查询DNS服务器,将域名转换为IP地址
    • 首先检查浏览器缓存
    • 然后检查操作系统缓存
    • 接着查询本地DNS服务器
    • 必要时进行递归查询
  3. 建立TCP连接:浏览器与服务器建立TCP连接(三次握手)
    • 客户端发送SYN包
    • 服务器回应SYN-ACK包
    • 客户端发送ACK包
  4. TLS握手(HTTPS):如果是HTTPS,还需进行TLS握手
    • 服务器证书检查
    • 密钥交换
    • 建立加密通信隧道
  5. 发送HTTP请求:浏览器向服务器发送HTTP请求,包含请求方法、路径、头部信息等
  6. 服务器处理请求:服务器接收请求,进行相应处理,生成HTTP响应
  7. 接收HTTP响应:浏览器接收服务器返回的数据,包含状态码、响应头、响应体
  8. 解析HTML:浏览器开始解析HTML,构建DOM树
  9. 处理CSS:解析CSS,构建CSSOM树
  10. 执行JavaScript:加载并执行JavaScript代码
  11. 构建渲染树:将DOM树和CSSOM树结合,形成渲染树
  12. 布局:计算每个元素在屏幕上的精确位置和大小
  13. 绘制:将计算好的像素信息绘制到屏幕上
  14. 合成:将多个图层合成为最终显示的页面
  15. TCP连接关闭:数据传输完成后,断开TCP连接(四次挥手)
    • 客户端发送FIN包,表示客户端不再发送数据
    • 服务器发送ACK包,确认收到客户端的FIN
    • 服务器发送FIN包,表示服务器也不再发送数据
    • 客户端发送ACK包,确认收到服务器的FIN

离职系列 第十三篇
离职系列,回忆过去,在这做个记录。

SQL脚本管理规范:小团队高频发版实践指南

1. 背景与目的

作为一个研发不到10人的团队,从0到1构建SAAS平台,且每周需要发布2-3个版本,所以总有些团队管理等问题会慢慢暴露,我们再慢慢修复,就跟修BUG一样,这一篇就是因为上线出过SQL脚本的问题(阿里云的SQL控制台对一些写的不太规范的sql执行存在兼容性问题会导致SQL执行不符合预期),所以有了这篇SQL规范,先说问题,当前SQL脚本管理存在以下问题:

  • 各开发人员风格不一,缺乏统一规范
  • 脚本分散、无统一管理,难以追踪变更历史
  • 发版频繁导致变更混乱,增加了维护成本
  • 部分脚本质量不高,存在安全隐患

本规范旨在提供一套简单、实用且专业的SQL脚本管理方案,帮助团队高效管理数据库变更,可根据实践情况持续优化。

2. 目录结构

采用精简的目录结构,既能满足版本管理需求,又不过于复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/PG                      # 数据库名称
├── YYYY/ # 年份目录
│ └── MM-DD/ # 日期目录,如04-15表示4月15日
│ ├── release_N/ # 发布版本号,如release_1
│ │ ├── DDL/ # 数据结构变更脚本
│ │ │ ├── 001_create_table_xxx.sql
│ │ │ └── 002_create_index_xxx.sql
│ │ ├── DML/ # 数据操作脚本
│ │ │ └── 001_add_data_xxx.sql
│ │ └── ROLLBACK/ # 回滚脚本(可选)
│ │ └── 001_rollback_xxx.sql
├── dictionary/ # 数据字典
│ ├── base/ # 基础数据
│ │ ├── init_v1.0.sql # 2025年1月版本
│ │ └── init_v2.0.sql # 2025年7月版本(整合上半年变更)
│ ├── incremental/ # 按数据字典type
│ │ ├── dict_payment_type.sql
│ │ └── ...
├── menu/ # 菜单
│ ├── base/ # 基础数据
│ │ ├── init_v1.0.sql # 2025年1月版本
│ │ └── init_v2.0.sql # 2025年7月版本(整合上半年变更)
│ ├── ...
│ ├── module_xx/ # 按模块
│ │ ├── crm.sql
│ │ └── ...

3. 文件命名规范

3.1 脚本文件命名

采用序号_描述[_rollback].sql格式:

  • 序号:确保执行顺序,如001002
  • 描述:简明表达脚本用途,如create_user_tableadd_email_column
  • rollback:回滚脚本添加_rollback后缀

示例:

  • 001_create_user_table.sql
  • 001_create_user_table_rollback.sql

3.2 数据库对象命名

  1. 表命名

    • 使用小写和下划线
    • 采用前缀区分业务模块,如sys_userorder_item
    • 名称应能清晰表达表的用途
  2. 列命名

    • 主键统一为id
    • 外键使用entity_id格式,如user_id
    • 创建和更新时间统一为create_timeupdate_time
  3. 索引命名

    • 主键索引:pk_表名
    • 唯一索引:uk_表名_列名
    • 普通索引:idx_表名_列名

4. 脚本编写规范

4.1 文件头注释

所有SQL脚本必须包含统一的文件头注释:

1
2
3
4
5
6
-- ========================================
-- 描述: [功能简述]
-- 文件名: [文件名]
-- 作者: [作者]
-- 创建日期: [YYYY-MM-DD]
-- ========================================

4.2 SQL编写原则

  1. 原子性:一个脚本只完成一个独立任务

  2. 幂等性:脚本可以重复执行而不产生副作用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    -- 好的做法
    CREATE TABLE IF NOT EXISTS users (
    id VARCHAR(36) NOT NULL,
    username VARCHAR(50) NOT NULL,
    PRIMARY KEY (id)
    );

    -- 或者
    DROP TABLE IF EXISTS users;
    CREATE TABLE users (
    id VARCHAR(36) NOT NULL,
    username VARCHAR(50) NOT NULL,
    PRIMARY KEY (id)
    );
  3. 向后兼容:尽量避免破坏性变更

    1
    2
    3
    4
    5
    -- 推荐
    ALTER TABLE users ADD COLUMN email VARCHAR(100) NULL;

    -- 不允许
    ALTER TABLE users ADD COLUMN email VARCHAR(100) NOT NULL;
  4. 安全性:敏感信息不应明文存储

4.3 代码风格

  1. 关键字大写:所有SQL关键字使用大写形式

    1
    SELECT * FROM users WHERE status = 'active';
  2. 适当缩进:使用一致的缩进提高可读性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    SELECT 
    u.id,
    u.username,
    r.name AS role_name
    FROM
    users u
    JOIN
    roles r ON u.role_id = r.id
    WHERE
    u.status = 'active';
  3. 添加注释:为复杂SQL语句添加适当注释

5. 特殊脚本管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
数据字典和菜单:

├── dictionary/ # 数据字典
│ ├── base/ # 基础数据
│ │ ├── init_v1.0.sql # 2025年1月版本
│ │ └── init_v2.0.sql # 2025年7月版本(整合上半年变更)
│ ├── incremental/ # 按数据字典type
│ │ ├── dict_payment_type.sql
│ │ └── ...
├── menu/ # 菜单
│ ├── base/ # 基础数据
│ │ ├── init_v1.0.sql # 2025年1月版本
│ │ └── init_v2.0.sql # 2025年7月版本(整合上半年变更)
│ ├── ...
│ ├── module_xx/ # 按模块
│ │ ├── crm.sql
│ │ └── ...

6. 实施流程

针对高频发版小团队,简化流程但不降低质量要求:

6.1 开发变更流程

1
2
3
4
5
6
7
8
9
10
11
12
graph TD
A[编写SQL脚本] --> B[本地测试]
B --> C{通过?}
C -->|否| A
C -->|是| D[通知huantao]
D --> E[SQL评审]
E --> F{云平台工具测试通过?}
F -->|否| A
F -->|是| G[测试环境部署]
G --> H{测试人员验证通过?}
H -->|否| A
H -->|是| I[生产环境部署]

6.2执行顺序

为确保数据库变更的安全有序,按以下顺序执行:

  1. DDL脚本(按文件名序号顺序)
  2. DML脚本(按文件名序号顺序)
  3. 特殊配置(数据字典、菜单)

最后

团队应当将本规范视为基础标准,在实践中不断完善和优化,形成最适合团队的工作方式。