DANGER-ZONES.md 8.2 KB

祈盟SDK 代码禁区

这些写法在本项目历史中出过事。 AI 生成代码时,必须避免以下 8 种模式。 最后更新:2026-06-30


禁区 1:OR 条件拼 SQL

错误写法(永远不要这么写)

// 错误:直接拼 OR 条件,2w 条条件打 MySQL
String sql = "SELECT * FROM t WHERE " +
    conditions.stream()
        .map(c -> "(a=" + c.a + " AND b='" + c.b + "')")
        .collect(Collectors.joining(" OR "));

事故记录

  • 5/26 MySQL CPU 爆掉事故:2w 条 OR 条件,CPU 90%+ 持续 8 分钟

正确写法(3 种方案)

// 方案 A: IN 语法(推荐,最简单)
String sql = "SELECT * FROM t WHERE a IN (...) AND b = ?";

// 方案 B: 分批 IN(数据量超大时)
List<List<String>> batches = Lists.partition(userIds, 1000);
for (List<String> batch : batches) {
    userDao.findByIds(batch);
}

// 方案 C: 临时表 JOIN(性能最优)
"INSERT INTO tmp_query (a, b) VALUES (?, ?), (?, ?), ...";
"SELECT t.* FROM t JOIN tmp_query USING (a, b)";

禁区 2:无退避重试

错误写法(永远不要这么写)

// 错误:无退避连续重试
for (int i = 0; i < 5; i++) {
    try { fetch(); break; }
    catch (Exception e) { log.error("retry " + i); }
}
# Python 错误写法
for i in range(5):
    try:
        fetch_data()
        break
    except Exception as e:
        log.error(f"retry {i}")

事故记录

  • 5/26 MySQL 二次伤害:连续 5 次重试,每次都打 DB

正确写法

// Resilience4j
RetryConfig config = RetryConfig.custom()
    .maxAttempts(5)
    .intervalFunction(IntervalFunction.ofExponentialBackoff(30000L, 2.0, 300000L))
    .retryOnException(e -> e instanceof TransientException)
    .build();
# Python 指数退避
import time
import random

def retry_with_backoff(func, max_attempts=5):
    delays = [30, 60, 120, 240, 300]
    for attempt in range(max_attempts):
        try:
            return func()
        except TransientException:
            if attempt == max_attempts - 1:
                return get_cached_or_default()  # 熔断
            delay = delays[attempt] + random.uniform(0, 5)
            time.sleep(delay)
        except PermanentException:
            raise  # 持续故障不重试

禁区 3:客户端缓存 > 服务端会话

错误写法(永远不要这么写)

# 错误:客户端缓存时长 > 服务端会话有效期
SDK_SESSION_HOURS = 14       # 服务端
SCRIPT_CACHE_HOURS = 24      # 客户端
# 结果:第 14h 静默中断,无任何报错

事故记录

  • 6/16 Cookie 失效导致抓取中断

正确写法

# 正确:客户端缓存 < 服务端会话,预留 10%-20% 余量
SDK_SESSION_HOURS = 14
SCRIPT_CACHE_HOURS = SDK_SESSION_HOURS - 2  # = 12h
SCRIPT_CACHE_HOURS = 12

# 正确:每次使用前校验缓存有效性
def fetch_orders():
    if not is_cookie_valid():
        re_login()
    return do_fetch()

禁区 4:用 DOUBLE 存金额

错误 DDL(永远不要这么写)

-- 错误:DOUBLE 精度溢出
amount DOUBLE(11,2)
amount FLOAT
-- 0.1 + 0.2 在 double 下不是精确的 0.3
-- 累计计算后误差放大

事故记录

  • SDK 金额存储 double 精度问题(长期未根治,周报标"架构治理最低分")

正确 DDL

-- 方案 A: DECIMAL(适合报表展示)
amount DECIMAL(15,2) NOT NULL DEFAULT 0 COMMENT '金额(元)'

-- 方案 B: BIGINT(适合高频计算,避免小数运算)
amount_cents BIGINT NOT NULL DEFAULT 0 COMMENT '金额(分)'

正确应用层写法

// 错误:double 累加
double total = 0.0;
for (Order order : orders) {
    total += order.getAmount();
}

// 正确:BigDecimal
BigDecimal total = BigDecimal.ZERO;
for (Order order : orders) {
    total = total.add(order.getAmount());
}

禁区 5:实时查明细做统计

错误 SQL(永远不要这么写)

-- 错误:直播平台查"昨日活跃",每次全表扫描
SELECT COUNT(DISTINCT user_id) FROM active_log
WHERE active_time >= ? AND active_time < ?
-- 直播平台每秒调一次 → DB 爆炸

