跳到主要内容

2025-12-23

一言

滟滟随波千万里,何处春江无月明! --- 《春江花月夜》 · 唐代 张若虚

Details

全文 春江潮水连海平,海上明月共潮生。
滟滟随波千万里,何处春江无月明!
江流宛转绕芳甸,月照花林皆似霰。
空里流霜不觉飞,汀上白沙看不见。
江天一色无纤尘,皎皎空中孤月轮。
江畔何人初见月,江月何年初照人?
人生代代无穷已,江月年年只相似。
不知江月待何人,但见长江送流水。
白云一片去悠悠,青枫浦上不胜愁。
谁家今夜扁舟子,何处相思明月楼?
可怜楼上月徘徊,应照离人妆镜台。
玉户帘中卷不去,捣衣砧上拂还来。
此时相望不相闻,愿逐月华流照君。
鸿雁长飞光不度,鱼龙潜跃水成文。
昨夜闲潭梦落花,可怜春半不还家。
江水流春去欲尽,江潭落月复西斜。
斜月沉沉藏海雾,碣石潇湘无限路。
不知乘月几人归,落月摇情满江树。


Docker Registry 学习文档

Docker Registry 基础概念

Docker Registry 是用于存储、分发 Docker 镜像的服务端组件。Docker 官方提供的实现是 registry:2(Distribution)。
核心定位非常明确:

它只负责镜像的存取,不负责复杂的管理与治理。

Registry 本身是无状态服务,真正的数据保存在后端存储(本地文件系统、对象存储等)。


Docker Registry 的能力边界

在学习和使用 Registry 之前,必须先明确它能做什么 / 不能做什么

可以做的事情

  • 私有镜像仓库存储

  • 支持 push / pull / delete

  • 支持 Basic Auth 与 TLS

  • 支持单一上游的 pull-through cache(代理缓存)

  • 支持文件系统或对象存储作为 backend

不能做的事情

  • 多上游 Registry 聚合

  • 细粒度 RBAC 权限模型

  • 图形化管理界面

  • 镜像生命周期管理

  • 智能路由与策略控制

一句话总结:

registry:2 是“镜像 HTTP 存储服务”,不是“镜像管理平台”。


Registry 的两种典型使用模式

Hosted(私有仓库模式)

  • 镜像由内部系统 docker push

  • Registry 只负责保存

  • 地址形式:

    registry.example.com:5000/project/app:1.0.0

适合:

  • 内部研发镜像

  • CI/CD 构建产物

  • 离线或半离线环境


Pull-Through Cache(代理缓存模式)

Registry 可配置一个上游(如 Docker Hub):

  1. 本地不存在镜像

  2. 转发到上游拉取

  3. 缓存到本地

  4. 返回给客户端

主要用途:

  • Docker Hub 加速

  • 网络不稳定或限流环境


Pull-Through Cache 的工作原理

请求链路

docker pull nginx

Docker daemon

registry(proxy 模式)

Docker Hub
  • 首次拉取:命中上游并缓存

  • 再次拉取:直接命中本地缓存


Proxy 配置示例(单上游)

version: 0.1
http:
addr: :5000

storage:
filesystem:
rootdirectory: /var/lib/registry

proxy:
remoteurl: https://registry-1.docker.io

关键点:

  • remoteurl 只能配置一个

  • Docker Hub 正确入口是 registry-1.docker.io

  • registry.docker.io 不是 Registry V2 API 入口


为什么 Registry 不支持多个 remoteurl

这是设计层面的限制,不是配置能力问题。

原因包括:

  • Registry V2 的 token / scope 强绑定单一上游

  • 镜像命名空间无法做路由判定

  • 认证流程无法组合

结论:

registry:2 的 proxy 模型是“单上游缓存”,而不是“多源聚合”。


Docker 客户端与 Registry 的关系

Registry Mirror 的作用

Docker 客户端可配置镜像加速器:

{
"registry-mirrors": ["http://registry.example.com:5000"]
}

行为说明:

  • 优先通过 mirror 拉取 Docker Hub 镜像

  • mirror 不存在再回退直连 Docker Hub

注意:

  • mirror ≠ 多 registry 路由

  • 只针对 Docker Hub 命名空间


HTTP 与 HTTPS 的安全模型

HTTP Registry(Insecure)

特点:

  • 部署简单

  • 仅适合内网环境

客户端必须显式声明:

{
"insecure-registries": ["registry.example.com:5000"]
}

HTTPS Registry(推荐)

