# 祈盟SDK 代码禁区 > 这些写法在本项目历史中出过事。 > AI 生成代码时,必须避免以下 8 种模式。 > 最后更新:2026-06-30 --- ## 禁区 1:OR 条件拼 SQL ### 错误写法(永远不要这么写) ```java // 错误:直接拼 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 种方案) ```java // 方案 A: IN 语法(推荐,最简单) String sql = "SELECT * FROM t WHERE a IN (...) AND b = ?"; // 方案 B: 分批 IN(数据量超大时) List> batches = Lists.partition(userIds, 1000); for (List 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:无退避重试 ### 错误写法(永远不要这么写) ```java // 错误:无退避连续重试 for (int i = 0; i < 5; i++) { try { fetch(); break; } catch (Exception e) { log.error("retry " + i); } } ``` ```python # Python 错误写法 for i in range(5): try: fetch_data() break except Exception as e: log.error(f"retry {i}") ``` ### 事故记录 - 5/26 MySQL 二次伤害:连续 5 次重试,每次都打 DB ### 正确写法 ```java // Resilience4j RetryConfig config = RetryConfig.custom() .maxAttempts(5) .intervalFunction(IntervalFunction.ofExponentialBackoff(30000L, 2.0, 300000L)) .retryOnException(e -> e instanceof TransientException) .build(); ``` ```python # 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:客户端缓存 > 服务端会话 ### 错误写法(永远不要这么写) ```python # 错误:客户端缓存时长 > 服务端会话有效期 SDK_SESSION_HOURS = 14 # 服务端 SCRIPT_CACHE_HOURS = 24 # 客户端 # 结果:第 14h 静默中断,无任何报错 ``` ### 事故记录 - 6/16 Cookie 失效导致抓取中断 ### 正确写法 ```python # 正确:客户端缓存 < 服务端会话,预留 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(永远不要这么写) ```sql -- 错误:DOUBLE 精度溢出 amount DOUBLE(11,2) amount FLOAT -- 0.1 + 0.2 在 double 下不是精确的 0.3 -- 累计计算后误差放大 ``` ### 事故记录 - SDK 金额存储 double 精度问题(长期未根治,周报标"架构治理最低分") ### 正确 DDL ```sql -- 方案 A: DECIMAL(适合报表展示) amount DECIMAL(15,2) NOT NULL DEFAULT 0 COMMENT '金额(元)' -- 方案 B: BIGINT(适合高频计算,避免小数运算) amount_cents BIGINT NOT NULL DEFAULT 0 COMMENT '金额(分)' ``` ### 正确应用层写法 ```java // 错误: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(永远不要这么写) ```sql -- 错误:直播平台查"昨日活跃",每次全表扫描 SELECT COUNT(DISTINCT user_id) FROM active_log WHERE active_time >= ? AND active_time < ? -- 直播平台每秒调一次 → DB 爆炸 ``` ### 事故记录 - v4.24 之前的设计:实时统计打爆 DB ### 正确 SQL ```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:相信上游会传正确的数据 ### 错误写法(永远不要这么写) ```java // 错误:直接用入参,不校验 public Result query(List userIds) { return userDao.findByIds(userIds); } ``` ### 事故记录 - 5/12、5/26 都涉及"上游传了过多数据" ### 正确写法 ```java // 正确:所有入参必须校验 public Result query(List 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 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 |