Files
2026-03-01 01:43:46 +08:00

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. 冲突域名处理思路正确

  "这代码能用,但有定时炸弹。那个精准域名查询为空就抛异常的逻辑,需要立即修复。"

  ---
  现在清楚这段域名校验逻辑了吗?有哪里不明白的?