被运营追着要报表那晚,我重构了整个数据服务

唐秀英
2026-01-03 21:14
阅读 2041

上周五晚上十点半,地铁末班车已经开走,我还在公司盯着屏幕上不断刷屏的 kubectl logs 输出。耳机里放的是 The Weeknd 的《Blinding Lights》——不是因为我有多喜欢,而是它节奏稳定,能让我在疯狂 debug 时不手抖。

我是老张,一个从 DBA 转型的后端开发,坐标北京中关村,每天通勤一小时,靠咖啡和 Postgres 的 WAL 日志续命。以前在数据库机房里调 buffer pool size,现在在 K8s 集群里调 HPA 阈值,说到底,还是在跟“延迟”和“一致性”死磕。

今天想聊聊这几年在技术探索与实践中的几个真实片段。没有高大上的架构图,只有被运营催报表催到凌晨三点的血泪史、被产品经理临时改需求的窒息感,以及一次又一次在“能跑就行”和“优雅可维护”之间艰难平衡的挣扎。


运营要实时数据?先问问数据库同不同意

去年双11前两周,运营团队突然提了个“小需求”:能不能在用户下单后 5秒内 在后台看到订单状态更新?他们要做实时转化漏斗分析。

我第一反应是:“你确定不是5分钟?”
但人家拿出了竞品截图,人家真做到了。行吧,卷就卷。

最初方案很简单:订单服务写库 → 运营后台直接查主库。结果上线第一天,主库 CPU 直接飙到 95%,慢查询日志里全是 SELECT * FROM orders WHERE status = 'paid' ORDER BY created_at DESC LIMIT 50 —— 没索引,全表扫描,还带 *

我当时真的想砸键盘。DBA 时期的 PTSD 瞬间发作。

方案一:读写分离 + 缓存

我们立刻上了 Redis 缓存层,订单状态变更时发消息到 Kafka,下游消费写入缓存。运营查缓存,不碰主库。

# k8s deployment snippet for cache worker
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: order-cache-worker
        env:
        - name: KAFKA_TOPIC
          value: "order.status.change"
        - name: REDIS_ADDR
          value: "redis-cluster.prod.svc:6379"

效果立竿见影,主库压力降了 70%。但新问题来了:缓存和数据库偶尔不一致。运营发现某笔订单“已支付”却没出现在列表里,急得差点报警。

方案二:物化视图 + CDC

作为 DBA 出身的人,我对“最终一致性”容忍度极低。于是祭出老本行:用 物化视图(Materialized View) 做预聚合,配合 Debezium 捕获数据库变更事件。

我们在 PostgreSQL 里建了一个轻量级物化视图:

CREATE MATERIALIZED VIEW mv_recent_paid_orders AS
SELECT id, user_id, amount, created_at
FROM orders
WHERE status = 'paid'
  AND created_at > NOW() - INTERVAL '1 hour';
  
CREATE INDEX idx_mv_created ON mv_recent_paid_orders(created_at DESC);

然后通过 Debezium 监听 orders 表的 binlog(其实是 WAL),一旦有新支付订单,就触发物化视图刷新(增量刷新用 REFRESH CONCURRENTLY)。

这个方案把延迟压到了 2-3 秒,一致性也保证了。运维成本略高,但值得。

教训:别让运营直接查 OLTP 数据库!哪怕是“只读”。他们的“简单查询”往往是压垮系统的最后一根稻草。


教程写得再好,也抵不过一次线上事故

今年年初,我们团队决定把老旧的 Spring Boot 单体应用拆成微服务。领导说:“要拥抱云原生!” 我心想:行,反正 K8s 我熟。

我们选了 Spring Cloud Gateway + Nacos + Seata 的组合。拆分过程还算顺利,直到上线第三天。

那天早上九点,监控告警炸了:网关 5xx 错误率飙升至 40%。用户打不开商品详情页。

排查发现,是下游商品服务的一个接口响应变慢(GC 停顿),而网关的默认超时是 30 秒。大量请求堆积,线程池耗尽,雪崩了。

