16 KiB
16 KiB
1. 精准域名 - www.example.com 必须完全匹配
2. 冲突域名 - 多个企业可能配置同一个域名(联通/电信/移动各有一套)
3. 泛域名 - *.example.com 匹配所有子域名
"数据结构是什么?"
这才是关键:
【输入】
domains: [www.a.com, b.b.com, sub.c.com] // 用户请求的域名列表
absentDomain: [www.a.com, b.b.com] // Redis缓存未命中的域名
cacheDomain: {sub.c.com -> 域名信息PO} // Redis缓存命中的域名
【输出】
Map<域名, 平面JSON> = {
"www.a.com" -> "[1,2,3]", // 该域名可以下发到平面1,2,3
"b.b.com" -> "[1,2,5]",
"sub.c.com" -> "[1,2,3,4,5,6,7,8]"
}
---
【完整流程拆解】
阶段1: 初始化 (行993-998)
Map<String, DomainEnterpriseInfoRedisPO> preciseDomainMap = new HashMap<>();
if (!MapUtils.isEmpty(cacheDomain)) {
preciseDomainMap.putAll(cacheDomain); // 把Redis缓存的域名先放进来
}
目的: 构建一个"精准域名查找表",包含缓存+DB查询的完整域名信息。
---
阶段2: 处理未缓存的精准域名 (行1000-1035)
这是整段代码最复杂的部分,因为要处理冲突域名。
2.1 数据库查询
List<DomainEnterpriseInfoTmp> preciseDomains =
selfServiceDomainConfigDao.findDomainInfoByListDomain(absentDomain, enterpriseId);
SQL伪代码:
SELECT * FROM domain_config
WHERE domain IN ('www.a.com', 'b.b.com')
AND tenant_id = '企业123'
重要: 这里查询会返回两种情况的数据:
4. domain字段就是真实域名: {domain: 'www.a.com', cpDomain: null}
5. cpDomain字段是真实域名: {domain: 'www.a.com_conflict', cpDomain: 'www.a.com'}
---
2.2 构建两个Map (行1009-1014) - 关键设计
// Map1: 以domain字段为key
Map<String, Info> realDomainMap = {
"www.a.com" -> Info{domain='www.a.com', cpDomain=null, ...}
"b.b.com_conflict" -> Info{domain='b.b.com_conflict', cpDomain='b.b.com', ...}
}
// Map2: 以cpDomain字段为key (只包含冲突域名)
Map<String, Info> cpDomainMap = {
"b.b.com" -> Info{domain='b.b.com_conflict', cpDomain='b.b.com', ...}
}
为什么需要两个Map?
因为用户请求的是真实域名b.b.com,但数据库存储的可能是b.b.com_conflict!
---
2.3 遍历处理每个域名 (行1017-1034) - 双重匹配逻辑
for (String domain : absentDomain) { // domain = "b.b.com"
DomainEnterpriseInfoRedisPO redisPO = null;
// 第一次尝试: 直接匹配domain字段
DomainEnterpriseInfoTmp tmpInfo = realDomainMap.get(domain);
if (tmpInfo != null) {
redisPO = convert(tmpInfo);
} else {
// 第二次尝试: 从冲突域名Map中匹配cpDomain字段
tmpInfo = cpDomainMap.get(domain); // 用b.b.com查cpDomain
if (tmpInfo != null) {
redisPO = convert(tmpInfo);
redisPO.setDomain(domain); // 🔴 关键:覆盖domain为真实域名
}
}
if (redisPO != null) {
preciseDomainMap.put(domain, redisPO);
redis.set(cacheKey, redisPO, 1800秒); // 写回缓存
}
}
举例说明冲突域名处理:
数据库记录:
┌─────────────────────┬──────────────┬────────────┐
│ domain │ cpDomain │ tenantId │
├─────────────────────┼──────────────┼────────────┤
│ www.a.com │ NULL │ 企业123 │ // 普通域名
│ b.b.com_unicom │ b.b.com │ 企业123 │ // 冲突域名(联通)
│ b.b.com_telecom │ b.b.com │ 企业456 │ // 冲突域名(电信)
└─────────────────────┴──────────────┴────────────┘
用户请求: ["www.a.com", "b.b.com"]
处理流程:
6. www.a.com → realDomainMap.get("www.a.com") ✅ 直接命中
7. b.b.com → realDomainMap.get("b.b.com") ❌ 未命中
→ cpDomainMap.get("b.b.com") ✅ 命中 b.b.com_unicom
→ 覆盖domain字段为 "b.b.com"
→ Redis缓存key用真实域名 "b.b.com"
设计意图:
- 数据库用domain_conflict存储避免主键冲突
- 但对用户透明,用户永远使用真实域名
- 通过企业ID区分不同企业的同名域名
---
阶段3: 处理广义域名(泛域名) (行1037-1061)
Map<String, DomainEnterpriseInfoRedisPO> extensiveDomainMap = new HashMap<>();
for (String domain : domains) { // 遍历所有域名
DomainEnterpriseInfoRedisPO domainInfo = preciseDomainMap.get(domain);
if (domainInfo == null) { // 精准域名没找到
// 延迟加载:只在第一次需要时才查询广义域名
if (extensiveDomainMap.isEmpty()) {
List<Info> extensiveDomains = service.findAllExtensiveDomain();
extensiveDomainMap = toMap(extensiveDomains);
// 可能包含: {"*.example.com", "*.test.com"}
}
// 泛域名匹配
String matchedDomain = matchExtensiveDomain(domain, extensiveDomainMap.keySet());
// 例: domain="sub.example.com" → 匹配到 "*.example.com"
if (StringUtils.isBlank(matchedDomain)) {
throw new PlatformException("未查询到域名 " + domain);
}
domainInfo = extensiveDomainMap.get(matchedDomain); // 使用泛域名的配置
redis.set(cacheKey, domainInfo, 1800秒); // 也缓存到Redis
}
// ... 后续校验
}
泛域名匹配算法 (CdniContextVO.matchExtensiveDomain)
public static String matchExtensiveDomain(String domain, Collection<String> extensiveDomains) {
// domain = "sub.example.com"
// extensiveDomains = ["*.example.com", "*.test.com"]
int index = domain.indexOf('.'); // index = 3
if (index != -1) {
String match = "*" + domain.substring(index); // match = "*.example.com"
for (String s : extensiveDomains) {
if (match.equals(s)) {
return s; // 返回 "*.example.com"
}
}
}
return null;
}
匹配逻辑:
sub.example.com → *.example.com ✅
a.b.example.com → *.b.example.com (如果存在) ✅
→ 否则匹配失败 ❌
注意: 只匹配第一级子域名!
延迟加载优化:
if (extensiveDomainMap.isEmpty()) {
// 只在第一次遇到泛域名需求时才查询
// 如果所有域名都是精准域名,这个查询就省了
}
---
阶段4: 校验域名状态和企业归属 (行1063-1074)
// 4.1 状态校验
if (!DomainTicketStateEnum.judgeDomainState(domainInfo.getDomainState())) {
throw new PlatformException("域名状态无效");
}
// 允许的状态: 已生效、部署中、部署失败、启用中
// 4.2 企业归属校验
if (!enterpriseId.equals(domainInfo.getTenantId())) {
throw new PlatformException("域名不存在"); // 实际是权限不足
}
// 4.3 提取平面信息
result.put(domain, domainInfo.getDomainPlain());
// domainPlain 是JSON字符串: "[1,2,3,4,5]"
---
【完整数据流示例】
假设场景:
用户请求:
domains = ["www.a.com", "sub.b.com", "c.example.com"]
enterpriseId = "企业123"
Redis缓存:
命中: {"www.a.com" -> Info{...}}
未命中: ["sub.b.com", "c.example.com"]
数据库 domain_config:
┌──────────────────┬────────────┬──────────────┬──────────────┐
│ domain │ cpDomain │ tenantId │ domainPlain │
├──────────────────┼────────────┼──────────────┼──────────────┤
│ www.a.com │ NULL │ 企业123 │ [1,2,3] │
│ sub.b.com │ NULL │ 企业123 │ [1,2,5] │
│ *.example.com │ NULL │ 企业123 │ [1,2,3,4,5] │
└──────────────────┴────────────┴──────────────┴──────────────┘
执行流程:
┌─ 阶段1: 初始化 ─────────────────────────────────┐
│ preciseDomainMap = {"www.a.com" -> Info{...}} │ // 来自Redis
└──────────────────────────────────────────────┘
┌─ 阶段2: 查询DB ──────────────────────────────────┐
│ absentDomain = ["sub.b.com", "c.example.com"] │
│ ↓ DB查询 (精准域名) │
│ 结果: [Info{domain='sub.b.com', ...}] │
│ c.example.com 未找到 │
│ ↓ 处理 │
│ preciseDomainMap.put("sub.b.com", ...) │
│ Redis.set("sub.b.com", ..., 1800秒) │
└──────────────────────────────────────────────┘
┌─ 阶段3: 泛域名匹配 ────────────────────────────────┐
│ 遍历: ["www.a.com", "sub.b.com", "c.example.com"]│
│ │
│ www.a.com → preciseDomainMap.get() ✅ │
│ sub.b.com → preciseDomainMap.get() ✅ │
│ c.example.com → preciseDomainMap.get() ❌ │
│ ↓ 触发广义域名查询 │
│ extensiveDomainMap = {"*.example.com"│-> Info}
│ ↓ 匹配 │
│ matchExtensiveDomain("c.example.com")│
│ → "*.example.com" ✅ │
│ ↓ 使用泛域名配置 │
│ domainInfo = Info{domain='*.example.com', plain='[1,2,3,4,5]'}
└──────────────────────────────────────────────┘
┌─ 阶段4: 校验 + 输出 ─────────────────────────────┐
│ result = { │
│ "www.a.com" -> "[1,2,3]", │
│ "sub.b.com" -> "[1,2,5]", │
│ "c.example.com" -> "[1,2,3,4,5]" // 泛域名的平面│
│ } │
└──────────────────────────────────────────────┘
---
【代码问题分析】
🔴 严重问题
1. 冲突域名处理的设计缺陷
// 行1026
redisPO.setDomain(domain); // 直接修改对象
这个操作污染了数据!如果tmpInfo被其他地方引用,也会被修改。应该:
redisPO = tmpInfo.cloneAndSetDomain(domain); // 创建新对象
2. 广义域名查询性能问题
// 行1045
List<Info> extensiveDomains = service.findAllExtensiveDomain();
// 查询全量泛域名!如果有1000个泛域名,全部加载到内存
问题:
- 每次处理任务时,只要有一个域名需要泛域名匹配,就查询全部
- 没有缓存到Redis(只缓存匹配结果)
- 如果泛域名数量巨大,这是性能炸弹
改进:
// 泛域名应该缓存到Redis或本地内存
private static final Cache<String, List<Info>> EXTENSIVE_DOMAIN_CACHE =
CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
3. 缓存一致性问题
// 行1032、1060
redisUtil.setJSONString(key, redisPO, 1800L); // 30分钟
问题场景:
4. 域名状态从"已生效"变更为"已下线"
5. Redis缓存还有30分钟才过期
6. 用户仍然可以操作已下线的域名!
解决方案:
- 域名状态变更时主动清除Redis缓存(事件总线)
- 或者缩短TTL到5分钟
4. 重复查询问题
// 行1002
List<Info> preciseDomains = dao.findDomainInfoByListDomain(absentDomain, enterpriseId);
// 如果absentDomain = ["a.com", "b.com", "c.com"]
// 而数据库只有 a.com 和 b.com
// c.com 不存在,直接抛异常 (行1005)
问题: 无法区分"域名不存在"还是"需要泛域名匹配"。
正确流程:
5. 查询精准域名,允许部分未找到
6. 未找到的再尝试泛域名匹配
7. 最后仍未找到才抛异常
但当前代码:
- 如果查询结果为空,直接抛异常
- 导致泛域名逻辑(行1037)永远无法执行!
等等,我看错了吗?让我重新看代码...
// 行1002
List<Info> preciseDomains = dao.findDomainInfoByListDomain(absentDomain, enterpriseId);
if (CollectionUtils.isEmpty(preciseDomains)) {
log.error("域名不存在: {}", absentDomain);
throw new PlatformException("未查询到域名信息"); // 🔴 这里直接抛异常了!
}
妈的,这是个BUG!
如果absentDomain中全是泛域名的子域名(比如sub.example.com),数据库查不到精准记录,会直接抛异常,永远走不到泛域名匹配逻辑(行1037)!
正确逻辑应该是:
if (!CollectionUtils.isEmpty(absentDomain)) {
List<Info> preciseDomains = dao.findDomainInfoByListDomain(absentDomain, enterpriseId);
// 允许查询结果为空,部分域名可能是泛域名
if (!CollectionUtils.isEmpty(preciseDomains)) {
// 处理查到的精准域名
// ...
}
// 未查到的域名留给后面的泛域名匹配
}
---
🟡 中等问题
5. 方法命名误导
private Map<String, String> checkValidDomainRefresh(...)
方法名叫checkValidDomainRefresh,但实际上:
- 不仅校验,还查询、转换、缓存
- 不仅适用于Refresh,INSERT/DISTRIBUTE也用这个方法
应该叫:
private Map<String, String> resolveDomainPlainsWithValidation(...)
6. 日志不足
缺少关键日志:
- 冲突域名处理的日志
- 泛域名匹配的日志
- Redis缓存命中率日志
---
【Linus式总结】
核心判断
这段代码在解决真实问题吗? → ✅ 是的
- CDN系统确实需要支持精准域名+泛域名+多租户隔离
- 缓存策略也是必要的(减少DB压力)
数据结构设计合理吗? → ⚠️ 凑合
- 用两个Map处理冲突域名,思路正确但实现有bug
- 延迟加载广义域名是好的优化
有特殊情况需要消除吗? → 🔴 有!
- 冲突域名的特殊处理应该封装成独立方法
- 精准域名和泛域名的处理应该统一
---
改进方案
如果让我重写这段代码:
private Map<String, String> resolveDomainPlains(
Set<String> domains,
String enterpriseId,
Map<String, DomainInfo> cached) {
DomainResolver resolver = DomainResolver.builder()
.withCache(cached)
.withEnterpriseId(enterpriseId)
.build();
Map<String, String> result = new HashMap<>();
for (String domain : domains) {
DomainInfo info = resolver.resolve(domain);
// 内部统一处理: 缓存查询 → 精准域名 → 冲突域名 → 泛域名
info.validateState();
info.validateOwnership(enterpriseId);
result.put(domain, info.getPlains());
}
return result;
}
消除特殊情况,用统一的Resolver模式。
---
最终评价
品味评分: 🟡 凑合
问题:
1. 有个严重BUG(行1003-1005的异常处理)
2. 缓存一致性风险
3. 泛域名查询性能隐患
优点:
4. 延迟加载广义域名是个好主意
5. 冲突域名处理思路正确
"这代码能用,但有定时炸弹。那个精准域名查询为空就抛异常的逻辑,需要立即修复。"
---
现在清楚这段域名校验逻辑了吗?有哪里不明白的?