如何基于 TPC-C 模型把“订单”映射为 CouchDB 文档?
解读
面试官想知道两件事:
- 你是否真正理解 TPC-C 订单的五张关系表(oorder、order_line、new_order、customer、district)以及它们之间的主外键约束;
- 你是否能把这套强范式、行级、事务型模型,无损地反范式化成一份 CouchDB JSON 文档,同时兼顾 CouchDB 的MVCC、离线同步、视图索引三大机制。
在国内金融、运营商、零售中台面试里,这道题常作为“分布式事务退化”+“文档建模”双重考点,答得太浅会被追问“余额超卖怎么办”,答得太深又容易把 CouchDB 当成 MongoDB 讲,必须紧扣 CouchDB 的 _id、_rev、attachments、validation function 来展开。
知识点
- TPC-C 订单主键:
(W_ID, D_ID, O_ID)全局唯一,O_ID 在每个 (W_ID,D_ID) 内自增;CouchDB 的 _id 是整个集群唯一,因此需要把三元组扁平化成字符串并保证字节序可排序。 - CouchDB 没有外键,也没有行级锁,所有关联必须内嵌或冗余;但冗余后又要防止同步冲突,需要给每个冗余字段加业务时间戳+源节点标识,方便做last-write-wins之外的业务合并。
- 订单状态流:new_order 行存在即“待发货”,删除即“已发货”;在 CouchDB 里不能真删,用状态字段+TTL 视图实现“逻辑删除”,否则触发MVCC 墓碑同步风暴。
- 订单行明细高达 15 条,CouchDB 单文档最大 4 MB,必须内嵌;若未来可能膨胀到百行,需要拆成主文档+行明细附件,用 attachment 存储 gzip 后的 JSON 数组,减少带宽与索引开销。
- 国内等保 3 级要求敏感字段脱敏,CouchDB 提供field-level AES 加密的第三方插件,面试时主动提及可加分。
- 视图索引:国内大厂常用couchdb-lucene或elasticsearch-couchdb-river做二级索引,但面试官更想听你如何用原生 JavaScript map 函数把
(W_ID,D_ID,O_ID)拆出来做复合键,实现按仓号+区号范围查订单。
答案
-
_id 设计
采用定长字节序字符串:w<W_ID>_d<D_ID>_o<O_ID>,例如w1_d10_o1234567,保证同一仓库+区域连续存储,视图范围查询可直接用startkey="w1_d10_"&endkey="w1_d10_\uffff"。 -
单文档 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 后存成attachment,content_type=application/gzjson,并在文档里留lines_att_name字段,视图只索引前 5 行摘要,降低索引膨胀。
- 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 一致'});
}
}
}
强制模型自校验,防止离线端乱改结构导致同步回写失败。
- 视图(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] 支持按仓号+区号范围查订单,并直接聚合订单金额,避免二次拉取。
- 并发与回滚
CouchDB 无多文档事务,余额扣减放在仓库级单文档(库存快照)里,用_rev 冲突机制实现乐观锁;若冲突,业务层重试三次后转人工台,符合国内银行核心账务补偿规范。
拓展思考
- 如果后续要支持分库分仓横向扩容,可把 _id 前缀改为一致性哈希区间号,如
w<hash(W_ID%64)>_d<D_ID>_o<O_ID>,视图查询时客户端并行扫 64 个区间,再归并结果,兼容 CouchDB 2.x 集群分片。 - 面对618 大促峰值,CouchDB 的预写日志+异步索引可能拖慢写入,可临时关闭视图索引(
delay_view_update=true),凌晨低峰期批量重建,这是国内电商错峰索引的常用手段。 - 若监管要求订单不可篡改,可把每次状态变更做成子文档附件,主文档仅保留最新状态+附件清单,利用 CouchDB _attachments 的 MD5 摘要实现WORM(一次写多次读),满足证券行业留痕需求。