当老板说「数据库里加个字段就行了」时,我在想什么——一个后端开发的奇葩需求大赏

16 阅读1分钟

当老板说「数据库里加个字段就行了」时,我在想什么——一个后端开发的奇葩需求大赏

带话题:#奇葩需求大赏

前言

大家好,我是梦见猫,一个全栈独立开发者。前面聊了 AI 时代和前端角度的奇葩需求,今天换个频道——聊聊后端开发那些让人血压飙升的「简单需求」。

做后端久了,你会发现一个规律:需求越短,坑越大。

  • 「在数据库里加个字段就行」——实际要改 5 张表、3 个 API、2 个定时任务
  • 「加个导出功能,就拉个 Excel」——数据量 200 万行,OOM 了
  • 「加个定时任务,每天跑一次」——然后凌晨 3 点被报警电话打醒

这些需求,产品经理说出来可能只需要 10 秒,但落地的时候,往往是无数个深夜和无数次「为什么要这样设计」的自我怀疑。

今天就来聊聊我亲身经历的几个后端开发奇葩需求,以及我是怎么活着熬过来的。

需求一:「加个字段就行,数据库不就是加一列吗?」

需求背景

去年年底,我接手了一个电商系统的维护工作。某天产品经理突然找我:「有个小需求,在商品表里加个字段,记录一下商品的推荐指数,方便运营排序。」

我心说:嗯,确实是个小需求。

然后他补充了一句:「哦对了,这个推荐指数每天凌晨自动更新一次,根据销量、评分、收藏数、浏览量、退货率综合计算。然后运营后台要能手动调整,有历史记录,还有,这个字段要能按权限控制谁看得到。」

我沉默了。

技术分析

这个需求的真实工作量,和「加个字段」差了十万八千里:

表面需求:加个字段
实际需求:
├── 数据库:商品表加字段,但还要建历史记录表
├── 定时任务:每天凌晨计算推荐指数
├── 算法:销量、评分、收藏、浏览、退货率的加权公式
├── API:运营后台手动调整 + 权限控制 + 变更日志
├── 缓存:商品列表页读取这个字段,需要缓存策略
└── 兼容:数据库迁移方案 + 回滚方案

最坑的是,这个「简单需求」里藏着一个分布式计算难题。

我们当时有 200 万商品,如果凌晨 0 点全量计算,哪怕每条耗时 10ms,也要 20000 秒——将近 6 个小时。等算完天都亮了。

我的方案

我花了 3 天时间,做了以下设计:

  1. 增量更新:只计算当天有变动的商品(销量变了、新评论了等),而不是全量
  2. 分片并行:用消息队列把商品 ID 分到 10 个 worker 并行处理
  3. 兜底全量:每周日做一次全量重算,防止增量累积误差
# 推荐指数增量更新方案
import redis
from celery import Celery
from typing import List, Dict

app = Celery('recommend', broker='redis://localhost:6379/0')

# 权重配置(可动态调整)
WEIGHTS = {
    'sales': 0.35,
    'rating': 0.25,
    'favorites': 0.15,
    'views': 0.15,
    'return_rate': -0.10  # 退货率是负向指标
}

# 记录哪些商品需要更新
def mark_product_dirty(product_id: str):
    """商品发生变更时,标记需要重新计算"""
    redis.sadd('recommend:dirty_products', product_id)

@app.task
def update_recommend_scores():
    """凌晨定时任务:增量更新推荐指数"""
    # 1. 获取需要更新的商品 ID
    dirty_ids = redis.smembers('recommend:dirty_products')
    if not dirty_ids:
        return

    # 2. 分片(每批 1000 个)
    batch_size = 1000
    batches = [list(dirty_ids)[i:i+batch_size]
               for i in range(0, len(dirty_ids), batch_size)]

    # 3. 并行处理
    for batch in batches:
        update_batch.delay(batch)

    # 4. 清空脏标记
    redis.delete('recommend:dirty_products')

