一个错误的架构决策,差点毁了我的职业生涯……

TechWithNeer 2025-10-10 11:43:54
我从事后端系统建设已有七年多了,把应用程序从100个并发用户扩展到了10万个,设计过每月处理数十亿次请求的微型服务,也指导过几十名工程师。但有一个架构决策一直困扰着我,它单独毁掉了我手上的三个主要项目,并给我上了职业生涯中最昂贵的一课。

 

这个决策是什么?过早的数据库抽象

 

那个愚弄了我的模式!

 

它开始得很无辜。刚读完《整洁架构》,又手握SOLID原则,我以为通过在精巧的仓库模式和ORM后面抽象数据库交互,自己很聪明。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// What I thought was "clean architecture"type UserRepository interface {    GetUser(id string) (*User, error)    CreateUser(user *User) error    UpdateUser(user *User) error    DeleteUser(id stringerror    FindUsersByStatus(status string) ([]*User, error)}type userRepositoryImpl struct {    db *gorm.DB}func (r *userRepositoryImpl) GetUser(id string) (*User, error) {    var user User    if err := r.db.First(&user, "id = ?", id).Error; err != nil {        return nil, err    }    return &user, nil}

 

看起来很整洁,对吧?每个数据库调用都被抽象了。每个查询都藏在整齐的接口后面。我可以轻松更换数据库。能出什么错?

 

项目一:电商平台

 

时间线:2019年

规模:5万日活用户

技术栈:Go、PostgreSQL、GORM

 

第一个牺牲品是一个电商平台。我们的商品目录关系复杂——类别、变体、价格层级、库存跟踪。随着业务需求演进,抽象成了牢笼。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// Business requirement: "Show products with variants in stock, grouped by category"// What the abstraction forced me to write:func (s *ProductService) GetAvailableProductsByCategory() ([]CategoryProducts, error) {    categories, err := s.categoryRepo.GetAll()    if err != nil {        return nil, err    }    var result []CategoryProducts    for _, category := range categories {        products, err := s.productRepo.GetByCategory(category.ID)        if err != nil {            return nil, err        }        var availableProducts []Product        for _, product := range products {            variants, err := s.variantRepo.GetByProductID(product.ID)            if err != nil {                return nil, err            }            hasStock := false            for _, variant := range variants {                if variant.Stock > 0 {                    hasStock = true                    break                }            }            if hasStock {                availableProducts = append(availableProducts, product)            }        }        result = append(result, CategoryProducts{            Category: category,            Products: availableProducts,        })    }    return result, nil}

 

结果是什么? 到处是N+1查询。原本一条JOIN就能搞定的事,变成了几百次数据库往返。

 

性能冲击:

 

  • 页面加载时间:3.2秒

  • 每请求数据库连接:847个

  • 用户跳出率:67%

 

黑五周末,商品页扛不住流量,公司损失了20万美元收入。

 

项目二:分析仪表盘

 

时间线:2021年

规模:每天200万事件的实时分析

技术栈:Node.js、MongoDB、Mongoose

 

没从第一次失败吸取教训,我在一个实时分析平台上加倍下注抽象。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// The "clean" way I structured itclass EventRepository {    async findEventsByTimeRange(startDate, endDate) {        return await Event.find({            timestamp: { $gte: startDate, $lte: endDate }        });    }    async aggregateEventsByType(events) {        // Client-side aggregation because "separation of concerns"        const aggregated = {};        events.forEach(event => {            aggregated[event.type] = (aggregated[event.type] || 0) + 1;        });        return aggregated;    }}

 

灾难现场:

 

架构概览(我造的):

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
客户端请求API网关分析服务事件仓库(抽象层)MongoDB(抓取200万+文档)内存聚合(Node.js堆溢出)503服务不可用

 

本该的样子:

 

  •  
客户端请求 → API网关 → MongoDB聚合管道 → 响应

 

要命的数据:

 

  • 内存占用:每请求8GB+

  • 响应时间:45秒+(超时前)

  • 服务器崩溃:每天12次

  • 客户流失:34%

 

项目三:最后一课

 

时间线:2023年

规模:每月5亿次请求的微型服务

技术栈:Go、PostgreSQL、Docker、Kubernetes

 

