如何基于 TPC-C 模型把“订单”映射为 CouchDB 文档?

解读

面试官想知道两件事:

  1. 你是否真正理解 TPC-C 订单的五张关系表(oorder、order_line、new_order、customer、district)以及它们之间的主外键约束
  2. 你是否能把这套强范式、行级、事务型模型,无损地反范式化成一份 CouchDB JSON 文档,同时兼顾 CouchDB 的MVCC、离线同步、视图索引三大机制。
    在国内金融、运营商、零售中台面试里,这道题常作为“分布式事务退化”+“文档建模”双重考点,答得太浅会被追问“余额超卖怎么办”,答得太深又容易把 CouchDB 当成 MongoDB 讲,必须紧扣 CouchDB 的 _id、_rev、attachments、validation function 来展开。

知识点

  1. TPC-C 订单主键:(W_ID, D_ID, O_ID) 全局唯一,O_ID 在每个 (W_ID,D_ID) 内自增;CouchDB 的 _id 是整个集群唯一,因此需要把三元组扁平化成字符串并保证字节序可排序
  2. CouchDB 没有外键,也没有行级锁,所有关联必须内嵌或冗余;但冗余后又要防止同步冲突,需要给每个冗余字段加业务时间戳+源节点标识,方便做last-write-wins之外的业务合并
  3. 订单状态流:new_order 行存在即“待发货”,删除即“已发货”;在 CouchDB 里不能真删,用状态字段+TTL 视图实现“逻辑删除”,否则触发MVCC 墓碑同步风暴
  4. 订单行明细高达 15 条,CouchDB 单文档最大 4 MB,必须内嵌;若未来可能膨胀到百行,需要拆成主文档+行明细附件,用 attachment 存储 gzip 后的 JSON 数组,减少带宽与索引开销
  5. 国内等保 3 级要求敏感字段脱敏,CouchDB 提供field-level AES 加密的第三方插件,面试时主动提及可加分。
  6. 视图索引:国内大厂常用couchdb-luceneelasticsearch-couchdb-river做二级索引,但面试官更想听你如何用原生 JavaScript map 函数(W_ID,D_ID,O_ID) 拆出来做复合键,实现按仓号+区号范围查订单

答案

  1. _id 设计
    采用定长字节序字符串:w<W_ID>_d<D_ID>_o<O_ID>,例如 w1_d10_o1234567,保证同一仓库+区域连续存储,视图范围查询可直接用 startkey="w1_d10_"&endkey="w1_d10_\uffff"

  2. 单文档 JSON 模板(已脱敏)

{
  "_id": "w1_d10_o1234567",
  "type": "order",
  "customer": {
    "c_id": 259,
    "c_last": "张*",
    "c_credit": "BC",
    "c_discount": 0.15,
    "c_since": "2023-06-01"
  },
  "district": {
    "d_tax": 0.0875,
    "d_next_o_id": 1234568
  },
  "order": {
    "o_entry_d": "2023-06-15T14:23:45+08:00",
    "o_carrier_id": null,
    "o_ol_cnt": 5,
    "o_all_local": 1,
    "o_status": "N"
  },
  "lines": [
    {
      "ol_number": 1,
      "ol_i_id": 12345,
      "ol_supply_w_id": 1,
      "ol_delivery_d": null,
      "ol_quantity": 10,
      "ol_amount": 9999.99,
      "ol_dist_info": "DIST01"
    }
  ],
  "audit": {
    "created_at": "2023-06-15T14:23:45+08:00",
    "created_node": "node-42-sh",
    "last_modified": "2023-06-15T14:23:45+08:00",
    "last_node": "node-42-sh"
  }
}

要点:

  • 所有外键字段全部冗余,避免跨文档 join;
  • o_status"N" 代表 new_order 存在,"C" 代表已发货,逻辑删除
  • audit 块给每条冗余字段加时间戳+节点号,方便离线合并时做业务时间戳比对
  • 若单行明细超过 15 条,把 lines 数组 gzip 后存成attachmentcontent_type=application/gzjson,并在文档里留 lines_att_name 字段,视图只索引前 5 行摘要,降低索引膨胀。
  1. Validation Function(存 design doc)
function(newDoc, oldDoc, userCtx){
  if(newDoc.type==='order'){
    if(!newDoc._id.match(/^w\d+_d\d+_o\d+$/)){
      throw({forbidden:'订单 _id 必须满足 w<W>_d<D>_o<O> 格式'});
    }
    if(newDoc.lines.length !== newDoc.order.o_ol_cnt){
      throw({forbidden:'lines 长度必须与 o_ol_cnt 一致'});
    }
  }
}

强制模型自校验,防止离线端乱改结构导致同步回写失败。

  1. 视图(map.js)
function(doc){
  if(doc.type==='order'){
    var keys = doc._id.split('_');
    emit([parseInt(keys[0].substr(1)), parseInt(keys[1].substr(1)), parseInt(keys[2].substr(1))], {
      o_entry_d: doc.order.o_entry_d,
      o_status: doc.order.o_status,
      c_last: doc.customer.c_last,
      amount: doc.lines.reduce(function(s,l){return s+l.ol_amount;},0)
    });
  }
}

复合键 [W_ID,D_ID,O_ID] 支持按仓号+区号范围查订单,并直接聚合订单金额,避免二次拉取。

  1. 并发与回滚
    CouchDB 无多文档事务,余额扣减放在仓库级单文档(库存快照)里,用_rev 冲突机制实现乐观锁;若冲突,业务层重试三次后转人工台,符合国内银行核心账务补偿规范

拓展思考

  1. 如果后续要支持分库分仓横向扩容,可把 _id 前缀改为一致性哈希区间号,如 w<hash(W_ID%64)>_d<D_ID>_o<O_ID>,视图查询时客户端并行扫 64 个区间,再归并结果,兼容 CouchDB 2.x 集群分片
  2. 面对618 大促峰值,CouchDB 的预写日志+异步索引可能拖慢写入,可临时关闭视图索引delay_view_update=true),凌晨低峰期批量重建,这是国内电商错峰索引的常用手段。
  3. 若监管要求订单不可篡改,可把每次状态变更做成子文档附件,主文档仅保留最新状态+附件清单,利用 CouchDB _attachments 的 MD5 摘要实现WORM(一次写多次读),满足证券行业留痕需求。