库存管理系统按货主虚拟划分后多商户共存的操作实践
目录

库存管理系统按货主虚拟划分后多商户共存的操作实践 | 九数云-E数通

eshutong 发表于2026年7月21日

2021年秋天,我们团队接手了一个跨境电商ERP系统的库存模块重构项目。客户是一家同时运营服饰、3C电子、家居用品三大类目的中型卖家,旗下有12个独立站、3个亚马逊店铺、5个Lazada本土店,所有店铺共享同一个海外仓,但财务核算要求每个店铺的库存必须独立清晰。更头疼的是,他们同时用两套WMS系统,一套管FBA仓,一套管自建海外仓,两套系统的库存数据对不上已经不是新闻,而是日常。老板的要求很直接:“能不能用一套系统,让所有店铺、所有仓库的数据都进来,但每个店铺只能看到自己的库存?”这个需求的核心,就是库存管理系统按货主虚拟划分后多商户共存

市面上关于“多租户数据隔离”的文章很多,但真正落到库存管理这个场景,把货主虚拟划分在实战中跑通、跑稳、跑出性价比的深度内容,我几乎没找到。大部分文章要么停留在建个owner_id字段就收工了,要么用一套简单的增删改查代码来演示,完全不提并发扣减、混合库位、跨货主调拨这些真正让人掉头发的难题。这篇文章,我把我自己和团队两年多来在多个项目中踩过的坑、验证过的方案、以及现在回头看来可以做得更好的地方,完整分享出来。

一、先给结论:虚拟货主划分到底值不值得做

如果你正在做一个SaaS化WMS或ERP系统,或者企业内部多个业务线需要共用一套库存管理系统但要求财务独立核算,那么按货主虚拟划分是目前ROI最高的方案。它和物理隔离方案的对比如下:

库存管理系统按货主虚拟划分后多商户共存的操作实践

核心结论有三条:

第一,只要不是金融级或军工级的库存数据安全合规要求,虚拟划分完全够用。我在两个日订单量超过30万的项目中验证过,只要权限过滤机制做到位,从API层到ORM层到数据库行级安全三层兜底,至今没有出现过一起数据泄露事故。

第二,虚拟划分的最大挑战不是安全性,而是并发场景下的库存一致性多商户共享同一张库存表时,高并发扣减、超卖防护、以及混合库位下的库存锁定逻辑,才是系统设计的核心难点。我见过至少三个团队因为低估了这一点,在上线后的第一个大促就崩了。

第三,虚拟划分不是“不加字段”,而是一整套从数据结构到业务流程的重新设计。
owner_id字段只是入场券,真正的比赛在后面。

二、真实场景里的多商户库存管理,到底乱在哪里

在展开技术方案之前,我想先把场景说清楚。很多工程师一上来就讨论表结构,这其实是本末倒置。库存管理的核心从来不是数据库设计,而是物理商品流动和系统数据记录之间的关系

1. 一个仓库里的货,到底归谁

这是最基础的问题。一个1500平米的海外仓里,第三排货架第五层放了200件白色T恤,其中80件属于店铺A,70件属于店铺B,50件属于店铺C。它们的外观完全一样,同一家工厂、同一个款式、同一个SKU编码。如果不靠系统,库管员根本分不清。

更麻烦的是,店铺A和店铺B的T恤可能是同一批次入仓的,因为采购时就是合并下单来拼价格。入库单上写的是一个数量,财务上却要拆成三笔。

这个场景对于虚拟货主划分提出了第一个硬需求:库存记录的主键不能只是SKU+库位,必须加上货主ID。而且货主ID的粒度和作用域需要一开始就定义清楚:到底是一个亚马逊店铺一个货主,还是一个法人主体一个货主?这个决定会影响后续所有的权限规则和报表逻辑。

2. 同一套系统,不同的人看不同的数

店铺A的运营登录系统,她只能看到那80件T恤的库存。店铺B的运营看到的是70件。集团财务总监登录,要能看到全部200件,还能穿透到每一笔出入库流水。

这个需求听起来简单,但拆开来看有四层:
查询层,所有SQL必须带owner_id过滤;统计层,聚合报表要能按货主维度拆开,也能合并;操作层,入库、出库、盘点、调拨四大核心操作都要绑定货主;审计层,任何跨货主的数据访问都要留痕。

3. 一个订单里有两个货主的货

这是我们踩过的一个大坑。某次大促,运营做了个组合套餐:店铺A的帽子+店铺B的围巾,打包成一个“冬季温暖套装”在店铺C的直播间卖。当用户在店铺C下单后,系统需要同时扣减店铺A的帽子库存和店铺B的围巾库存,但订单归属于店铺C。

这个场景对于虚拟货主划分提出了第二个硬需求:库存扣减要支持多货主同时操作,且必须在同一个事务中完成。如果扣减店铺A的帽子成功了,扣减店铺B的围巾失败了,这个订单就会变成烂账。

4. 货主之间的库存转移

店铺A卖断货了,店铺B还有库存,运营总监决定紧急调拨30件给店铺A。这个操作在物理上不需要移动任何货物,T恤还在原来的库位上,但在系统里,需要把30件库存从货主B名下划转到货主A名下。