事故记录

  • v4.24 之前的设计:实时统计打爆 DB

正确 SQL

-- 正确:读预聚合表(凌晨任务算好的)
SELECT active_count FROM stat_account_summary
WHERE stat_date = ? AND game_id = ? AND promoter_id = ?

-- 配合 Redis 缓存(5 分钟 TTL)

适用边界

  • 统计类数据(昨日/近 30 天/留存/LTV)→ 走预聚合表
  • 实时数据(最近 30 分钟活跃)→ 才允许读明细表
  • 高频查询(> 10 QPS)→ 必须走预聚合 + 缓存

禁区 6:相信上游会传正确的数据

错误写法(永远不要这么写)

// 错误:直接用入参,不校验
public Result query(List<String> userIds) {
    return userDao.findByIds(userIds);
}

事故记录

  • 5/12、5/26 都涉及"上游传了过多数据"

正确写法

// 正确:所有入参必须校验
public Result query(List<String> userIds) {
    // 1. 非空校验
    if (userIds == null || userIds.isEmpty()) {
        return Result.fail("INVALID_PARAMS", "参数不能为空");
    }
    // 2. 上限校验
    if (userIds.size() > 2000) {
        return Result.fail("BATCH_SIZE_EXCEEDED", "单次最多2000条");
    }
    // 3. 格式校验
    for (String id : userIds) {
        if (id == null || id.length() > 64) {
            return Result.fail("INVALID_PARAMS", "参数格式错误");
        }
    }
    // 4. 去重
    List<String> dedupIds = userIds.stream()
        .distinct()
        .collect(Collectors.toList());
    return userDao.findByIds(dedupIds);
}

禁区 7:群里通知一下就算"跨部门同步"

错误做法(永远不要这么做)

错误:在群里 @ 一下就算通知完事
"@产品 @发行 接口切了,你们看下"

事故记录

  • 6/16:5/26 上线新接口,5/26 晚用户反馈数据缺失,临时切换为爬虫 只群里通知,没告知产品/发行,跨部门信息不同步

正确做法(4 步走)

步骤 1: 邮件
- 收件人:产品部 + 发行部 + 技术部 + 客服部
- 主题:【变更通知】[具体变更内容]
- 正文:变更原因 + 时间 + 内容 + 影响 + 回滚 + 负责人

步骤 2: 工单
- 在 PMO / 飞书项目里建变更单
- 关联到具体需求/事故

步骤 3: 文档更新
- 在 Confluence / 飞书更新变更记录
- 写明前后差异

步骤 4: 72h 跟踪
- 变更后 72h 内确认无问题
- 出问题立即回滚并通知

禁区 8:出事后才补监控

错误流程(永远不要这么做)

错误流程:
5/12 出事 -> 5/13 加 Redis 监控
5/26 出事 -> 5/27 加 MySQL 监控

事故记录

  • 5/12 事故后才补 Redis 监控
  • 5/26 事故后才补 MySQL 监控
  • 6/16 事故前没有任何抓取任务监控

正确流程(上线即有)

所有新接口上线 = 必须有监控(QPS / 错误率 / 延迟)
所有新数据源接入 = 必须有告警(可用性 / 数据量 / 时延)
所有批量任务 = 必须有完成/失败告警
所有外部依赖 = 必须有可用性监控(DB / Redis / 第三方接口)

推荐监控清单

监控项 阈值 告警级别
接口 QPS > 1000 P2
接口错误率 > 1% P1
接口 P99 延迟 > 3s P1
MySQL CPU > 70% P1
MySQL 慢查询 > 100 条/min P1
Redis 带宽 > 800Mbps P1
抓取任务执行时长 > 1h P2
抓取任务连续失败 > 3 次 P0
跨部门数据同步延迟 > 1h P1

附录:禁区速查表

禁区 一句话 事故
1 OR 拼 SQL 用 IN / 临时表 JOIN 替代 5/26
2 无退避重试 30s -> 1min -> 2min -> 4min -> 5min 5/26
3 缓存 > 会话 客户端 < 服务端,每次校验 6/16
4 DOUBLE 存金额 用 DECIMAL(15,2) 或 BIGINT 长期隐患
5 实时查明细 走预聚合表,不查明细 v4.24 之前
6 相信上游 入参必校验:非空 + 上限 + 格式 5/12、5/26
7 群里通知 邮件 + 工单 + 文档 + 72h 跟踪 6/16
8 出事后补监控 上线即有监控和告警 5/12、5/26