@app.task
def update_batch(product_ids: List[str]):
    """批量更新推荐指数"""
    scores = {}
    for pid in product_ids:
        # 从缓存/数据库获取各维度数据
        metrics = get_product_metrics(pid)
        # 加权计算
        score = sum(
            metrics.get(k, 0) * w
            for k, w in WEIGHTS.items()
        )
        scores[pid] = round(score, 2)

    # 批量写入数据库
    batch_update_product_scores(scores)

成果

上线后,推荐指数计算从 6 小时缩短到 15 分钟。运营后台的排序功能也顺利上线。

但最让我哭笑不得的是——产品经理后来跟我说:「其实你可以不用做那么复杂,我就想加个字段,运营手动填就行了。」

我:……那你之前说的那些需求呢?

他:「哦,那些是我觉得既然做了就做完整一点。」

这个需求教会我一件事:产品经理说的「顺便」和「既然做了」,是后端开发最大的坑。

需求二:「导出功能很简单,就拉个 Excel 吧」

需求背景

另一个项目,客户需要一个数据导出功能。他的原话是:「就是后台那个数据表格,加个导出按钮,点一下下载 Excel,很简单的吧?」

我问他:「数据量大概多少?」

他:「不多,就几万条吧。」

我去看了下数据库——好家伙,200 万条。

技术分析

导出功能的坑,不在实现本身,而在于数据量

数据量方案风险
< 1 万一次性查询 + 生成文件没什么风险
1 万 - 10 万分页查询 + 流式写入注意内存
10 万 - 100 万异步导出 + 分片 + 队列需要任务系统
> 100 万异步导出 + 多 worker + 文件合并相当复杂

200 万条数据,一次性导出 = 内存爆炸 = 服务器 OOM = 凌晨被叫醒。

我的方案

# 异步导出方案
import asyncio
from io import BytesIO
import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill

class AsyncExcelExporter:
    """异步 Excel 导出器"""
    def __init__(self, export_id: str, query_params: dict):
        self.export_id = export_id
        self.query_params = query_params
        self.batch_size = 5000  # 每批 5000 条

    async def export(self):
        # 1. 先查总数
        total = await self.get_total_count()
        # 2. 更新导出任务状态
        await self.update_progress(0, total)

        # 3. 创建 Excel 文件
        wb = openpyxl.Workbook()
        ws = wb.active
        # 写入表头
        headers = ['ID', '商品名称', '价格', '销量', '评分', '创建时间']
        ws.append(headers)

        # 4. 分页查询 + 流式写入
        offset = 0
        while offset < total:
            batch = await self.query_batch(offset, self.batch_size)
            for row in batch:
                ws.append(row)
            offset += self.batch_size
            # 更新进度
            await self.update_progress(min(offset, total), total)
            # 每 5 万条释放一次内存
            if offset % 50000 == 0:
                gc.collect()

        # 5. 保存到文件/OSS
        buffer = BytesIO()
        wb.save(buffer)
        buffer.seek(0)
        file_url = await self.upload_to_oss(buffer)

        # 6. 通知用户下载
        await self.notify_user(file_url)
        return file_url

成果

异步导出上线后,用户点击「导出」按钮会收到一个提示:「导出任务已创建,完成后会通知您下载」。导出完成后,系统自动发送通知,用户点击链接即可下载。

客户反馈:「这个导出功能做得不错,不过我后来发现其实不需要导出全部数据,一般就导出筛选后的几千条就够了。」

我又沉默了。

需求三:「安全性不用管,先上线再说」

需求背景

这个需求来自一个创业团队。他们急着上线 MVP,老板说:「安全性先不用管,我们先把功能跑通,后面再补。」

我做了一个内部管理后台,但老板说「登录太麻烦了,能不能去掉?」。于是我把登录给去掉了,直接裸奔上线。

然后——两周后,用户数据被爬了。

技术分析

「先上线再补安全」是创业项目最常见的坑。安全不是「功能」,而是基础设施。事后补安全的成本,是提前做的 10 倍。

常见的安全「先上线」陷阱:

1. 没有任何认证 → 接口裸奔,数据被爬
2. 密码明文存储 → 数据库泄露 = 用户密码泄露
3. SQL 拼接 → SQL 注入,删库跑路
4. 文件上传无校验 → 服务器被挂马
5. 接口无限流 → 被刷接口,账单爆炸
6. 日志里打印敏感信息 → 密码、token 全在日志里