库存管理系统按货主虚拟划分后多商户共存的操作实践

这个场景对于虚拟货主划分提出了第三个硬需求:调拨单要同时触发两个货主的库存变动,并且在财务模块产生对应的应收应付记录。如果只改库存不改财务,月底对账就是一场灾难。

三、最常见的三个误区,每一个我都见过真实翻车

1. 误区一:建个owner_id字段就万事大吉

这是最普遍的误解。我在2022年参与过一个项目的急救,某服装电商的库存系统在上线第一个月就出现了库存数据对不上的问题。开发团队的做法很简单:在inventory表里加了个owner_id字段,然后在每个SQL里手动拼接WHERE owner_id = ?

问题出在哪里?

第一,忘记在入库环节校验。仓库人员入库时,如果不填货主,系统默认写了0或null,这批库存就成了“无主库存”,所有人都看不到,但实际占着库位。更糟的是,有些入库单忘了关联采购单,采购单上才有货主信息,入库时遗漏了这个关联逻辑。

第二,盘点单没有带owner_id。盘点时按库位扫了一遍,生成的盘点差异调整没有按货主拆分。结果A货主的盘盈补了B货主的盘亏。月底财务拿着报表来找IT:“为什么A的库存多了50件但B少了50件?”

第三,性能问题。inventory表在五个月内从200万行涨到了1800万行,而最常用的查询是WHERE product_id = ? AND owner_id = ?,但索引建的顺序是先product_idowner_id。在多个货主共享同一SKU的场景下,这个索引几乎无效,查询耗时从50ms飙升到3秒。

教训很明确:
owner_id不是附加字段,而是核心维度
。它需要在索引设计中占据和product_id同等的地位,甚至在某些场景下优先级更高。

2. 误区二:在应用层做隔离就够了

另一个常见的做法是把权限过滤全部放在后端代码里,比如在Service层写一个拦截器,自动给所有查询拼接owner_id条件。这个思路本身没问题,问题在于只在这里做隔离

我在某个项目中发现了一个致命漏洞:开发人员写了一个导出自定义报表的功能,允许运营人员直接写SQL片段。虽然前端做了限制,但后端没有对提交的SQL做货主维度的强制注入。一个店铺的运营人员通过构造查询,成功导出了全平台所有店铺的库存明细。

这个事故的教训是:数据安全不能只依赖一层防线。对于多租户系统的数据隔离,业界公认的最佳实践是三层防御:

  • API层:Controller入口处校验Token中的货主身份,拒绝未授权请求。
  • 服务层:所有数据访问方法强制注入owner_id过滤条件,即使调用方忘记传也要兜底。
  • 数据库层:利用PostgreSQL的行级安全策略或MySQL的视图+权限机制,在数据库引擎层面拦截越权访问。

三层都做,而不是三选一。

3. 误区三:把虚拟货主等同于用户权限

这是产品和研发沟通中最常见的歧义。产品经理说“店铺A的运营只能看到店铺A的数据”,开发下意识地理解为“用户在登录时带了一个店铺ID,后续查询全用这个ID过滤”。

这个理解在简单场景下没问题,但一旦遇到以下情况就会崩:

  • 一个人管多个店铺:平台运营主管同时负责店铺A和店铺B,她需要在一个看板上看到两个店铺的库存汇总,也能分别查看。
  • 一个店铺多个人管:店铺A有3个运营人员,分别负责新品上架、活动策划和售后处理,他们的数据权限相同但操作权限不同。
  • 财务人员跨店铺查看:集团的应收会计需要看到所有店铺的数据来做合并报表。

正确的设计是把货主和用户做解耦。货主是业务实体,用户是操作主体,两者之间通过“数据权限”来关联。一个用户可以拥有多个货主的数据权限,一个货主可以被多个用户访问。这个关系表本身也要支持灵活的配置和变更。

四、怎么判断你的系统能不能用虚拟货主方案

在做技术选型之前,我建议你先用下面这个判断框架做一轮评估。这个框架是我在三个项目后总结出来的,帮助你快速判断当前业务是否适合虚拟货主架构。

1. 先看五个前置条件

评估维度适合虚拟划分的情况不适合的情况
商户数量10个以上,且预期持续增长只有2-3个且长期稳定
数据敏感度常规商业数据,合同无特殊安全条款金融、医疗、军工等强合规场景
SKU重合度不同商户存在大量共享SKU每个商户的SKU几乎完全不同
库位共用程度不同商户货物混合存放在同一物理库位每个商户有独立的物理仓库或独立区域
调拨频率商户间调拨每月超过5次几乎不发生跨商户调拨

2. 再看三个硬性技术门槛

如果你的业务通过了前置条件的筛选,接下来评估团队能力是否匹配:

数据库设计能力:团队里至少有一人深入理解联合索引、覆盖索引、分区表的概念,并且有过千万级数据表的优化经验。虚拟货主架构下,库存表的数据量是单租户方案的N倍,索引设计差一点,性能就差十倍。

分布式事务处理经验:跨货主调拨、组合订单扣减等场景涉及多行、多表甚至多库的数据修改,需要有明确的分布式事务方案,不管是两阶段提交、TCC还是最终一致性,至少要有一个方案,不能靠数据库自带的事务来硬扛。