到2023年,我以为自己学乖了,对性能更上心,但还是抱着那些抽象模式不放。

 

压垮骆驼的最后一根草,是我们要做带复杂SQL聚合的财务报表:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
-- What the business actually neededWITH monthly_revenue AS (    SELECT         DATE_TRUNC('month', created_at) as month,        SUM(amount) as revenue,        COUNT(*as transaction_count    FROM transactions t    JOIN accounts a ON t.account_id = a.id    WHERE a.status = 'active'       AND t.created_at >= '2023-01-01'    GROUP BY DATE_TRUNC('month', created_at)),growth_analysis AS (    SELECT         month,        revenue,        transaction_count,        LAG(revenue) OVER (ORDER BY monthas prev_month_revenue,        revenue / LAG(revenue) OVER (ORDER BY month- 1 as growth_rate    FROM monthly_revenue)SELECT * FROM growth_analysis WHERE growth_rate IS NOT NULL;

 

我的抽象逼出了这个怪物:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 47 lines of Go code to replicate a 20-line SQL queryfunc (s *ReportService) GenerateMonthlyGrowthReport() (*GrowthReport, error) {    // Multiple repository calls    // Manual data processing    // In-memory aggregations    // Complex business logic spread across 3 services}

 

性能对比:

 

  • 原生SQL:120毫秒,1个数据库连接

  • 抽象版:2.8秒,15个数据库连接

  • 内存占用:高10倍

  • 代码复杂度:增加200%

 

真正管用的架构

 

三个项目折戟后,我终于悟了。现在我这么干:

 

2024现代架构:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
┌─────────────────┐│ HTTP API │├─────────────────┤│ 业务逻辑 │ ← 薄层,专注业务规则├─────────────────┤│ 查询层 │ ← 直接SQL/NoSQL查询,已优化├─────────────────┤│ 数据库 │ ← 让数据库干它擅长的事└─────────────────┘

 

真实代码示例:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// Current approach: Let the database do database thingstype FinanceService struct {    db *sql.DB}
func (s *FinanceService) GetMonthlyGrowthReport(ctx context.Context) (*GrowthReport, error) {    query := `    WITH monthly_revenue AS (        SELECT             DATE_TRUNC('month', created_at) as month,            SUM(amountas revenue,            COUNT(*as transaction_count        FROM transactions t        JOIN accounts a ON t.account_id = a.id        WHERE a.status = 'active'           AND t.created_at >= $1        GROUP BY DATE_TRUNC('month', created_at)    ),    growth_analysis AS (        SELECT             month,            revenue,            transaction_count,            LAG(revenue) OVER (ORDER BY monthas prev_month_revenue,            revenue / LAG(revenueOVER (ORDER BY month) - 1 as growth_rate        FROM monthly_revenue    )    SELECT month, revenue, transaction_count, growth_rate     FROM growth_analysis WHERE growth_rate IS NOT NULL`    rows, err := s.db.QueryContext(ctx, query, time.Now().AddDate(-200))    if err != nil {        return nil, fmt.Errorf("failed to execute growth report query: %w", err)    }    defer rows.Close()
    // Simple result mapping, no business logic    return s.mapRowsToGrowthReport(rows)}

 

改变一切的教训

 

抽象不是架构。 数据库不只是傻存储,它们是专用计算引擎。PostgreSQL的查询规划器比你写的Go循环聪明。MongoDB的聚合管道比你JavaScript的reduce快。

 

我的新原则:

 

  • 啥活用啥家伙:让数据库处理数据操作

  • 为变化优化,不为替换:业务逻辑变得比数据库引擎勤

  • 一切都要测:性能指标比干净接口重要

  • 拥抱数据库特性:窗口函数、CTE、索引都是好朋友

 

现在我设计的系统,负载高10倍,代码却少50%,响应时间提升800%。开发速度也上去了,因为我们不再跟抽象打架。

 

最痛的领悟: 有时候最好的架构决策就是你压根不做的那个。

 

七年过去,我明白了好架构不是套模式,而是懂权衡,基于真约束而非假想敌做决策。

 

作者丨TechWithNeer       编译丨Rio
来源丨网址:https://medium.com/@neerupujari5/the-one-architecture-decision-that-destroyed-every-project-i-touched-627fd83bea0f
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告