跳到主要内容

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 是什么​

  • @Validated 是 Spring 提供的校验触发注解

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

  • 默认实现是 Hibernate Validator

核心作用:

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


1.2 @Validated 与 @Valid 的区别​

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

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


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

2.1 问题本质​

需求示例:

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

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


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

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

  • 实际业务语义是 OR

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

结论:

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


三、字符串校验的正确语义:null ≠ blank​

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

隐含规则:

  • ip 与 domain 二选一

  • 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. 正确性 > 性能优化 > 代码简短


十三、一句总工程总结​

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

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