
花了一个下午终于把项目里最难的那段 id 逻辑写完。剩下的部分安排到明天继续当天的运动打卡也交了。这看起来只是一条普通的工作日志但如果你亲手处理过真正复杂的 ID 问题就会明白“最难”这两个字不是客气而是 ID 背后那一连串唯一性、幂等、索引、删除、日志和业务关联的集合。很多人会把 id 理解成一个字段最多再加一个唯一索引。可在实际开发里ID 很容易变成一个卡住半天的隐性难题。它不是不会写而是写了以后很难确认“真的写完了”。这个下午过完我最大的收获不是一段正确代码而是一套判断 ID 问题有没有做完的框架。如果你现在也正在被某个 ID 相关的问题卡着这篇文章也许能帮你把思路理顺。我会从“为什么一个 ID 会这么难”开始讲到常见的 ID 生成方案、真正要小心的边界问题再落到一个下午能交付的任务拆分方法。1. 一个 ID 为什么能卡住一个下午1.1 ID 不是字段是工程闭环我在这些年看过的项目里最容易被低估的大概就是 ID。几乎所有表都有主键几乎所有接口参数都有 id但这不代表它简单。理想情况下一个 ID 要完成下面这些事在并发环境里保持唯一。在重复提交时仍能保证业务幂等。在数据库里能被索引高效使用。在删除时能确定影响范围。在日志里能作为定位线索。在数据迁移后仍然稳定。这六件事串起来才是一个完整的 ID 工程闭环。很多人只做了第一件甚至只做了“字段不重复”后面几件完全没有考虑。所以问题往往不是“生成 ID”难而是“确认这个 ID 在所有场景下都成立”难。如果只把 id 当作一个自增数字那么你早晚会在某个环节碰到问题。可能是线上突然出现重复订单可能是按 id 删除时把关联数据也带走了也可能是分表之后按 id 查询变得极其缓慢。这些都不是生成算法本身的错而是整条链路没有闭合。1.2 “写完”的判定标准没那么简单很多开发新手判断“ID 写完了”的标准是程序能跑数据库里有值不报错。这只是最小可行不是真的完成。真正的完成标准至少还要包括重复调用不产生脏数据。异常重试不会重复插入。时间回拨或并发增加时不会撞号。删除以后不会留下孤儿数据。数据迁移或跨库合并时还能保持唯一。比如自增 ID 在单表里很简单但一旦把数据从一个库迁移到另一个库或者需要把多个库的数据合并自增 ID 就很可能冲突。你会发现不是生成那段代码难而是“确认它永远没问题”难。写完一个 ID 不能只看生成不重复要确认重复提交、失败重试、数据迁移之后的唯一性仍然成立。1.3 不同语境下的 ID难度完全不一样我们经常会把很多概念都叫 ID但它们背后的规则完全不同类型典型例子核心要求数据库主键用户表 id唯一、稳定、索引友好业务编号订单号、城市 ID可读、有规则、版本可控会话 ID登录会话、请求会话随机、不可预测、短生命周期事件 ID系统日志事件可查找来源、描述清晰设备唯一 ID芯片序列号、硬件 ID跟随设备、由出厂写入外部系统 ID流程实例 ID、仓库名称符合第三方格式约束如果把这些概念混为一谈排查问题时会走很多弯路。比如系统日志里出现一个事件 ID本地找不到描述常见原因不是 ID 本身错了而是来源组件或描述文件缺失。这和数据库主键的处理思路完全不是一回事。再比如很多 MCU 芯片都有唯一 ID可以用来区分不同设备但读取方式、长度和访问权限要看芯片手册。你不能用数据库自增 ID 的思路去理解它。先分清“这是哪个层面的 ID”再谈怎么处理才是正确的起点。2. 先选对生成方案再谈优化2.1 自增主键仍然够用并不是所有系统都需要分布式 ID。很多内部管理后台、个人项目、小规模业务单库单表自增主键就是最简单可靠的选择。优点实现简单索引有序查询快方便人读。注意点并发量非常高时插入容易成为瓶颈数据合并不方便会把业务量暴露在 URL 和接口里。常见做法插入后通过 ORM 的lastInsertId或lastrowid拿到自增主键。这个流程很多人写过但要注意事务边界如果插入回滚ID 可能被消耗但不会落库。换句话说自增 ID 不一定连续只保证唯一。# 示意插入记录后获取自增主键 # 很多 ORM 会提供 lastrowid / lastInsertId 之类的接口 row_id db.execute( INSERT INTO biz_order (user_id, amount) VALUES (?, ?), [user_id, amount] ).lastrowid如果你在做一个小项目自增主键完全够用。不要因为网上都在讨论分布式 ID就觉得自增方案过时了。2.2 UUID全局唯一但别忽视索引和长度UUID 最大好处是不依赖数据库生成客户端或服务端都可以算重复概率极低。但问题往往集中在后续成本字符串长通常 36 位。作为主键时无序性可能导致页分裂。可读性差调试时很难记住。日志里如果直接输出长度会占据很多空间。如果是小数据量、低频写入UUID 带来的索引问题通常不明显。一旦数据量上了千万级就要重新评估。不要因为“不会重复”就选它。尤其是关系型数据库里主键的有序性对写入性能影响很大。2.3 雪花算法和 UUIDv7分布式场景怎么选雪花算法是很多高并发团队的选择。它把 64 位拆成时间戳、机器 ID、序列号保证趋势递增。但分布式方案从来不是免费的依赖机器 ID 分配需要保证每个节点有独立 ID。依赖系统时钟如果服务器时钟回拨可能生成重复 ID。标准处理思路是判断回拨范围小回拨等待大回拨走备用方案或直接抛错不能静默返回。UUIDv7 是相对较新的方案核心思想是让同一时间窗口内的 ID 有序同时保留随机性。它在很多场景下能兼顾全局唯一和索引局部性但生态和工具兼容性仍在逐步完善。你需要先验证数据库版本、ORM 依赖、查询工具是否都支持再决定要不要用。# 示意雪花 ID 的基本思路 64 位 1 bit 符号位 41 bit 时间戳 10 bit 机器 ID 12 bit 序列号这里的关键不是背结构而是理解凡是依赖时间的有序 ID都要处理时钟问题。2.4 做一个适合自己项目的选型判断选型时不要只看性能数字要看团队能维护什么。小团队用一个自增主键比引入一套分布式 ID 服务更务实。高并发场景用雪花算法但要明确时钟回拨和机器 ID 分配方案。新项目可以尝试 UUIDv7但要补兼容性测试。ID 方案优点典型问题通常适合自增主键简单、有序、索引友好多库合并冲突、暴露量级中小型内部系统、单表单库UUID客户端生成、几乎唯一无序、长度长、可读性差不需要排序的分布式标识雪花算法趋势递增、高性能依赖时钟和机器 ID高并发分布式写入UUIDv7时间有序、全局唯一依赖生态兼容性新项目可试用先做验证业务编码可读、含规则需要格式校验和版本管理城市 ID、订单号、流程实例选 ID 方案不是选最好的而是选团队能长期维护的。3. 下午真正要解决的是四个边界问题3.1 幂等同一请求重复提交时ID 还能唯一吗如果订单系统收到同一用户重复提交的请求一个不小心就会生成两条订单。这时候不是 ID 生成算法的问题而是缺少幂等控制。常见做法有两种客户端生成请求 ID服务端用这个请求 ID 做去重。用业务唯一键做前置检查再用唯一索引兜底。代码示意大概是这样的思路# 示意先查请求是否已处理 existing db.execute( SELECT biz_id FROM request_log WHERE request_id ?, [request_id] ).fetchone() if existing: return existing.biz_id # 不存在才插入并且靠唯一索引兜底 biz_id generate_id() db.execute( INSERT INTO biz_order (biz_id, request_id, user_id, amount) VALUES (?, ?, ?, ?), [biz_id, request_id, user_id, amount] )这里要注意先查后插在并发场景下并不可靠。真正可靠的兜底是唯一索引或唯一约束。否则两个请求同时通过检查最后还是可能插入两条数据。3.2 删除按 ID 删除前先确认作用域按 ID 删除是最常见的操作也是风险最高的操作。举一个前端例子localStorage 里缓存了某个列表如果你想根据 id 删除一条缓存直接写localStorage.removeItem(item_ id)看起来没问题。但前提是 key 的命名规则统一且这个 id 只代表当前模块。如果同一个 id 被多个模块共用或者 key 前缀不统一删除以后其他模块的数据也会丢。// 示意统一 key 前缀避免裸 ID 冲突 const keyPrefix cart_item_; const key keyPrefix itemId; localStorage.removeItem(key);数据库里删除用户数据时也一样。一个用户 ID 可能关联订单、评论、文件、日志。先删主表再删关联表还是先做软删除这需要确认 ID 的作用域。不要因为“按 id 删”这三个字就把DELETE FROM table WHERE id ?直接当成万能方案。不要在没确认作用域前就执行按 ID 的删除命令。删除是不可逆的先查一遍再删永远值得。3.3 分区、导出、字段缺失ID 和存储结构的关系分区表是另一个容易踩坑的地方。举个例子报警表按alarmtime分区每天或每月一个分区。如果业务里突然按id查询某一条报警分区裁剪可能不生效SQL 会扫描多个分区。创建索引时要结合分区键否则查询效率会打折。这不是说“按 id 查询不好”而是说设计表结构时要提前想清楚分区键是什么主键怎么设计ID 和分区键的关系是什么。还有一个常见问题从数据库导出数据到新环境如果目标表没有主键 id后续同步、去重、定位问题都会变得很麻烦。很多工具导出来的数据看着一样实际上已经失去了可追踪性。最好在导入前补一个业务唯一键或自增列避免给自己埋坑。3.4 日志里的 ID排错线索不是结论事件 ID、request id、业务 ID 在排错时都是很好的线索但不是最终答案。Windows 事件日志里如果出现某个显卡事件 ID本地找不到对应描述通常不是 ID 错了而是缺少描述文件或来源组件。这个原则放到后端接口里同样成立你用请求 ID 定位到一个具体请求但要还原完整逻辑还是得结合输入参数、时间戳、堆栈和日志上下文。调用外部 API 时如果遇到 429 Too Many Requests响应里通常会带 request id。先把它记下来再去查限流逻辑比反复重试更有用。因为 request id 能帮你区分是限流、网络重试还是代码里没有处理 429。此外有些报错说 repo id 格式不对。这种错误看起来小但本质是外部系统对 ID 有强约束。开发时要在入口处做格式校验而不是等底层报错之后才去查。4. 怎么把“最难的 ID”拆成一个下午能交付的任务4.1 先定义“完成”的最小标准如果你面对一个复杂的 ID 问题第一件事不是写代码而是定义完成后长什么样。可以问三个问题主流程是什么哪些特性必须今日完成哪些可以明天再做这个最小标准能防止一个下午陷入无限优化。以“最难的那段 ID 逻辑”为例今天的最小标准可以是生成唯一 ID、写入数据库、基本查询正常、常见异常能兜住。不是所有边界都需要当天解决。4.2 最小可运行路径先跑通主流程具体操作顺序一般建议是先写一个最简单的 ID 生成函数不追求复杂。把生成结果写入目标表确认字段类型和索引。跑通查询、更新、删除中的一条核心链路。再把幂等、并发、日志补上。这套顺序可以避免你在还没跑通时就开始优化。很多项目卡住不是因为 ID 生成不出来而是因为想一步到位结果在无关细节上浪费了时间。从工程经验看最小路径应该是最小可运行版本 - 单例验证 - 批量验证 - 异常恢复 - 提交。对个人开发者来说这个顺序同样有效。4.3 验证要从单次跑到并发和异常一次跑通只能说明正常路径没断。真正的验证要覆盖这些场景单条正常提交。重复提交。并发插入。失败重试。删除后重新插入。迁移数据后重新初始化。每次验证都看两个东西数据是否重复日志是否清晰。如果重复提交时生成了两个 ID说明幂等没做好。如果并发插入时报主键冲突要看是生成算法的问题还是唯一索引的预期行为。4.4 提交前的检查清单和明天的收尾完成以后不要急着继续写下一个功能。先做收尾检查是否有未处理的 TODO、调试输出和临时注释。确认关键函数有注释至少说明输入输出和异常策略。把验证结果写进提交信息。把明天的任务拆成 3 个以内的小块避免再次变成“巨型任务”。这是很多开发者会忽略的环节。越早熟练这套收尾流程越能避免返工。尤其是 ID 相关逻辑一旦上线后续改动的成本会很高。提交前多花十分钟可能省下后面一整天。最难的部分写完以后把剩余任务拆小比硬撑着全部完成要更可靠。5. 真正值得沉淀的不是代码而是检查单5.1 一套可以复用的 ID 处理检查单我建议把 ID 相关的经验沉淀成一张检查单而不是留在脑子里。可以按以下问题走这个 id 属于哪一层业务 ID、数据库主键、会话 ID、事件 ID谁负责生成数据库、应用、外部系统唯一性由什么保证算法、唯一索引、幂等表查询时能用索引吗分区键会不会冲突删除时影响哪些模块日志里能否用 id 定位问题数据迁移后 id 会变吗这张检查单看起来简单但如果你每次处理 ID 问题都过一遍会少踩很多坑。它不是把简单问题复杂化而是确保你不是在某一个问题里死磕而是把所有环节都检查一遍。5.2 分清必要工程化和过度设计并不是每个项目都需要雪花 ID。个人项目、内部后台、Demo自增就够了。硬上分布式 ID 会引入时钟、机器 ID、序列号、部署等额外复杂度。反过来如果你在做一个跨系统、高并发、数据需要合并的业务仍坚持自增 ID后期迁移成本会很高。核心判断标准是当前系统会不会在将来遇到多实例写入、数据合并、跨库查询。如果不确定先选最简单的方案留好扩展余地。这里没有对错只有适不适合。真正专业的判断不是你用了多先进的算法而是你清楚为什么用这个方案以及它的边界在哪里。5.3 长期维护ID 一旦上线就很难改ID 策略有一个特点上线以后很难改。因为它会被无数外键、缓存、日志、接口引用。所以当天写完以后更重要的不是“我记住了这行代码”而是把“为什么这样选”记录下来。这样半年后别人问你你还能说出当时的约束条件而不是只能说“当时就是这么写的”。如果你刚完成一个“最难的部分”我通常建议第二天先做一个小任务比如跑一遍验证脚本或者补一段注释。不要一上来就开新功能。稳定的小步推进比一个下午拼到底更可持续。回到那个下午最难的一段 id 逻辑写完剩余任务留到明天运动打卡也已经完成。真正让我放松的不是“把代码写完”而是“我知道它为什么算写完了”。如果你也正在处理类似的 ID 问题不妨先停下来把上面这套检查单走一遍。一个下午能不能做完所有事情不重要重要的是你清楚下一步该做什么。