如何记录每次写入的交易 ID?
解读
国内面试官问“交易 ID”时,往往把 CouchDB 的“写操作”类比为金融或电商场景里的“交易”,本质是想确认两点:
- 你能否在最终一致、无全局锁的分布式环境里,给每一次写请求一个业务层面唯一、可回溯、可审计的标识;
- 你能否保证该标识在离线→同步→冲突解决全链路不丢失、不重复,且对客户端透明。
如果仅回答“用 _id 就行”会被认为太浅;若直接搬关系型数据库的 Sequence 或全局事务号,又会被质疑“不懂 CouchDB 模型”。因此必须结合文档版本机制 + 本地序列 + 复制冲突策略给出可落地的“中国式”方案。
知识点
- _id 与 _rev 语义:_id 是文档唯一键,_rev 是 MVCC 版本号,二者共同构成一次“写”的物理标识,但都不等同于业务交易 ID。
- update_handler:CouchDB 内置的服务器端钩子,可在写入前注入自定义逻辑,原子性由 Erlang 层保证,避免客户端“读-改-写”竞争。
- _local 文档:以 _local/ 开头的特殊文档不会复制,可用来存放节点级自增序列,实现“本地有序、全局唯一”的雪花号段。
- cradle、nano、erlang couchbeam 等国内常用驱动均支持自定义请求头,可在 HTTP 层携带 X-Tx-ID,供 Nginx 日志、网关链路追踪。
- 国内等保 2.0 与金融行业规范要求“写操作留痕”,因此交易 ID 必须落盘到文档内部并建立视图索引,以便后续审计报表快速检索。
答案
生产级做法分三层:
- 业务层序列:每个节点维护一份 _local/seq 文档,存放“当前号段最大值”,号段长度 1000~10000,由运维脚本定期扩容;写入时由 update_handler 把“节点编号 + 号段值”组合成 64 位雪花 ID,原子加一后回填到文档 tx_id 字段。
- 视图索引:设计视图
function (doc) { if (doc.tx_id) emit(doc.tx_id, {_id: doc._id, _rev: doc._rev}); }
国内审计系统可直接通过/{db}/_design/audit/_view/tx?key="雪花ID"在百毫秒级定位原始记录。 - 复制冲突兜底:若出现冲突,冲突文档的 tx_id 不会重复(雪花 ID 已全局唯一),在业务合并函数里保留 tx_id 数组,供后台对账系统追溯。
一句话总结:用 _local 号段 + update_handler 雪花算法生成 tx_id,落盘到文档并建视图,既满足 CouchDB 分布式语义,又符合国内审计合规要求。
拓展思考
- 如果业务要求同一笔交易修改多文档,可把 tx_id 作为批量写入的公共字段,再设计“主档-子档”模式:主档带状态机,子档通过 tx_id 关联,冲突解决时以主档状态为准,子档重放补偿。
- 在跨省多活场景,可给每个机房分配奇偶号段,通过 _local/seq 的步长=2 做号段隔离,避免机房级网络隔离时雪花重复。
- 国内部分银行采用CouchDB + Kafka 架构,写入后由 update_handler 把 tx_id 推送到 Kafka 统一对账,此时需幂等 Producer,并在 Kafka 消息 key 里携带 tx_id,实现“DB-消息”双向可追踪。