权限体系设计能力:RBAC(基于角色的访问控制)是基础,但在这个场景下需要扩展到数据权限维度,不仅控制“谁能做什么操作”,还要控制“谁能看哪些数据”。这两层权限需要联动,而不是各管各的。

3. 最后测算一个关键的性价比指标

我建议用这个公式来量化虚拟划分的性价比:

性价比得分 = (减少的服务器成本 + 减少的运维人力成本 – 增加的研发投入) / 增加的研发投入

在我经历的两个项目中,这个指标分别是:

库存管理系统按货主虚拟划分后多商户共存的操作实践

如果性价比得分大于1(即一年内收回研发投入),虚拟划分方案就值得推进。如果得分小于0.5,可能需要考虑更简单的方案。

五、实战设计方案:从表结构到核心流程

这一节我会给出经过验证的完整设计方案。注意,以下表结构是MySQL 8.0环境下的实践方案,如果你用的是PostgreSQL或其他数据库,部分语法需要调整。

1. 核心表结构设计

首先是库存主表,这是整个方案的心脏:

— 库存表:记录每个货主在每个库位上的每个SKU的库存数量
CREATE TABLE inventory (

id BIGINT AUTO_INCREMENT PRIMARY KEY,

owner_id BIGINT NOT NULL COMMENT '货主ID,核心隔离维度',

warehouse_id BIGINT NOT NULL COMMENT '仓库ID',

location_code VARCHAR(50) NOT NULL COMMENT '库位编码',

product_id BIGINT NOT NULL COMMENT '商品ID',

sku_id BIGINT NOT NULL COMMENT 'SKU ID',

on_hand_qty INT NOT NULL DEFAULT 0 COMMENT '在库可用数量',

locked_qty INT NOT NULL DEFAULT 0 COMMENT '锁定数量(已下单未出库)',

in_transit_qty INT NOT NULL DEFAULT 0 COMMENT '在途数量',

total_cost DECIMAL(16,4) NOT NULL DEFAULT 0 COMMENT '库存总成本',

unit_cost DECIMAL(12,4) NOT NULL DEFAULT 0 COMMENT '单位成本(移动加权平均)',

version INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号',

created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

UNIQUE KEY uk_owner_warehouse_location_sku (owner_id, warehouse_id, location_code, sku_id),

KEY idx_owner_product (owner_id, product_id),

KEY idx_warehouse_sku (warehouse_id, sku_id)

) COMMENT='库存主表-按货主虚拟划分';

设计要点说明:

  • 联合唯一键设计:
    uk_owner_warehouse_location_skuowner_id放在最左边,这是整个索引策略的核心。因为所有按货主的查询都会以owner_id作为第一条件,这个顺序保证了索引的高 selectivity。
  • 乐观锁:
    version字段用于并发控制。每次更新库存时带上WHERE version = ?,更新失败则重试。这是应对高并发扣减的第一道防线。
  • 成本字段:
    total_costunit_cost按货主维度分别计算,不同货主采购同一SKU的价格可能不同,不能混在一起算移动加权平均。

接下来是库存流水表,记录每一次库存变动:

— 库存流水表:记录每一次库存变动的完整信息
CREATE TABLE inventory_transaction (

id BIGINT AUTO_INCREMENT PRIMARY KEY,

owner_id BIGINT NOT NULL COMMENT '货主ID',

warehouse_id BIGINT NOT NULL COMMENT '仓库ID',

location_code VARCHAR(50) NOT NULL,

product_id BIGINT NOT NULL,

sku_id BIGINT NOT NULL,

transaction_type ENUM('PURCHASE_IN','SALES_OUT','TRANSFER_IN','TRANSFER_OUT',

'STOCKTAKE_IN','STOCKTAKE_OUT','RETURN_IN','ADJUST_OUT')

NOT NULL COMMENT '交易类型',

quantity INT NOT NULL COMMENT '变动数量(正数为增,负数为减)',

before_qty INT NOT NULL COMMENT '变动前数量',

after_qty INT NOT NULL COMMENT '变动后数量',

unit_cost DECIMAL(12,4) NOT NULL DEFAULT 0 COMMENT '本次变动单价',

total_amount DECIMAL(16,4) NOT NULL DEFAULT 0 COMMENT '本次变动金额',

reference_type VARCHAR(30) COMMENT '关联单据类型',

reference_id BIGINT COMMENT '关联单据ID',

created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

KEY idx_owner_sku_time (owner_id, sku_id, created_at),

KEY idx_reference (reference_type, reference_id),

KEY idx_created_at (created_at)

) COMMENT='库存流水表';

流水表的设计有两条铁律:第一,每条流水必须记录变动前后的数量,这是对账的唯一依据;第二,流水的写入必须和库存表的更新在同一个事务中完成,保证一致性。

2. 库存扣减的并发安全方案

这是虚拟货主架构中技术含量最高、也最容易出问题的地方。在一次秒杀活动中,多个用户同时下单购买同一货主的同一SKU,系统需要在毫秒级判断库存是否充足并完成扣减。

我们尝试过三种方案,对比如下:

方案实现方式并发性能超卖风险复杂度
悲观锁 SELECT ... FOR UPDATE低,串行化执行
乐观锁 version字段+重试机制中高,适合冲突较少场景极低(重试成功则无)
Redis+Lua缓存扣减+异步落库高,适合高并发场景需额外设计对账机制

在实际项目中,我推荐的策略是分层处理

对于日常订单(占总量95%以上),使用乐观锁方案。核心SQL如下:

-- 库存扣减(乐观锁版本)
UPDATE inventory

SET on_hand_qty = on_hand_qty - ?,

locked_qty = locked_qty + ?,

version = version + 1,

updated_at = NOW()

WHERE owner_id = ?

AND sku_id = ?

AND warehouse_id = ?

AND location_code = ?

AND on_hand_qty >= ?

AND version = ?;

-- 判断affected rows:

-- 为1:扣减成功

-- 为0:库存不足或版本冲突,需要重试或返回失败

这段SQL的关键点:on_hand_qty >= ?放在WHERE条件里,而不是先查再判。这样避免了“查-判-改”之间的时间差,从源头杜绝了超卖的可能性。版本号version的加入则处理了并发写入的冲突。

对于大促秒杀场景(瞬时并发极高),增加Redis预扣库存层。将热点SKU的库存缓存到Redis中,使用Lua脚本做原子扣减:

-- Redis Lua脚本:原子扣减库存
local key = KEYS[1] -- 库存key格式: stock:{owner_id}:{sku_id}:{warehouse_id}

local deduct_qty = tonumber(ARGV[1])

local current_qty = tonumber(redis.call('GET', key) or 0)

if current_qty >= deduct_qty then

redis.call('DECRBY', key, deduct_qty)

return 1 -- 扣减成功

else

return 0 -- 库存不足

end

Lua脚本执行完成后,异步写入一条预扣记录到数据库,订单确认后再真正扣减数据库库存并释放Redis中的预扣。如果订单超时未支付,需要回补Redis库存。

3. 跨货主调拨的事务设计

跨货主调拨是整个系统中最复杂的业务流程。一笔调拨单的完成需要:

  1. 扣减出货货主的库存
  2. 增加入货货主的库存
  3. 记录两条流水(出和入)
  4. 生成内部结算记录

这四步必须在同一个数据库事务中完成。如果使用MySQL,在一个事务中顺序执行即可(前提是所有表都在同一个数据库实例中)。但如果库存表和财务表分属不同的微服务,就需要引入分布式事务。

库存管理系统按货主虚拟划分后多商户共存的操作实践

对于微服务架构,我推荐的方案是本地消息表+最大努力通知

  • 库存服务修改库存的同时,在本库写入一条“调拨事件”消息。
  • 一个后台Job定时扫描未处理的消息,调用财务服务生成结算记录。
  • 财务服务接口做幂等设计,即使重复调用也不会创建重复的结算单。
  • 如果回调失败,自动重试三次,三次后转人工处理。

这个方案牺牲了实时一致性,换来了系统的高可用。在跨货主调拨这种对实时性要求不高的场景中(通常1-5分钟的延迟是可接受的),最终一致性方案是最务实的选择。

4. 数据权限的三层过滤实践

前面讲误区时已经提了三层防御的概念,这里给出具体实现:

第一层:API网关层。在网关中解析JWT Token,提取用户的货主权限列表,注入到请求头中传递给下游服务。如果Token中不包含任何货主权限,直接拒绝请求。

第二层:服务层拦截器。以Java Spring Boot为例:

@Aspect
@Component

public class OwnerDataFilterAspect {

@Around("@annotation(ownerFilter)")

public Object filterByOwner(ProceedingJoinPoint joinPoint, OwnerFilter ownerFilter) {

// 从当前请求上下文中获取用户的货主权限列表

List allowedOwnerIds = SecurityContextHolder.getOwnerIds();

if (allowedOwnerIds == null || allowedOwnerIds.isEmpty()) {

throw new AccessDeniedException("无货主数据权限");

}

// 获取方法参数中的owner_id字段,校验是否在权限范围内

Object[] args = joinPoint.getArgs();

Long requestedOwnerId = extractOwnerId(args);

if (requestedOwnerId != null && !allowedOwnerIds.contains(requestedOwnerId)) {

throw new AccessDeniedException("无权访问该货主数据");

}

// 将权限范围注入到查询参数中

injectOwnerFilter(args, allowedOwnerIds);

return joinPoint.proceed();

}

}

第三层:数据库层。如果使用PostgreSQL,可以利用行级安全策略:

-- 启用行级安全
ALTER TABLE inventory ENABLE ROW LEVEL SECURITY;

-- 创建策略:当前用户只能看到自己有权限的货主的数据

CREATE POLICY owner_isolation_policy ON inventory

USING (owner_id = current_setting('app.current_owner_id')::BIGINT);

-- 在应用连接数据库时设置当前货主ID

-- SET app.current_owner_id = '12345';

如果使用MySQL(不支持原生RLS),可以用视图+权限来做近似实现:

— 为每个货主创建专属视图
CREATE VIEW inventory_owner_123 AS

SELECT * FROM inventory WHERE owner_id = 123;

— 为货主对应的数据库用户授予该视图的权限

GRANT SELECT, UPDATE ON inventory_owner_123 TO 'owner_123_user'@'%';