我的教训

这次事故后,我给自己列了一个「最小安全清单」:

# 哪怕再赶时间,这些也要做:
MINIMUM_SECURITY_CHECKLIST = [
    '✅ 所有接口至少加 JWT 认证',
    '✅ 密码哈希存储(bcrypt/argon2),绝不明文',
    '✅ 使用 ORM 参数化查询,禁止 SQL 拼接',
    '✅ 文件上传:限制类型、大小、扫描病毒',
    '✅ 敏感接口加限流(rate limiting)',
    '✅ 生产环境日志脱敏(不打印密码、token)',
    '✅ CORS 配置白名单',
    '✅ HTTPS 强制',
    '✅ 数据库访问 IP 白名单',
]

类似的事情防不胜防。我后来给团队上了一课:

安全不是「功能」,是地基。地基没打好,楼盖得再高也会塌。而且修地基的时候,楼上的人还得全部搬走——这个成本,你付得起吗?

需求四:「把数据库从 MySQL 换成 MongoDB,因为听说更快」

需求背景

这个需求来自一个 CTO。他在某技术大会上听了 MongoDB 的分享,回来就决定:「我们的用户系统数据量太大了,MySQL 扛不住,换成 MongoDB 吧。」

我问他:「用户系统有哪些查询场景?」

他:「登录、注册、查用户信息、修改资料、分页列表……就这些。」

我:「这些场景 MySQL 完全能搞定啊,而且我们用的是关系型数据。」

他:「但 MongoDB 更快啊,NoSQL 是趋势。」

技术分析

这个需求的核心问题不是技术选型,而是:「听说更快」是技术选型最危险的理由。

MySQL vs MongoDB 的核心区别:

维度MySQLMongoDB
数据模型关系型,表+行文档型,集合+文档
适用场景结构化数据,事务半结构化数据,灵活 schema
关联查询JOIN 能力强不适合多表关联
事务ACID,成熟4.0+ 支持多文档事务
数据一致性强一致默认最终一致

用户系统是典型的关系型数据

用户表 ←→ 用户资料表(1:1)
用户表 ←→ 订单表(1:N)
用户表 ←→ 收货地址表(1:N)
用户表 ←→ 关注关系表(N:N,自关联)

这种场景换成 MongoDB,等于用锤子拧螺丝——不是不能拧,但拧完你会后悔为什么不用螺丝刀。

我的方案

我没有直接拒绝,而是做了三件事:

  1. 用 MongoDB 写了一个 POC:把用户系统核心功能用 MongoDB 实现了一遍
  2. 对比测试:同样的数据量,对比两种方案的查询性能
  3. 列出真正的问题:MySQL 慢不是因为数据库本身,而是因为缺少索引、查询没优化
-- 测试结果:MySQL 加了索引后,性能完全够用
-- 优化前:全表扫描,200ms
SELECT * FROM users WHERE email = 'test@example.com';

-- 优化后:加索引,0.5ms
CREATE INDEX idx_users_email ON users(email);
SELECT * FROM users WHERE email = 'test@example.com';

结论:MySQL 不是不行,是我们的索引没加对。

成果

最终 CTO 看了对比数据,放弃了迁移。我们花了 3 天优化索引和查询,用户系统的响应时间从 200ms 降到了 5ms。

很多时候,不是技术不行,是你还没用对。换数据库不如先优化现有的。

需求五:「加个定时任务,每天早上 8 点跑一下」

需求背景

这个需求看起来最简单——加个定时任务。但它是所有后端开发噩梦的起点。

我前前后后遇到过这些「定时任务」:

  • 「每天凌晨 2 点同步一次数据」——结果数据源挂了,全量同步失败,第二天全公司数据不一致
  • 「每小时清理一次过期数据」——结果清理逻辑有 bug,把还没过期的数据也删了
  • 「每分钟检查一次服务状态」——结果检查接口本身有性能问题,每分钟打一次,把服务器打挂了

技术分析

定时任务最大的坑,不在「怎么写」,而在「它挂了怎么办」。

定时任务踩坑大全:

1. 任务执行时间超过调度间隔 → 任务堆积,系统崩了
2. 任务执行失败 → 没人知道,数据一直有问题
3. 分布式环境重复执行 → 同一任务跑了多次
4. 服务器时钟不同步 → 本该 8 点执行的任务,有些服务器 7 点就跑了
5. 没有幂等性 → 重试导致数据重复
6. 没有超时控制 → 任务卡死,占用资源

我的方案

# 一个靠谱的定时任务框架
import asyncio
from datetime import datetime, timedelta
import logging
from functools import wraps

logger = logging.getLogger(__name__)

def scheduled_task(
    name: str,
    max_runtime: int = 300,  # 最大执行时间(秒)
    max_retries: int = 3,
    retry_delay: int = 60
):
    """定时任务装饰器:自动处理失败、超时、通知"""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            start_time = datetime.now()

            for attempt in range(max_retries + 1):
                try:
                    # 超时控制
                    result = await asyncio.wait_for(
                        func(*args, **kwargs),
                        timeout=max_runtime
                    )
                    # 记录成功
                    elapsed = (datetime.now() - start_time).total_seconds()
                    logger.info(
                        f'[定时任务] {name} 执行成功 '
                        f'(耗时: {elapsed:.1f}s, 重试: {attempt}次)'
                    )
                    return result

                except asyncio.TimeoutError:
                    logger.error(
                        f'[定时任务] {name} 执行超时 '
                        f'(超过 {max_runtime}s, 第 {attempt+1} 次尝试)'
                    )

                except Exception as e:
                    logger.error(
                        f'[定时任务] {name} 执行失败: {e} '
                        f'(第 {attempt+1} 次尝试)'
                    )

                if attempt < max_retries:
                    await asyncio.sleep(retry_delay)
                else:
                    # 重试耗尽,发送告警
                    await send_alert(
                        f'定时任务 {name} 连续失败 {max_retries+1} 次,请检查!'
                    )
                    raise

        return wrapper
    return decorator

# 使用示例
@scheduled_task(name='sync-user-data', max_runtime=600, max_retries=3)
async def sync_user_data():
    """同步用户数据"""
    # 1. 获取分布式锁(防止重复执行)
    lock = await acquire_lock('sync-user-data', ttl=600)
    if not lock:
        logger.info('上一次任务还在执行中,跳过')
        return

    try:
        # 2. 幂等性:使用批次号,确保同一条数据不会被重复处理
        batch_id = f"batch_{datetime.now().strftime('%Y%m%d%H%M%S')}"
        # 3. 分页处理,每批记录进度
        # 4. 异常处理:单条失败不影响整体
        pass
    finally:
        await release_lock(lock)

三条铁律

吃了这么多亏后,我给自己定了三条定时任务铁律:

  1. 必须带告警:任务失败 = 自动通知,不能等用户发现
  2. 必须幂等:重试不会导致数据重复
  3. 必须可观测:日志、耗时、成功率,一个都不能少

写在最后:后端开发的「简单」和「复杂」

回顾这些奇葩需求,我发现一个规律:

需求方说的「简单」,和开发理解的「简单」,是两个完全不同的概念。

需求方理解的「简单」= 需求描述短,听起来直观 开发理解的「简单」= 技术方案清晰,无坑,改动范围小

这中间的 Gap,就是后端开发最常踩的坑。

给同行们的建议:

  • 「加个字段」:先问清楚要这个字段干什么,可能涉及的表、接口、缓存、定时任务
  • 「导出功能」:先问数据量,再设计方案
  • 「换个数据库」:先问痛点,优化现有方案,再考虑迁移
  • 「先上线再说」:安全是地基,不能后面补
  • 「定时任务」:告警、幂等、可观测,缺一不可

最后,给产品经理们一句话:

当你觉得一个需求很简单的时候,请相信——它一定不简单。 如果它真的很简单,说明你还没想清楚它到底要做什么。

你遇到过哪些让你血压飙升的后端需求?欢迎在评论区分享,我也想看看大家的「深夜排查日志」瞬间 😂


#奇葩需求大赏 #后端开发 #程序员日常 #数据库 #技术分享