特点:

  • 默认安全

  • 无需 insecure 配置

客户端需信任 CA:

/etc/docker/certs.d/registry.example.com:5000/ca.crt

Registry 的认证机制

Basic Auth

  • 基于 htpasswd

  • Registry 不维护用户体系

  • 适合小规模使用

Token / OAuth

  • Registry 原生不支持

  • 通常由反向代理或上层平台提供


Registry 的数据存储模型

存储内容

  • Layer blobs(占用磁盘)

  • Manifest / index(元数据)

重要认知

  • 相同 layer 只存一份(内容寻址)

  • 删除 tag 不等于立即释放空间

  • 必须执行 GC 才能回收磁盘


Registry 的运维关注点

磁盘空间

  • 镜像增长快

  • 必须监控磁盘水位

备份策略

  • 直接备份存储目录

  • Registry 自身无状态

垃圾回收(GC)

  • 执行 GC 时需停服务

  • 不适合高并发在线环境


Registry 的适用与不适用场景

适用场景

  • 内网镜像缓存

  • Docker Hub 加速

  • CI/CD 私有仓库

  • 离线镜像分发

不适用场景

  • 企业级多团队治理

  • 多 Registry 聚合

  • 复杂权限与审计

  • 可视化管理需求


Registry 在整体架构中的定位

一个工程化结论:

Docker Registry 是“底层存储组件”,而不是“平台级产品”。

在成熟架构中,它通常:

  • 被更高层平台接管

  • 或仅作为专用缓存节点存在


学习总结

  • Registry 只支持单上游 proxy

  • remoteurl 不能配置多个是设计必然

  • registry-1.docker.io 才是 Docker Hub 的真实入口

  • Registry Mirror ≠ 多 Registry 路由

  • 多上游统一入口必须使用更高层的仓库管理系统

这套认知建立后,Registry 的所有行为都会变得可预测、可解释、可维护

Spring Boot / Java 项目中校验、唯一键与字符串处理的系统化学习文档

本文是对我们整段对话的完整工程级总结,目标不是“会写代码”,而是知道为什么这么写、哪些地方不能省、哪些地方不能过度设计。内容以 Spring Boot + Java 11/21 + jOOQ 为背景,但原则具有普适性。


一、Bean Validation 基础:@Validated 的正确定位

1.1 @Validated 是什么

  • @ValidatedSpring 提供的校验触发注解

  • 底层基于 Bean Validation(JSR-303 / JSR-380)

  • 默认实现是 Hibernate Validator

核心作用:

触发校验 + 支持校验分组 + 支持方法级校验


1.2 @Validated@Valid 的区别

维度@Valid@Validated
来源JSR 标准Spring
分组校验
方法参数校验
Controller 参数校验

工程建议
在 Spring Boot 项目中 统一使用 @Validated


二、跨字段校验:为什么必须用“类级校验”

2.1 问题本质

需求示例:

String aString b
不能同时为空(null / blank),但允许只填一个

这是一个跨字段组合规则,而不是单字段规则。


2.2 为什么字段级注解不适合

  • @NotNull / @NotBlank → 表达的是 AND

  • 实际业务语义是 OR

  • 字段级注解无法感知“另一个字段”

结论:

只要规则依赖多个字段,就必须使用类级校验


三、字符串校验的正确语义:nullblank

3.1 业务语义层面的区分

情况语义
null未提供
"" / " "提供但无意义
" 10.1.1.1 "有值但带噪声
"1.1 .1.1"非法输入

业务校验关心的是“语义是否有值”,不是内存状态。


3.2 Java 原生 vs Apache Commons

比较两种写法:

s != null && !s.isBlank()
StringUtils.isNotBlank(s)

结论:

  • Java 11+:优先使用原生 isBlank()

  • 不为一个 isNotBlank 单独引入 commons-lang3

  • 依赖治理 > 少写几个字符


四、推荐的字符串归一化(normalize)策略

4.1 标准实现

private static String normalize(String s) {
if (s == null) return null;
var t = s.trim();
return t.isBlank() ? null : t;
}

4.2 为什么要 trim()isBlank()

  • isBlank() 能判断全空白

  • trim() 的真正目的不是判空,而是 消除首尾噪声

  • "10.1.1.1"" 10.1.1.1 " 在业务上应视为同一个值


4.3 为什么不处理“中间空格”

例如:

1.1 .1.1
1.1. 1.1

结论:

normalize 不负责纠错,只负责归一化