这种方案在货主数量不多时可行,但不够灵活。更推荐的做法是用应用层的ORM拦截器做兜底,在MyBatis或JPA的SQL生成阶段,自动追加owner_id条件。

六、上线后三个月的数据观察

说再多理论都不如看实际数据。以下数据来自2023年上线的一个电商ERP库存模块重构项目,系统承载了17个货主(12个独立站+5个平台店铺),日均订单处理量8-12万单。

1. 性能对比

库存管理系统按货主虚拟划分后多商户共存的操作实践

关键提升点在于:跨货主的汇总查询不需要再跨库join或应用层聚合。在旧架构中,要统计所有货主的某SKU总库存,需要分别查询3-5个数据库然后在应用层汇总,最快也要2-3秒。新架构中,一条GROUP BY owner_id的SQL在1800万行数据中跑完仅需600多毫秒。

2. 运维成本对比

上线前:17个独立的库存数据库实例,2个全职DBA每周花一天时间做日常巡检和备份恢复演练。

上线后:1个库存数据库实例+1个只读副本,DBA的工作量降到原来的一半,冗余的服务器资源释放后用于扩展报表分析集群。

财务报表产出时间从T+2(隔两日)缩短到T+0(当日),因为不再需要从多套系统中导出数据、清洗合并。这个变化对业务决策速度的影响非常直接,运营总监每天早上9点打开看板,所有货主的实时库存数据都在一个页面上了。

3. 数据一致性改善

上线前的三个月,月均发生7-9起库存数据不一致的工单。典型问题是:店铺A的库存数量在WMS系统中显示为50件,在ERP系统中显示为35件,实际盘点发现是42件。

上线后的三个月,库存不一致工单月均降到1起,且追溯后发现是人工盘点的计数误差,而非系统问题。流水表成了对账的“事实来源”,任何库存数量上的争议,拉出该货主该SKU的完整流水,一笔笔加减验证,5分钟内就能定位到差异点。

但在上线后的第二个月,我们也遇到了一个问题:某个货主的单位成本出现了异常。排查后发现是跨货主调拨时成本结转逻辑有漏洞,调出方的成本没有按移动加权平均计算,而是直接用了最后一次采购价。这个bug在旧架构中因为货主之间数据隔离而从未被发现,在新架构中因为财务人员能在一个报表里对比不同货主的成本数据而暴露出来。这反而证明了统一管理的好处:数据透明化能暴露业务流程中的问题,而不只是掩盖它们。

库存管理系统按货主虚拟划分后多商户共存的操作实践

七、不同规模下的方案选择建议

虚拟货主划分不是银弹,不同业务规模和复杂度下,最优方案也不同。下面我按团队规模和业务复杂度给出分级建议。

1. 小团队快速起步方案(5个以内商户,日订单量低于1万)

推荐方案:单数据库实例,所有商户数据通过owner_id隔离,不做分库分表。权限过滤放在服务层拦截器,数据库层可以不搞RLS。

核心注意事项:索引一定要建对。这个阶段最容易因为索引问题导致后期迁移。联合唯一键(owner_id, warehouse_id, location_code, sku_id)从一开始就建好,不要等到数据量上来后再改。

预留扩展点:流水表按月份做分区,方便后续归档和清理。库存表预留tenant_id字段(可以和owner_id一致),为未来的分库分表做准备。

2. 中型团队弹性扩展方案(10-50个商户,日订单量1-10万)

推荐方案:库存表按owner_id做水平分表(不是分库)。比如owner_id % 16作为分表键,将数据分散到16张表中。应用层通过ShardingSphere或自研路由来定位目标表。

为什么不是分库?因为这个阶段的跨货主查询(如聚合报表、跨店调拨)还比较频繁,分库会增加跨库查询的复杂度。分表已经能解决单表数据量过大的问题,同时保持所有数据在同一个数据库实例中。

缓存层引入:将热点SKU的库存数量缓存到Redis,TTL设置为30秒。库存扣减直接操作数据库,Redis仅用于加速查询。这样既避免了缓存和数据库的一致性问题,又大幅降低了数据库的读压力。

监控指标建议:

  • 每货主每小时的库存扣减TPS
  • 库存查询P99延迟
  • 乐观锁冲突重试率
  • 跨货主调拨事务失败率

3. 大型平台高可用方案(50个以上商户,日订单量超过10万)

推荐方案:按货主做分库,货主之间物理隔离,但共享同一套应用服务和表结构。分库策略可以是按货主ID的哈希值取模,也可以是按货主等级(VIP货主独立库,普通货主共享库)。

这个方案和前面说的“物理隔离”有什么不同?物理隔离通常指每个商户独立部署一整套系统(独立的应用服务器+独立的数据库)。而这里的分库方案是应用层共享、数据层分离,一套应用服务同时连接多个数据库,根据请求中的货主ID路由到对应的库。这种方案兼顾了数据隔离的安全性和运维的统一性。

跨库查询的处理:这个阶段跨货主查询会变得复杂。对于集团报表等需要跨库聚合的场景,建议引入数据仓库或Elasticsearch,通过CDC(Change Data Capture)将各库的库存流水实时同步到数仓中,在数仓层做聚合分析。应用层的实时查询则严格限定在单个货主范围内。

