cloud-disk 采用控制面与数据面分离的微服务架构,通过消息队列解耦两层的一致性需求。
┌──────────────────────────┐
│ Nacos (8848) │
│ 注册中心 + 配置中心 │
└────────────┬─────────────┘
│
┌────────────────────────────┴─────────────────────────────┐
│ Gateway (38080) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ 路由转发 │ │ JWT 鉴权 │ │ Sentinel 流控(Gateway)│ │
│ └──────────┘ └──────────┘ └──────────────────────┘ │
└────────┬──────────────────────────────────┬──────────────┘
│ │
┌──────────────▼──────────────┐ ┌────────────────▼────────────────┐
│ object-storage │ │ object-metadata │
│ (38081) │ │ (38082) │
│ 数据面 / IO 密集型 │ │ 控制面 / CPU 密集型 │
│ │ │ │
│ ┌────────────────────┐ │ │ ┌─────────────────────────┐ │
│ │ ObjectController │ │ │ │ BucketController │ │
│ │ MultipartControl │ │ │ │ ShareController │ │
│ └────────┬───────────┘ │ │ │ RecycleController │ │
│ │ │ │ └─────────────────────────┘ │
│ ▼ │ │ │ │
│ ┌────────────────────┐ │ │ ▼ │
│ │ ObjectService │ │ │ ┌─────────────────────────┐ │
│ │ MultipartUploadSvc│ │ │ │ BucketService │ │
│ └──┬──────────┬──────┘ │ │ │ ShareService │ │
│ │ │ │ │ │ RecycleService │ │
│ ▼ ▼ │ │ └──────┬──────────┬───────┘ │
│ ┌──────┐ ┌────────┐ │ │ │ │ │
│ │MinIO │ │ Redis │ │ │ ▼ ▼ │
│ │数据面│ │秒传/分片│ │ │ ┌────────┐ ┌──────────┐ │
│ └──┬───┘ └────────┘ │ │ │ MySQL │ │ Redis │ │
│ │ │ │ │元数据 │ │ 缓存 │ │
│ ▼ │ │ └────────┘ └──────────┘ │
│ ┌──────────────────┐ │ └────────────────────────────────┘
│ │ StorageBackend │ │
│ │ MinIO/Local/OSS │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ RabbitMQ │─────────│────► ObjectCreatedConsumer
│ │ (Outbox) │ │ (写入 object_info)
│ └────────────────┘ │
└─────────────────────────────┘
Sentinel Dashboard (38858) MySQL master (33306) + slave (33307) Redis Cluster (37000-37005)
MinIO (39000/39001)/本地/OSS RabbitMQ (35672/45672)
| 层次 | 组件 | 职责 |
|---|---|---|
| 接入层 | Spring Cloud Gateway + Admin UI | 统一入口、路由、JWT 鉴权、Sentinel 流控、跨域 |
| 数据面 | object-storage | 对象数据 IO(上传/下载),Redis 缓存操作 |
| 控制面 | object-metadata | 元数据管理(索引/回收站/分享/Bucket) |
| 事件总线 | RabbitMQ + Outbox Table | 异步解耦数据面与控制面 |
| 持久层 | StorageBackend (MinIO/本地/OSS) + MySQL + Redis | 可插拔对象存储、关系元数据、缓存/状态 |
| 基础设施 | Nacos / Sentinel / XXL-Job | 注册配置、流控熔断、分布式定时任务 |
Client Gateway object-storage MinIO Redis
│ │ │ │ │
│ POST /bucket/key?uploads │ │ │
├────────────────────────►│ │ │ │
│ │ 路由至 object-storage │ │ │
│ ├─────────────────────►│ │ │
│ │ │ generateUuidV7() │ │
│ │ │ 初始化 Redis │ │
│ │ │ parts:{uploadId} │ │
│ │ ├──────────────────────────────────────────────►│
│ │ │ │ │
│ ← 200 { uploadId } │ │ │ │
│◄────────────────────────┤◄─────────────────────┤ │ │
│ │ │ │ │
│ PUT /bucket/key?partNumber=1&uploadId=xxx │ │ │
├────────────────────────►│ │ │ │
│ ├─────────────────────►│ │ │
│ │ │ 分片数据上传 │ │
│ │ ├───────────────────────►│ │
│ │ │ │ part-0001 写入 │
│ │ │◄───────────────────────┤ │
│ │ │ 记录 etag+size │ │
│ │ ├──────────────────────────────────────────────►│
│ ← 200 { etag } │ │ │ │
│◄────────────────────────┤◄─────────────────────┤ │ │
│ │ │ │ │
│ (重复 1..n 个分片) │ │ │ │
│ │ │ │ │
│ POST /bucket/key?uploadId=xxx │ │ │
│ Body: <CompleteMultipartUpload> │ │ │
├────────────────────────►│ │ │ │
│ ├─────────────────────►│ │ │
│ │ │ 校验 parts 完整性 │ │
│ │ │ 校验 etag 一致性 │ │
│ │ │ 校验 partNumber 升序 │ │
│ │ │ │ │
│ │ │ composeObject 合并 │ │
│ │ ├───────────────────────►│ │
│ │ │◄───────────────────────┤ │
│ │ │ │ │
│ │ │ 清理临时分片 │ │
│ │ ├───────────────────────►│ removeObjects │
│ │ │ │ │
│ │ │ 删除 Redis 状态 │ │
│ │ ├──────────────────────────────────────────────►│
│ │ │ │ │
│ │ │ 写入 Outbox 表 │ │
│ │ │ 发送 RabbitMQ │ │
│ ← 200 { etag } │ │ │ │
│◄────────────────────────┤◄─────────────────────┤ │ │
秒传利用 upload:hash:{md5}:{size} Redis Key 实现 O(1) 文件去重判定,避免重复传输相同内容。
Client Gateway object-storage Redis
│ │ │ │
│ PUT /bucket/key │ │ │
│ x-cloud-disk-md5: X │ │ │
├────────────────────────►│ │ │
│ ├─────────────────────►│ │
│ │ │ │
│ │ │ Redis GET │
│ │ │ upload:hash:{md5}:{size} │
│ │ ├───────────────────────────►│
│ │ │◄───────────────────────────┤
│ │ │ │
│ │ ┌─────┴─────┐ │
│ │ │ Key 存在? │ │
│ │ └─────┬─────┘ │
│ │ ┌─────┴─────┐ │
│ │ │ 是 (CACHE │
│ │ │ HIT) │ │
│ │ └─────┬─────┘ │
│ │ │ │
│ │ ┌─────▼─────┐ │
│ │ │ MinIO 校验 │ │
│ │ │ statObject │ │
│ │ └─────┬─────┘ │
│ │ ┌─────┴─────┐ │
│ │ │ 文件存在? │ │
│ │ └─────┬─────┘ │
│ │ ┌─────┴─────┐ ┌─────┐ │
│ │ │ 是 │ │ 否 │ │
│ │ │ │ │ │ │
│ │ │ 返回秒传 │ │清除 │ │
│ │ │ 响应 │ │缓存 │ │
│ │ └─────┬─────┘ └──┬──┘ │
│ │ │ │ │
│ │ ┌─────▼─────┐ │ │
│ │ │ 返回 │ │ │
│ ← 200 instant=true │ │ instant │ │ │
│◄────────────────────────┤◄───────────────┤ = true │ │ │
│ │ └───────────┘ │ │
│ │ │ │ │
│ │ │ false ▼ │
│ │ ├─── 正常上传到 MinIO ──────┤
│ │ │ │
│ │ │ SET 到 Redis (365天过期) │
│ │ ├───────────────────────────►│
│ │ │ │
│ │ │ 写入 Outbox 表 │
│ │ │ 发 RabbitMQ 事件 │
│ │ │ │
│ ← 200 instant=false │ │ │
│◄────────────────────────┤◄─────────────────────┤ │
关键设计点:
数据面写入对象后需要同步更新控制面的 object_info 索引表。使用 Outbox 模式保证最终一致性。
object-storage RabbitMQ object-metadata
PUT Object │ │ │
│ │ │ │
▼ │ │ │
MinIO 写入 │ │ │
│ │ │ │
▼ │ │ │
写入 Outbox 表 │ │ │
(status=PENDING) │ │ │
│ │ │ │
▼ │ │ │
RabbitMQ 发送 │ │ │
object.created ├─────────────────────────────────────►│ │
│ │ │ │
▼ │ │ │
更新 Outbox 表 │ │ @RabbitListener │
(status=SUCCESS) │ │ │
│ │ ┌───────────────────▼──┐
│ │ │ 查询 Bucket │
│ │ │ UPSERT object_info │
│ │ │ 更新 Bucket 使用量 │
│ │ │ 清除 Redis 缓存 │
│ │ └───────────────────┬──┘
│ │ │
│ │ ▼
│ │ MySQL object_info
故障恢复:
OutboxRetryService 通过 XXL-Job outboxRetryHandler(30s 间隔)定时扫描 PENDING 记录(idx_status_created 索引),重试发送;另有 outboxDeadCleanupHandler(1h 间隔)清理超过 24h 的死信。bucket_id + object_key 唯一索引,使用 UPSERT 语义。所有表使用 utf8mb4 字符集,时间字段统一使用 UTC_TIMESTAMP。
| 列名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT (AUTO) | 主键 |
| bucket_name | VARCHAR(255) | Bucket 名称,全局唯一 |
| region | VARCHAR(64) | 区域,默认 us-east-1 |
| storage_type | ENUM(‘MINIO’,’LOCAL’,’OSS’) | 存储后端类型,默认 MINIO |
| owner_user_id | BIGINT | 所属用户 ID |
| status | ENUM(‘ACTIVE’,’DELETED’) | 软删除标记 |
| quota_bytes | BIGINT | 配额上限,0 = 不限制 |
| used_bytes | BIGINT | 已用空间(由 ObjectCreated 事件更新) |
| created_at | DATETIME | 创建时间 |
| updated_at | DATETIME | 更新时间 |
索引:idx_owner(owner_user_id)
| 列名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT (AUTO) | 主键 |
| bucket_id | BIGINT | 所属 Bucket ID |
| object_key | VARCHAR(1024) | 对象键 |
| etag | VARCHAR(64) | MinIO 返回的 ETag |
| size | BIGINT | 文件大小(字节) |
| content_type | VARCHAR(255) | MIME 类型 |
| md5_hash | VARCHAR(64) | 文件 MD5,用于秒传判定 |
| upload_user_id | BIGINT | 上传者 ID |
| status | ENUM(‘NORMAL’,’RECYCLED’,’DELETED’) | 对象生命周期状态 |
| user_metadata | JSON | 用户自定义元数据 |
| created_at | DATETIME | 创建时间 |
| updated_at | DATETIME | 更新时间 |
唯一索引:uk_bucket_object(bucket_id, object_key)
索引:idx_status(status), idx_bucket_id(bucket_id)
此表在
sql/init.sql中定义,供将来扩展使用。当前运行时,分片上传状态完全由 Redisparts:{uploadId}(Hash) 维护,未读写此表。
| 列名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT (AUTO) | 主键 |
| upload_id | VARCHAR(64) | UUID v7,全局唯一 |
| bucket_id | BIGINT | 所属 Bucket |
| object_key | VARCHAR(1024) | 对象键 |
| initiated_by | BIGINT | 发起人 ID |
| initiated_at | DATETIME | 发起时间 |
| expires_at | DATETIME | 过期时间(24 小时后) |
| status | ENUM(‘IN_PROGRESS’,’COMPLETED’,’ABORTED’) | 上传状态 |
索引:idx_bucket_object(bucket_id, object_key)
| 列名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT (AUTO) | 主键 |
| message_type | VARCHAR(64) | 消息类型,如 OBJECT_CREATED |
| payload | JSON | 消息载荷 |
| status | ENUM(‘PENDING’,’SUCCESS’,’FAILED’) | 处理状态 |
| retry_count | INT | 重试次数 |
| created_at | DATETIME | 创建时间 |
| next_retry_at | DATETIME (NULLABLE) | 下次重试时间 |
索引:idx_status_created(status, created_at) —— 用于 XXL-Job 定时扫描待重试消息。
| 列名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT (AUTO) | 主键 |
| token | VARCHAR(32) | 短码,全局唯一 |
| bucket_id | BIGINT | 所属 Bucket |
| user_id | BIGINT | 分享创建者 ID |
| object_keys | JSON | 分享的对象键列表 |
| password | VARCHAR(255) (NULLABLE) | bcrypt 加密密码,空 = 公开 |
| expire_at | DATETIME | 过期时间 |
| max_downloads | INT | 下载次数上限,0 = 不限制 |
| current_downloads | INT | 当前下载次数 |
| status | ENUM(‘ACTIVE’,’EXPIRED’,’REVOKED’) | 链接状态 |
| created_at | DATETIME | 创建时间 |
索引:idx_token(token), idx_status(status), idx_user_id(user_id)
Key: upload:hash:{md5}:{size}
Type: STRING
Value: 文件 MinIO 中的 objectKey
TTL: 365 天
示例:
> SET upload:hash:d41d8cd98f00b204e9800998ecf8427e:1024 "/photos/sunset.jpg" EX 31536000
上传文件时携带 x-cloud-disk-md5 请求头,服务端计算 Key 后执行 GET:
instant=true。Key: parts:{uploadId}
Type: HASH
Value: { partNumber: "etag|size" }
TTL: 24 小时(与分片上传过期时间一致)
示例:
> HSET parts:0193f1a2-3b4c-5d6e-7f8a-9b0c1d2e3f4f 1 "abc123|5242880"
> HSET parts:0193f1a2-3b4c-5d6e-7f8a-9b0c1d2e3f4f 2 "def456|5242880"
> EXPIRE parts:0193f1a2-3b4c-5d6e-7f8a-9b0c1d2e3f4f 86400
{-1: "init"})确保 key 存在后再设置 TTL。completeMultipartUpload 合并分片(MinIO composeObject / 本地 FileChannel / OSS 原生 API),清理 Redis key。Key: bucket:storage:{bucketName}
Type: STRING
Value: 存储类型(MINIO / LOCAL / OSS)
TTL: 1 小时
示例:
> SET bucket:storage:my-bucket "LOCAL" EX 3600
StorageBackendFactory 使用此缓存避免每次请求都查 MySQL。缓存未命中时查 bucket_info.storage_type 字段,写入 Redis 后返回对应后端实现。
此 Key 同样通过
HotKeyUtil进行 3-shard hash-tag 打散(与秒传缓存相同的热点打散策略),避免高频 Bucket 的存储类型查询成为热点。
Key: buckets:list:{userId}
Type: STRING (序列化 JSON)
Value: BucketInfo 列表
TTL: 动态(创建/删除 Bucket 时主动失效)
Key: object:info:{bucketName}:{objectKey}
Type: STRING (序列化 JSON)
Value: ObjectInfo
TTL: 动态(ObjectCreated 事件消费后清除)
当前仅用于缓存失效(delete on write),写入时读取数据库最新数据。可扩展为读缓存以提升 ListObjects 性能。
/{bucket}/{key}),兼容 MinIO 默认配置,便于与 s3cmd、aws-cli 等工具集成。// 简化版 UUID v7 实现:timestamp (48 bits) + random (剩余)
private String generateUuidV7() {
long timestamp = Instant.now().toEpochMilli();
long random = new SecureRandom().nextLong();
return String.format("%08x-%s-%04x-%04x-%012x",
(int)(timestamp >> 16),
String.format("%04x", (int)(timestamp & 0xFFFF)),
(int)((random >> 48) & 0xFFFF),
(int)((random >> 32) & 0xFFFF),
random & 0xFFFFFFFFFFFFL);
}
对象状态迁移:NORMAL -> RECYCLED (30天) -> DELETED
RecycleServiceImpl.cleanExpiredObjects() 通过 XXL-Job recycleCleanupHandler 每天凌晨 3 点自动执行:标记过期的 RECYCLED 对象为 DELETED → 调用 object-storage 物理删除 MinIO 文件 → 删除 DB 记录。bucket_info.status 也使用 ACTIVE/DELETED 枚举,删除 Bucket 仅标记状态,避免级联删除复杂事务。数据面(object-storage)在写入 MinIO 后需要通知控制面(object-metadata)更新索引。直接双写或 RPC 调用存在以下问题:
| 方案 | 问题 |
|---|---|
| 直接双写 | MinIO 写成功 + MySQL 写失败 → 数据不一致 |
| 同步 RPC | 增加上传链路延迟,控制面不可用则上传失败 |
| 普通 MQ | 发送前宕机 → 消息丢失 |
Outbox 模式方案:
OutboxRetryService 通过 XXL-Job 定时扫描 PENDING 记录并重试(outboxRetryHandler,30s 间隔),另有定期清理死信任务(outboxDeadCleanupHandler,1h 间隔)。这确保了 至少一次投递 语义,且不增加上传主路径延迟。
Gateway 层配置了三组流控规则:
| 规则 ID | QPS 上限 | 场景 |
|---|---|---|
| object-storage-put | 200/s | 上传/秒传 |
| object-storage-get | 100/s | 下载 |
| object-storage-list | 50/s | 列举对象 |
下载和列举的流控阈值较低,保护后端 MinIO 和 MySQL 不被打满。
spring:
threads:
virtual:
enabled: true
对象存储服务是典型的 IO 密集型应用(MinIO 网络 IO、Redis 网络 IO、MySQL JDBC IO)。Java 21 虚拟线程将 IO 阻塞从操作系统线程模型剥离,在相同内存下支撑更高并发,避免传统线程池的线程耗尽问题。
Gateway 基于 Spring Cloud Gateway (Reactor Netty) 构建,整个请求链路必须保持非阻塞才能发挥 Netty event loop 的并发能力。一个关键的阻塞点是用 Redisson 检查 JWT 黑名单:
// ❌ 阻塞:在 Netty event loop 线程上执行同步 Redis 查询
RBucket<String> bucket = redissonClient.getBucket(jwtBlacklistKey);
if (bucket.isExists()) { ... } // 阻塞线程!
// ✅ 非阻塞:使用 RedissonReactiveClient 融入响应式链
return redissonClient.getBucket(jwtBlacklistKey)
.isExists() // 返回 Mono<Boolean>
.flatMap(exists -> exists
? unauthorized(exchange)
: chain.filter(exchange));
问题表现:redissonClient.getBucket().isExists() 是一个同步阻塞调用。当 2,000 并发请求同时到达时,16 个 Netty worker 线程全部被 Redis 查询阻塞 → event loop 永久死锁 → Gateway 不再接受任何新连接 → 整个系统不可用。
解决方案:注入 RedissonReactiveClient(由 RedissonClusterConfig 通过 redissonClient.reactive() 创建,复用同一连接池),所有 Redis 操作返回 Mono<T>,融入 Gateway 的响应式管道。
配合优化:
| 配置项 | 值 | 说明 |
|——–|—–|——|
| spring.cloud.gateway.httpclient.pool.type | elastic | 按需扩展连接池 |
| spring.cloud.gateway.httpclient.pool.max-connections | 4000 | 容纳 2000 并发 + 余量 |
| spring.cloud.gateway.httpclient.pool.max-idle-time | 15s | 尖峰后快速回收空闲连接 |
| reactor.netty.ioWorkerCount | 16 | 匹配并发连接数 |
| ChannelOption.SO_BACKLOG | 4096 | TCP accept 队列,应对突发连接 |
| JVM Heap | -Xms256m -Xmx384m | 容纳 2000 连接对象 + Netty 直接内存 |
| Container | 768M | JVM 堆 + Netty 堆外内存 + OS 开销 |
基准测试验证:尖峰测试 10→2000→10 线程,0 错误 100% 成功率,恢复后 P50=3ms。
预签名 URL 的核心挑战在于 Docker 环境中 MinIO 内部地址与客户端可访问地址不一致:
http://minio:9000(Docker 网络内部)http://localhost:39000(端口映射)S3 V4 签名绑定 Host 头,若用内部地址生成 URL 再替换 host,则签名校验失败。
方案:创建两个 MinioClient Bean:
// 日常 CRUD — 内部端点(minio:9000)
@Primary
public MinioClient minioClient() { ... }
// 预签名 URL — 公开端点(host.docker.internal:39000)
@Qualifier("presignMinioClient")
public MinioClient presignMinioClient() { ... }
presignMinioClient 配置为公开端点 minio.public-endpoint,生成的 URL 中 host 与客户端请求 host 一致,签名校验通过。Docker Compose 中通过 extra_hosts 确保 host.docker.internal 在容器内可解析。
MinioStorageBackend 注入两个 client,presignGetObject/presignPutObject 使用 presignMinioClient,其余方法使用 minioClient。
| 服务 | 镜像 | 内存限制 | CPU / JVM 参数 |
|---|---|---|---|
| mysql | mysql:8.0.34 | 512M | - |
| mysql-slave | mysql:8.0.34 | 512M | - |
| redis-7000~7005 | redis:7-alpine (6 节点) | 192M ×6 | - |
| redis-cluster-init | redis:7-alpine | - | 一次性初始化容器 |
| rabbitmq | rabbitmq:3.13.7-management-alpine | 320M | - |
| minio | minio/minio:RELEASE.2025-04-22T22-12-26Z | 320M | - |
| nacos | nacos/nacos-server:v2.4.3 | 448M | JVM: 128m/192m/64m (Xms/Xmx/Xmn) |
| sentinel | bladex/sentinel-dashboard:1.8.7 | 256M | Java: -Xms64m -Xmx128m |
| xxl-job | xuxueli/xxl-job-admin:2.4.2 | 320M | Java: -Xms128m -Xmx192m |
| gateway | 本地构建 (Dockerfile) | 768M | JVM: -Xms256m -Xmx384m, Netty workers=16, TCP backlog=4096 |
| object-storage | 本地构建 (Dockerfile) | 512M | JVM: -Xms128m -Xmx256m |
| object-metadata | 本地构建 (Dockerfile) | 512M | JVM: -Xms128m -Xmx256m |
FROM eclipse-temurin:21-jre-alpine AS builder
RUN addgroup -S clouddisk && adduser -S clouddisk -G clouddisk
FROM builder
ARG MODULE
ARG JVM_OPTS="-Xms128m -Xmx256m"
WORKDIR /app
COPY ${MODULE}/target/*.jar app.jar
ENV JVM_OPTS=${JVM_OPTS}
USER clouddisk
EXPOSE 8080
ENTRYPOINT exec java ${JVM_OPTS} --enable-preview -jar app.jar
eclipse-temurin:21-jre-alpine 作为基础镜像,体积小且安全。--enable-preview 启用 Java 21 预览特性。clouddisk 运行,符合安全最佳实践。MODULE 和 JVM_OPTS 由 Docker Compose 传入,同一 Dockerfile 构建所有业务模块。MySQL (master + slave, healthy) ──→ MySQL replication init (一次性)
──→ XXL-Job Admin
Nacos (healthy) ─────────────────→ gateway, object-storage, object-metadata
Redis 6节点 (healthy) ──→ Redis Cluster init (exit 0) ──→ gateway, object-storage, object-metadata
业务服务只有在 Nacos healthy 且 Redis Cluster init 成功退出 后才启动。depends_on 配合 healthcheck 确保服务间依赖链正确。所有业务服务通过 Nacos 服务发现互相定位。
注意:
docker compose down会删除 Docker 网络,重新up时网络子网可能变化, 导致 Redis Cluster 持久化 volumes 中存储的节点 IP 失效。完整 down/up 前必须删除所有 Redis data volumes。
| 面试考点 | cloud-disk 对应实现 |
|---|---|
| 微服务架构设计 | Gateway + 数据面/控制面分离,Nacos 服务发现 |
| 响应式编程 / 非阻塞 | Gateway AuthFilter 使用 RedissonReactiveClient,全链路非阻塞 |
| 分布式事务 | Outbox 模式 + 定时重试,最终一致性 |
| 大文件上传 | 分片上传 Init/Part/Complete/Abort |
| 策略模式 / 多后端 | StorageBackend 接口,Factory + Redis 缓存路由 |
| 缓存设计 | Redis 秒传缓存、分片状态、列表缓存、存储类型缓存 |
| Redis Cluster 热点 | HotKeyUtil 3-shard hash-tag 打散,偏差 <20% |
| 数据库索引优化 | UUID v7 时间有序,复合唯一索引 |
| 软删除与回收站 | 状态机 NORMAL → RECYCLED → DELETED |
| 限流熔断 | Sentinel Gateway 流控规则 |
| Java 21 新特性 | Virtual Threads, Pattern Matching |
| 消息队列 | RabbitMQ Topic Exchange, 消费者幂等 |
| 容器化部署 | Docker Compose, Healthcheck, 资源限制 |
| 性能调优 | Netty event loop 治理, JVM 堆外内存, TCP backlog, 连接池弹性伸缩 |