原因:

  • 中间空格属于 非法输入

  • 自动修复会“悄悄吞错”

  • 应在 DTO / 校验层明确拒绝

工程原则:

首尾空白是噪声,可以清;
中间空白是错误,必须报。


五、日常代码风格:getter 调用 vs 局部变量

5.1 两种写法对比

if (a.getS() != null && !a.getS().isBlank())
var s = a.getS();
if (s != null && !s.isBlank())

5.2 推荐结论

推荐先取局部变量,再判断

原因:

  • 避免重复表达式

  • 降低阅读与调试成本

  • 更利于逻辑扩展

  • 性能无实质差异


六、唯一键校验的核心问题建模

6.1 唯一键的真实语义

唯一键校验不是问:

“有没有这组字段?”

而是问:

“有没有另一条记录,与我拥有相同的唯一键语义?”

“另一条”是关键词。


6.2 唯一键包含分支逻辑的情况

示例:

  • systemName + sid + client + scheme + ip

  • systemName + sid + client + scheme + domain

隐含规则:

  • ipdomain 二选一

  • ip 优先级更高

这种规则应:

  • 在 DTO 层校验(至少一个非 blank)

  • 在 DAO 层保持一致的条件构造


七、Create 与 Update 场景下唯一性校验的差异

7.1 根本区别

场景当前记录是否存在是否需要排除自己
Create
Update

7.2 为什么 Update 必须排除自己

如果不排除:

exists (where 唯一键条件)
  • 更新未改唯一键时

  • 查询一定命中自己

  • 必然误判为冲突

正确语义:

exists (
where 唯一键条件
and id <> currentId
)

7.3 为什么 Create 不能用 id != null

  • SQL 中 id <> null 结果是 unknown

  • 会导致条件失效或逻辑错误

  • jOOQ 也不应生成这种条件


八、统一 Create / Update 的推荐实现方式

8.1 方法签名

public boolean existsConflictUniqueKey(
@Nullable Long currentId,
String systemName, String sid, Short client,
@Nullable String ip, @Nullable String domain,
Scheme scheme
)

8.2 核心逻辑

var cond = baseKeyCond(...);
if (cond == null) return false;

if (currentId != null) {
cond = cond.and(T.ID.ne(currentId)); // Update
}

return fetchExists(cond);

调用约定:

  • Create:currentId = null

  • Update:currentId = id


九、外层“唯一键是否变化”的判断:作用与边界

9.1 外层比较能做什么

if (oldKey.equals(newKey)) {
跳过 exists 查询
}

作用:

  • 减少数据库查询

  • 提升性能与体验


9.2 外层比较不能做什么

不能保证唯一性,原因包括:

  • 并发插入 / 并发更新

  • 数据漂移(stale read)

  • TOCTOU(检查与使用时间差)

结论:

外层比较是优化,不是正确性保证


十、完整工程级防线(强烈推荐)

10.1 三层防线模型

第一层:DTO / 参数校验
  • 非 blank

  • 至少一个字段存在

  • 格式合法(如 IP)

第二层:应用层唯一性检查
  • Create:exists(key)

  • Update:exists(key) AND id != currentId

  • 外层比较用于减少查询

第三层:数据库唯一约束(兜底)
  • 防止并发穿透

  • 捕获唯一约束异常 → 转业务异常


十一、关于校验注解与 Validator 的命名原则

11.1 不推荐“字段名型注解”

例如:

@AOrBNotBlank

问题:

  • 字段一变就失效

  • 注解数量爆炸

  • 复用性极差


11.2 推荐“语义型 + 参数化”

@AtLeastOneNotBlank(fields = {"a", "b"})

原则:

注解名 = 规则语义
字段名 = 参数
Validator = 规则引擎


十二、关键工程结论汇总

  1. @Validated 是 Spring 项目中的首选校验注解

  2. 跨字段规则必须使用类级校验

  3. 字符串校验关注语义,不只判断 null

  4. normalize 负责归一化,不负责纠错

  5. 局部变量优于重复 getter 调用

  6. Update 唯一性校验必须排除自己

  7. Create 场景不能生成 id != null

  8. 外层 key 比较是优化,不是正确性保证

  9. 数据库唯一约束是最终兜底

  10. 正确性 > 性能优化 > 代码简短


十三、一句总工程总结

校验是语义问题,
唯一性是并发问题,
数据库才是最终裁判。

把这三件事分清楚,你的代码就已经站在“长期可维护”的那一边了。