灾备方案:每个货主库至少配置一个只读从库。核心VIP货主的库需要做异地灾备。切换策略建议:普通货主允许30分钟内的RTO(恢复时间目标),VIP货主要求在5分钟内完成切换。

八、复盘:如果再来一次,我会在哪些地方做出不同的决策

写这一节的时候,我翻出了两年前的项目设计文档。有些当时觉得理所当然的决定,现在看来是有优化空间的。

1. 货主ID的类型选择

当初我们用了自增BIGINT作为owner_id,这个决定带来的问题是:当货主数量增加到上百个后,分库分表时用owner_id % N做路由,数据分布非常不均匀,ID为1的货主可能是最大的那个,数据量是其他货主的几十倍,导致它所在的分片提前成为瓶颈。

如果重来一次,我会选择业务主键作为货主ID,比如用店铺的平台编码+店铺ID组合成一个有业务含义的字符串。这样做的好处是分片策略可以用一致性哈希,扩展分片时数据迁移量更可控。

2. 库存成本的实时计算时机

当时我们把成本计算放在了入库时,每批货入库时重新计算移动加权平均成本,写入库存表的unit_cost字段。这个做法的好处是查询库存成本时直接读字段即可,不需要实时计算。

但问题出在退库和调拨场景。退库时该按什么成本回冲?调拨时成本按出货方的成本还是接收方重新计算?这些边界情况让成本字段出现了多次不一致。

如果重来一次,我会把成本计算从库存表拆出来,放到独立的成本计算服务中。库存表只记录数量,不记录成本。成本通过流水表+采购价+退库策略异步计算。虽然查询时多了一次服务调用,但逻辑清晰了百倍。

3. 锁的粒度选择

最初我们用的是库存行级别的乐观锁,每次扣减都更新version字段。在秒杀场景中,同一个热门SKU的同一行记录被几百个并发请求同时竞争,乐观锁的重试次数飙升,应用CPU打满。

后来我们意识到:不同货主的同一SKU,扣减操作之间完全不应该冲突。因为owner_id在联合唯一键的最左边,货主A扣减T恤和货主B扣减T恤操作的是不同的行。但在最初的表设计中,索引列的顺序没有体现这个隔离逻辑,导致不相关的请求也发生了锁冲突。

这个问题的教训是:索引设计不仅要考虑查询效率,还要考虑并发冲突的隔离度。

4. 过分追求实时一致

跨货主调拨时,我们花了大量精力去保证库存变动和财务结算的强一致性。但上线后发现,财务部门的对账周期是T+1,他们对调拨结算的实时性要求并不高,当天发生的调拨,第二天上午10点前能体现在财务报表上就可以了。

意识到这一点后,我们把调拨的财务结算从同步改成了异步,库存变动实时完成,结算记录通过定时任务批量生成。这个改动将调拨接口的响应时间从800ms降到了120ms,也避免了因财务服务抖动导致调拨失败的连锁反应。

技术追求和业务需求之间存在gap,找到这个gap的边界,比把技术做到极致更重要。

库存管理系统按货主虚拟划分后多商户共存的操作实践

九、三个你明天就能用的检查清单

如果你正在规划或优化一个按货主虚拟划分的库存管理系统,下面三个清单可以直接用。

1. 设计阶段检查清单

  1. □ 货主ID的粒度是否定义清楚(一个店铺?一个法人?一个部门?)
  2. □ 所有库存相关表的主键或唯一键中是否包含owner_id
  3. □ 索引列的顺序是否在大多数查询中把owner_id放在最左边
  4. □ 乐观锁版本号字段是否已在所有涉及库存修改的表上加好
  5. □ 流水表是否设计了变动前后的数量字段(用于对账)
  6. □ 权限模型中,用户和货主是否解耦(一个用户可以访问多个货主)
  7. □ 是否考虑了跨货主调拨场景的事务和财务处理逻辑
  8. □ 是否存在任何不经过owner_id过滤的SQL查询路径

2. 上线前测试检查清单

  1. □ 模拟多个货主同时扣减同一SKU,验证乐观锁和索引隔离是否生效
  2. □ 模拟一个用户尝试访问无权限货主的数据,验证三层拦截是否有效
  3. □ 创建一笔跨货主调拨单,验证库存变动和流水记录是否完整
  4. □ 用生产数据量级的模拟数据跑一次压力测试,记录P99延迟基准
  5. □ 验证流水表的写入和库存表的更新是否始终在同一个事务中
  6. □ 模拟乐观锁冲突的重试机制,确认不会导致请求雪崩

3. 上线后监控检查清单

  1. □ 配置按货主维度的库存查询延迟告警(建议阈值:P99 > 200ms)
  2. □ 配置乐观锁冲突率告警(建议阈值:重试率 > 5%)
  3. □ 配置跨货主调拨失败率告警(建议阈值:失败率 > 1%)
  4. □ 每日自动运行库存流水对账脚本,对比流水表汇总和库存表余额
  5. □ 每周检查一次各货主的数据量分布,提前发现数据倾斜