我当时坐在工位上,一边喝冰美式一边看 Grafana 曲线,冷汗直冒。产品在群里@我:“老张,用户投诉收不到货,怎么回事?”

关键修复:超时+熔断+重试策略

我们紧急调整了网关配置:

spring:
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/product/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
      httpclient:
        connect-timeout: 1000   # 1秒连接超时
        response-timeout: 2s    # 2秒响应超时

resilience4j:
  circuitbreaker:
    instances:
      productService:
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        sliding-window-size: 10

同时,在商品服务内部加了 本地缓存兜底:即使 DB 挂了,也能返回最近 5 分钟的商品快照。

这次事故后,我写了份内部 《微服务容错实战教程》,重点不是讲理论,而是列出了 10 个真实踩过的坑,比如:

问题 表现 解决方案
网关无超时 线程池耗尽 显式设置 connect/response timeout
无熔断机制 雪崩 Resilience4j + Hystrix
日志无 TraceID 排查困难 强制透传 X-B3-TraceId
服务注册慢 启动失败 K8s readinessProbe 延迟调高

这份教程后来成了新人必读文档。最好的教程,往往诞生于深夜的故障复盘会。


从“能跑就行”到“可运维”的转变

刚转后端那会儿,我写的代码只关心功能是否实现。DBA 时期养成的“数据严谨性”习惯,一度被同事吐槽“太较真”。

但随着系统规模扩大,我发现:可运维性 = 可观测性 + 可恢复性 + 可配置性

举个例子:我们有个定时任务,每天凌晨跑数据对账。最初是硬编码 cron 表达式:

@Scheduled(cron = "0 0 2 * * ?") // 凌晨2点
public void reconcile() {
    // ...
}

结果有一次生产环境时间不对(Docker 容器时区没设),任务没跑,第二天财务对不上账,差点背锅。

后来我们改成 动态配置 + 手动触发接口

@ConfigurationProperties(prefix = "job.reconcile")
@Data
public class ReconcileJobConfig {
    private String cron;
    private boolean enabled = true;
}

// 同时暴露 /actuator/reconcile/trigger 接口
@PostMapping("/trigger")
public ResponseEntity<String> triggerManually() {
    reconcileService.runNow();
    return ok("Triggered");
}

配合 K8s 的 ConfigMap + Prometheus 监控任务执行状态,终于睡得踏实了。


技术探索的本质,是解决人的焦虑

很多人觉得技术探索就是学新框架、玩新工具。但在我眼里,真正的技术探索,是为了缓解业务方的焦虑,减少团队的摩擦,让系统在 deadline 前稳如老狗

运营要实时数据?背后是他们怕错过转化机会。
产品要快速迭代?背后是 KPI 压力。
测试要自动化?背后是不想半夜被叫起来验回归。

我们写代码,终究是在服务人,而不是服务机器。

所以,我的经验总结下来就三点:

  1. 永远假设上游会乱来,下游会挂掉 —— 做好防御式编程。
  2. 可观测性不是附加项,是核心功能 —— 没日志、没指标、没链路追踪的系统等于黑盒。
  3. 文档和教程要写给“明天的自己”看 —— 你总会忘记为什么当初要这么设计。

最后一点碎碎念

现在我写代码前,还是会下意识想:“这个表要不要加索引?”“这个事务会不会锁太久?”——DBA 的执念,深入骨髓。

但我也学会了妥协:有时候为了快速交付,先上个脚本 + crontab 也能活。关键是 留好重构入口,别让“临时方案”变成“祖传代码”。

技术没有银弹,只有权衡。而每一次权衡的背后,都是对业务、团队、时间的深刻理解。

哦对了,那晚被运营追着要报表之后,我们做了一套 自助式 BI 平台,让他们自己拖拽生成报表。现在他们很少找我了——除了问我“为什么这个图表加载这么慢?”

我微微一笑,打开 Explain Plan,又是一场新的战斗。


P.S. 如果你也经历过“被运营支配的恐惧”,欢迎留言交流。顺便求推荐好听的 coding BGM,The Weeknd 听腻了。

评论 0

最热最新
暂无评论
唐秀英Lv.1
0
影响力
0
文章
0
粉丝