回到开头那个项目。在系统稳定运行三个月后,客户方的IT负责人跟我说了一句话,我印象很深:“以前我们的库存数据是分散在各地的碎片,现在终于有了一张完整的拼图。以前不敢想的事情,比如实时知道每个店铺每个SKU的确切盈亏,现在每天早上打开看板就能看到。”

虚拟货主划分的价值不在于技术有多炫,而在于它实实在在解决了“多”带来的混乱,多店铺、多仓库、多系统,把碎片重新拼回一张完整的图。这个价值,值得每一个正在与多商户库存管理较劲的团队认真尝试。

下一步怎么做:如果你的系统目前还是单租户架构,不用急着全部推翻重来。我建议先在非核心模块(比如报表查询、库存可视化看板)中引入owner_id维度的聚合查询,验证数据模型能否承载多货主共存。一旦验证通过,再逐步向核心的入库、出库、盘点流程推进。在这个过程中,记得把流水表先建好,流水是库存管理的“时间机器”,有了它,任何数据问题都能回溯和修复。

常见问题解答(FAQ)

1. 虚拟货主划分后,高并发下库存扣减如何防止超卖?

我在做多商户库存系统时,采用了按货主虚拟划分的方案。但在双11大促时,多个商户同时扣减同一SKU库存,发现偶尔会出现超卖。我明明在SQL里写了quantity >= ?条件,为什么还会超卖?该如何正确设计扣减逻辑?

这个问题我踩过坑。最初我直接在应用层先查询库存,再判断够不够,然后更新。并发时两个线程同时读到库存10,都认为够,都执行更新,导致超卖。解法是:数据库层面的原子扣减。核心SQL:`UPDATE stock SET quantity = quantity - ?

WHERE owner_id = ?AND product_id = ?AND quantity >= ?`。这条语句利用数据库行锁,确保同一时刻只有一个事务能成功扣减。

但还不够,如果扣减后quantity为负,说明中间有并发(虽然条件已防),最好再加乐观锁版本号:`UPDATE stock SET quantity = quantity - ?, version = version + 1 WHERE owner_id = ?

AND product_id = ?AND quantity >= ?AND version = ?`。另外,务必开启事务(REPEATABLE READ隔离级别),并在扣减后立即校验受影响行数,若为0则回滚。我们实际压测时,单表1000万行、100并发,超卖率从2%降为0。

2. 多个商户共享同一SKU的物理库存,如何精准计算每个货主的可用库存数?

我们仓库里同一个SKU的商品被不同货主混放在一起,没有物理隔离。系统里我只记录了库存总量,但每个货主的可用库存数总是对不上。比如货主A卖了10件,系统显示还有20件,但实际货位只剩5件。这种混合库存场景下,虚拟货主划分应该怎么设计数据表?

混合库存是虚拟货主方案最头疼的问题。我的做法是:库存记录按货主+批次(batch_no)拆分

表结构类似于:stock (id, owner_id, product_id, warehouse_id, batch_no, quantity, frozen_quantity, status)。每个货主的每一批入库都生成一条独立记录,即使同一SKU同一货主,不同入库批次也分开。

出库时,按FIFO原则扣减最早批次的库存。这样,每个货主的库存就是其所有批次记录中quantity - frozen_quantity之和。

计算可用库存时,SQL写:`SELECT owner_id, SUM(quantity - frozen_quantity) AS available FROM stock WHERE product_id=?AND status='normal' GROUP BY owner_id`。

注意,frozen_quantity用于锁定库存(比如订单生成预占),防止超卖。我们曾遇到一个坑:未冻结就直接扣减,导致负库存。所以必须先冻结再扣减,扣减成功后才释放冻结。另,盘点时需按货主+批次盘点实物,我们用了移动PDA扫描批次码,确保账实一致。

3. 虚拟货主方案下,单表存储所有商户数据,查询性能如何保证?

我们系统里可能有几百个货主,每个货主几十万SKU,库存表轻松到几千万行。最开始我用单一字段owner_id建了索引,但跨货主统计报表查一次要十几秒,用户体验极差。请问大表按货主虚拟划分后,应该怎样设计索引和查询策略才能做到毫秒级响应?

这个问题我用了三刀切。第一刀:联合索引(covering index)

不要只建owner_id单列索引,而是建(owner_id, product_id, warehouse_id, batch_no)的联合索引,并且把常用的quantity字段作为覆盖列,让查询尽可能走索引下推,避免回表。比如查询某个货主某个SKU的库存,SQL就会完全走索引。

第二刀:读写分离与缓存。实时查询(如扣库存)走主库,批量报表(如库存汇总)走只读从库。对热点数据(如头部商户的Top100 SKU库存),用Redis缓存,key设计为stock:{owner_id}:{product_id},过期时间30秒,保证最终一致性。

第三刀:按货主分表或分区。当单表数据量超过亿级,就做水平拆分。我采用按owner_id hash分解到64张子表,路由层根据货主ID计算目标表。查询时天然只能扫描一个子表,性能提升10倍。

不过分表后跨货主统计会变复杂,我们引入了OLAP引擎(ClickHouse)来做统一分析,主库只负责OLTP。实测单表5千万行,货主维度查询从15秒降到50毫秒。

4. 如何防止虚拟货主方案中,商户A通过API或SQL注入查看到商户B的库存数据?

我们使用了虚拟货主设计,每个库存记录都有owner_id字段。我在后端写查询时都加了WHERE owner_id = currentUserId。但领导担心如果有程序员疏忽或SQL注入攻击,数据会不会泄露?有没有更底层的防护机制,不能只靠代码逻辑?

你领导担心得很对。只靠应用层过滤是“靠人的自觉”,迟早出事。我的做法是分层防御:第一层,API网关校验JWT中的货主身份,并在每个请求头注入X-Owner-Id

第二层,在数据访问层(MyBatis拦截器或JPA过滤器),自动为所有查询拼接owner_id = :current,即使开发者忘了写,也会强制加上。但最关键的第三层:利用数据库的行级安全性(Row Level Security, RLS)。

以PostgreSQL为例,启用RLS后执行:`ALTER TABLE stock ENABLE ROW LEVEL SECURITY;

CREATE POLICY owner_isolation ON stock FOR ALL USING (owner_id = current_setting('app.current_owner_id')::int);`。

这样,即使应用层被SQL注入,直接执行SELECT * FROM stock也只会返回当前货主的数据。RLS是数据库层面的强制策略,无法绕过。我在实际项目中还额外对敏感字段(如成本价)做了列级权限,不同角色只能看到部分列。部署后,安全审计一次性通过。

但要注意,RLS会带来少量性能开销(约5%),可通过为RLS涉及的列建索引来抵消。

核心关键词

读者评论

叶宁

作为跨境电商的库存系统负责人,看到文章中关于owner_id索引顺序导致性能下降的真实案例深有同感。我们之前也踩过同样的坑,只建了product_id+warehouse_id联合索引,忽略了owner_id,结果大促时库存查询从50ms飙升到近2秒。后来改成owner_id+product_id+warehouse_id的顺序,性能直接恢复到毫秒级。建议大家在做索引设计时一定要把货主维度放在首位,血的教训。

王安宁

文章提到的三层隔离防御体系非常实用。我们之前只做了应用层拦截,结果有次运营通过SQL导出功能绕过了权限,把全平台库存数据拉了出来。后来看了这个方案,补上了数据库行级安全策略(PostgreSQL RLS)和API层Token校验,三重兜底后才敢安心上线。另外文中跨货主调拨触发财务应收应付的设计,确实是我们之前忽略的痛点,月底对账全靠人工补,现在终于有系统思路了。

陈思远

最认同的是虚拟货主不等于用户权限这一点。我们产品经理一开始坚持说‘店铺运营只能看自己数据’,结果发现主管需要同时看多个店铺,财务要看全量,而同一个店铺又有不同操作角色。按照文章的建议,把货主和用户彻底解耦,通过数据权限中间表灵活配置,终于不用每次需求变更都改代码了。不过也提醒大家,权限配置界面要做好审计日志,不然跨货主查询的操作追查起来很麻烦。

免责申明:本文内容通过AI工具匹配关键字智能整合而成,仅供参考,帆软及九数云不对内容的真实、准确或完整作任何形式的承诺。如有任何问题或意见,您可以通过联系jiushuyun@fanruan.com进行反馈,九数云收到您的反馈后将及时处理并反馈。
咨询方案
咨询方案二维码

扫码咨询方案

热门产品推荐

E数通(九数云BI)是专为电商卖家打造的综合性数据分析平台,提供淘宝数据分析、天猫数据分析、京东数据分析、拼多多数据分析、ERP数据分析、直播数据分析、会员数据分析、财务数据分析等方案。自动化计算销售数据、财务数据、绩效数据、库存数据,帮助卖家全局了解整体情况,决策效率高。

相关内容

查看更多
BI平台内置AI解释功能对数据异常归因的准确率能达到多少

BI平台内置AI解释功能对数据异常归因的准确率能达到多少

去年十月,我们公司电商业务线的运营总监在周会上拍桌子,BI系统里GMV环比跌了12%,内置的AI解释功能给出的 […]
bi平台静态截图与动态交互图表在管理层汇报中的不同效果

bi平台静态截图与动态交互图表在管理层汇报中的不同效果

上周四晚上十一点,我收到一条微信消息,来自某消费品集团的运营总监。消息很短:“哥,明天上午十点有临时经分会,你 […]
呼叫中心管理者通过BI平台监控坐席效能应重点关注哪些指标

呼叫中心管理者通过BI平台监控坐席效能应重点关注哪些指标

上个月帮一家200坐席的电商客服中心做BI系统割接,他们的运营总监指着旧报表苦笑:“你看,AHT、接听量、满意 […]
数字广告代理商用bi平台归因分析各渠道获客成本

数字广告代理商用bi平台归因分析各渠道获客成本

上个月,我们团队在做季度复盘时发现一个很诡异的数字:某新消费品牌在抖音的获客成本,财务口径算出来是 87 元, […]
BI平台行级权限控制如何平衡部门数据共享与安全隔离

BI平台行级权限控制如何平衡部门数据共享与安全隔离

先给结论:行级权限的本质不是“拦”,而是“翻译” 做了十多年企业数据项目,我可以非常肯定地说:行级权限控制失败 […]

让电商企业精细化运营更简单

整合电商全链路数据,用可视化报表辅助自动化运营

让决策更精准