十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java WMS多租户架构实战:Spring Cloud+MyBatis-Plus落地指南

Java WMS多租户架构实战:Spring Cloud+MyBatis-Plus落地指南 1. 项目概述为什么一套 Java WMS 必须长出“多租户”这根骨头JeeWMS 这个名字在仓储物流系统圈子里已经不是新鲜面孔了。它是一套基于 Java 技术栈、开源可二次开发的 WMSWarehouse Management System系统核心定位很清晰——不追求大而全的 ERP 级别庞然大物而是聚焦在“货怎么进、怎么存、怎么拣、怎么出”这一条主线上把仓库作业的每一个动作抠得足够细、足够稳。但真正让它从“能用”跃升到“值得规模化商用”的关键一跃不是加了多少新功能按钮而是底层架构上悄悄完成的一次静默重构多租户架构的落地。你可能已经见过不少标榜“支持多客户”的 WMS 系统但很多只是表面功夫——比如后台手动建几个“客户档案”前端登录时选一个然后所有数据硬编码地加上一个tenant_id字段做隔离。这种做法在小作坊式运营时没问题一旦客户数超过 5 个、仓库数超过 3 个、日单量突破 5000 单问题就来了数据库表膨胀得厉害SQL 查询越来越慢一个客户的 SQL 慢查询会拖垮所有人的响应更麻烦的是某个客户想改个打印模板或者调整下波次规则开发就得提个新版本上线其他客户被迫跟着升级体验极差。这就是典型的“伪多租户”。JeeWMS 的多租户是真正在 Spring Cloud 生态里扎下根来的。它不是靠业务层拼命加判断而是把租户隔离这件事从数据库连接池、SQL 解析、服务路由、配置加载一直贯穿到前端页面渲染的每一层。我去年帮一家第三方物流服务商做系统迁移他们原来用的是一套定制 WMS23 个货主共用一套库结果每次数据库维护都得挑凌晨三点因为白天一动就卡死。换成 JeeWMS 后我们给每个货主分配独立的逻辑租户 ID数据库层面共享物理表但通过 MyBatis-Plus 的自动分表插件 Spring Cloud Gateway 的路由标签让每个请求进来时系统就知道“这是 A 货主的 B 仓库的 C 操作”连缓存 Key 都自动带上tenant:1001:warehouse:2001:前缀。上线后他们第一次实现了“A 货主在改库存预警阈值时B 货主的出库单照常秒发”。这才是多租户该有的样子——不是“能分”而是“分得悄无声息改得互不干扰”。所以如果你正面临这些场景手上有多个品牌客户要独立管理库存和作业流程公司内部有多个事业部/子公司各自有独立的 KPI 和考核体系或者你是个 SaaS 型 WMS 厂商想用一套代码服务几十家中小客户……那么理解 JeeWMS 的多租户架构就不是看热闹而是看自己系统的未来骨架怎么搭。它解决的从来不是“能不能”而是“稳不稳、快不快、改不改得动”。2. 架构设计与核心思路拆解Spring Cloud 如何成为多租户的“承重墙”JeeWMS 的多租户不是凭空画出来的它的底座是 Spring Cloud Alibaba 生态具体来说是 Nacos Sentinel Seata Gateway 这四块砖垒起来的。很多人一看到“Spring Cloud”第一反应是“微服务”但在这里它扮演的角色远不止于此——它是整套多租户能力的调度中枢、安全守门人和资源协调员。下面我就一层一层剥开它的设计逻辑。2.1 租户识别从请求头到线程上下文的“身份烙印”多租户的第一步永远是“你是谁”。JeeWMS 不依赖登录态里的 session 或 cookie这对 API 调用不友好而是强制要求所有外部请求必须携带一个X-Tenant-ID请求头。这个设计看似简单实则暗藏玄机网关层拦截Spring Cloud Gateway 在路由前就做了第一道校验。它会读取X-Tenant-ID去 Nacos 的配置中心查这个租户是否合法、是否启用、是否绑定了哪些仓库。如果查不到直接返回401 Unauthorized连业务服务的毛都碰不到。这比让每个微服务自己去查数据库快得多也更安全。线程上下文透传Gateway 验证通过后会把这个tenantId注入到ReactiveSecurityContext中并通过WebFlux的Mono.deferContextual机制把它绑定到当前请求的整个 Reactor 链路里。这意味着从网关进来到订单服务、库存服务、作业服务再到最终的数据库操作这个tenantId就像一个隐形的身份证始终跟着请求走不需要你在每个 service 方法里手动传参。为什么不用 ThreadLocal你可能会问为啥不直接用ThreadLocal存因为在 WebFlux 的非阻塞模型下ThreadLocal是不可靠的——一次请求可能在多个线程间跳转。JeeWMS 用的是reactor.util.context.Context这是 Project Reactor 官方推荐的上下文传递方式稳定且无副作用。提示实际部署时我们会在 Nginx 层就统一注入X-Tenant-ID比如根据域名a.client.jeewms.com自动映射为tenantId1001。这样前端完全不用感知租户概念对用户透明。2.2 数据隔离共享库下的“逻辑围栏”而非物理割裂JeeWMS 采用的是“共享数据库 逻辑隔离”模式而不是为每个租户建一套库Shared Database, Shared Schema。这个选择背后是成本、运维和扩展性的综合权衡。物理成本低不用为每个新客户单独采购数据库实例、单独做备份策略、单独调优参数。一套 MySQL 主从集群就能支撑上百个租户。运维负担轻DBA 只需维护一套 SQL 规范、一套慢查询监控、一套索引优化方案。如果每个租户一套库光是定期巡检就得翻好几倍时间。但逻辑隔离必须铁腕为了防止 A 租户的数据被 B 租户的 SQL 查出来JeeWMS 在 MyBatis-Plus 层做了两件事全局 SQL 注入器所有SELECT、UPDATE、DELETE语句都会被自动追加AND tenant_id #{tenantId}条件。这个tenantId就是从前面说的 Reactor Context 里取的不是业务代码传的。租户字段强校验在实体类上标注TableField(fill FieldFill.INSERT)并配合MetaObjectHandler确保插入时tenant_id字段必填且不可为空。哪怕你忘了写框架也会帮你补上。为什么不用视图或行级安全策略RLSMySQL 8.0 确实支持 RLS但 JeeWMS 要兼容 MySQL 5.7而且 RLS 的策略管理复杂调试困难。MyBatis-Plus 的自动注入开发友好、调试直观、兼容性好是更务实的选择。2.3 服务路由Gateway 如何当好“多租户交通警察”Spring Cloud Gateway 在这里不只是个反向代理它是个智能的流量调度员。JeeWMS 的路由规则不是简单的path/api/order/** - order-service而是带租户标签的动态路由spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service predicates: - Path/api/order/** filters: - name: TenantHeaderFilter args: headerName: X-Tenant-ID这个TenantHeaderFilter是个自定义 Filter它的作用是读取X-Tenant-ID然后根据 Nacos 里配置的“租户-服务实例映射表”决定把请求转发给哪个具体的order-service实例组。比如tenantId1001→ 转发到order-service-v1专为大客户定制的高性能版本tenantId1002→ 转发到order-service-v2标准版资源配额较低这样做的好处是不同租户可以跑在不同的服务版本上互不影响。A 客户要上新功能B 客户还在用老版本完全没问题。而这一切对前端来说只是换了个请求头的事。2.4 配置中心Nacos 如何实现“千人千面”的租户配置多租户最怕什么就是“改一个崩一片”。JeeWMS 把所有可配置项都扔进了 Nacos按三级命名空间管理Groupjeewms-tenant-configData IDapplication-tenant-1001.yaml租户专属配置Profileprod/test每个租户的配置项比如wms.inventory.min-stock-threshold: 5最低库存预警wms.picking.batch-size: 20波次拣货单最大行数wms.print.template-id: template-a4-2023出库单打印模板都独立存放。服务启动时会根据X-Tenant-ID动态拉取对应的Data ID。更绝的是JeeWMS 还支持“租户继承配置”如果tenant-1001没配min-stock-threshold就自动 fallback 到application-default.yaml里的全局默认值。这就避免了每个租户都要配全所有项大幅降低配置管理成本。注意Nacos 的配置监听是长轮询JeeWMS 在EventListener里监听RefreshEvent收到变更后会触发InventoryService的缓存刷新确保配置生效不重启。3. 核心细节解析与实操要点从代码到部署的“避坑指南”光知道架构图是没用的真正干活时那些藏在文档角落、没人告诉你、但一踩就深坑的细节才是区分“会用”和“用好”的关键。我把 JeeWMS 多租户落地过程中团队踩过的、验证过的、反复打磨过的实操要点一条条列出来。3.1 租户 ID 的生成与生命周期管理别用自增主键一开始我们图省事直接用数据库tenant表的id当tenantId结果上线两周就出事了某客户要求注销账号DBA 执行了DELETE FROM tenant WHERE id 1001结果所有关联的订单、库存记录全丢了因为外键没设ON DELETE CASCADE也没做软删除。血泪教训告诉我们租户 ID 必须是业务无关、永不变更、全局唯一、语义中立的字符串。JeeWMS 最终采用的是UUID的变种TNT-{8位随机字母数字}-{4位年份}-{2位月份}例如TNT-ab3x9k2m-2024-06。生成逻辑封装在TenantIdGenerator工具类里调用UUID.randomUUID().toString().substring(0, 8).toUpperCase() 当前年月。这个 ID保证全球唯一不怕分布式生成冲突包含年月方便归档和审计前缀TNT明确标识租户属性避免和warehouseId、orderId混淆字符串类型方便在日志、监控、ES 索引里直接搜索。实操心得千万别在tenant_id字段上建自增索引MySQL 的自增主键在高并发插入时会有锁竞争。我们把tenant_id设为VARCHAR(32)并在tenant表和所有业务表上对(tenant_id, created_time)建联合索引。这样既能快速按租户查又能按时间范围高效分页。3.2 仓库与租户的绑定关系一对多还是多对多这是个容易被忽略但直接影响权限模型的设计点。JeeWMS 默认采用租户-仓库多对多模型。为什么场景一某品牌客户租户 A在全国有 5 个自营仓需要统一管理场景二某三方物流商租户 B为 10 个不同品牌提供仓配服务每个品牌对应不同仓库场景三某集团租户 C旗下有 3 个子公司每个子公司有自己的仓库但集团要汇总看全国库存。如果只做“租户-仓库一对一”场景二和三就无法满足。JeeWMS 的tenant_warehouse_rel关联表除了tenant_id和warehouse_id还存了rel_typeOWNER/OPERATOR/VIEWER和statusACTIVE/DISABLED。这样一个仓库可以同时是 A 的OWNER、B 的OPERATOR、C 的VIEWER权限粒度拉到极致。注意前端页面的“仓库切换”下拉框不是简单查warehouse表而是查tenant_warehouse_rel再 JOINwarehouse表获取名称。否则会出现“租户看不到自己没权限的仓库”这种低级错误。3.3 缓存穿透与雪崩防护Redis Key 的租户前缀不是加个冒号那么简单多租户系统里缓存是最容易出问题的地方。我们最初给 Redis Key 加前缀是这么写的inventory: tenantId : skuCode。结果压测时发现当某个 SKU比如爆款手机在多个租户里都存在时Key 冲突导致缓存击穿——A 租户查inventory:TNT-abc-2024-06:SKU123B 租户查inventory:TNT-def-2024-06:SKU123两个 Key 完全不同但底层数据源都是同一个 MySQL 行。如果 A 租户的缓存失效了大量请求打到 DBB 租户的缓存也会跟着失效因为它们查的其实是同一行数据。解决方案是引入“数据域”概念把租户 ID 和业务实体 ID 绑定成一个不可分割的原子单位。JeeWMS 的 Redis Key 格式是inventory:{tenantId}:{warehouseId}:{skuCode}并且所有涉及库存变更的操作入库、出库、盘点都会触发一个CacheInvalidationService它会根据tenantId和warehouseId批量删除所有以该组合开头的 Key比如DEL inventory:TNT-abc-2024-06:*:SKU123。这样A 租户的库存变动只影响 A 租户的缓存B 租户完全不受干扰。3.4 日志与链路追踪如何一眼看出“这是谁的锅”多租户系统最大的运维噩梦就是日志里全是tenantIdxxx但你根本不知道xxx对应哪个客户。JeeWMS 的日志框架Logback做了两层增强MDCMapped Diagnostic Context注入在TenantHeaderFilter里把tenantId和warehouseId放进 MDCMDC.put(tenantId, tenantId); MDC.put(warehouseId, warehouseId);然后在logback-spring.xml的 pattern 里直接引用pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{tenantId}|%X{warehouseId}] %logger{50} - %msg%n/patternSkyWalking 链路打标在application.yml里配置 SkyWalking agent开启plugin.spring-cloud-gateway-3.x-plugin它会自动把X-Tenant-ID作为trace的 tag 上报。在 SkyWalking UI 里你可以直接按tenantId过滤所有链路查某个客户最近 1 小时的慢请求精准定位问题。实操心得千万别在日志里打印tenantId的明文我们用的是tenantId.substring(0, 4) ***既保留辨识度又符合 GDPR 和等保要求。生产环境日志脱敏是底线。4. 实操过程与核心环节实现从零搭建一个可运行的多租户 WMS现在我们来动手。假设你已经 clone 了 JeeWMS 的 GitHub 仓库v3.2.0下面是如何一步步把它变成一个真正可用的多租户系统。这不是 demo而是我们给客户交付时的标准 SOP。4.1 环境准备三台服务器撑起最小高可用集群角色服务器配置数量说明Nacos 集群4C8G50GB SSD3 台用官方推荐的集群模式nacos-1、nacos-2、nacos-3通过cluster.conf配置MySQL 主从主8C16G200GB SSD从4C8G200GB SSD2 台主库负责读写从库只读用于报表和 BI 查询应用节点4C8G100GB SSD3 台jeewms-gateway、jeewms-auth、jeewms-order等服务每台部署 2~3 个提示不要用 Docker Compose 搞单机伪集群Nacos 集群必须跨机器否则脑裂风险极高。我们吃过亏——一台宿主机宕机整个 Nacos 集群就挂了。4.2 数据库初始化执行脚本前的三个必改项JeeWMS 的sql/jeewms-mysql.sql脚本不能直接source。必须先改三处修改tenant表的id字段类型原脚本是BIGINT AUTO_INCREMENT改成VARCHAR(32) NOT NULL并删掉AUTO_INCREMENT。添加tenant_warehouse_rel表脚本里没有这个关联表必须手动创建CREATE TABLE tenant_warehouse_rel ( id bigint NOT NULL AUTO_INCREMENT, tenant_id varchar(32) NOT NULL COMMENT 租户ID, warehouse_id varchar(32) NOT NULL COMMENT 仓库ID, rel_type varchar(20) NOT NULL DEFAULT OWNER COMMENT 关系类型OWNER/OPERATOR/VIEWER, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-启用0-禁用, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_tenant_warehouse (tenant_id,warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;给所有业务表加tenant_id字段order_info、inventory_detail、picking_task等表都要执行ALTER TABLE xxx ADD COLUMN tenant_id VARCHAR(32) NOT NULL AFTER id;并建索引。做完这三步再source jeewms-mysql.sql否则后续服务启动会报Column tenant_id not found错误。4.3 Nacos 配置导入一份配置管住所有租户登录 Nacos 控制台http://nacos-1:8848/nacos依次导入以下配置全局配置Data ID:application.yamlGroup:jeewms-defaultspring: datasource: url: jdbc:mysql://mysql-master:3306/jeewms?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: your_password jeewms: tenant: default-tenant-id: TNT-DEFAULT-2024-01 # 兜底租户租户专属配置Data ID:application-tenant-1001.yamlGroup:jeewms-tenant-configjeewms: wms: inventory: min-stock-threshold: 10 max-reserve-ratio: 0.8 picking: batch-size: 25 timeout-minutes: 30服务路由配置Data ID:gateway-routes.yamlGroup:jeewms-gatewayspring: cloud: gateway: routes: - id: order-service-tenant-1001 uri: lb://order-service predicates: - Path/api/order/** filters: - name: TenantHeaderFilter args: headerName: X-Tenant-ID注意Nacos 的配置发布是异步的服务启动后要等 30 秒左右才能生效。我们写了个HealthCheckController暴露/actuator/tenant-config接口返回当前租户的配置快照方便前端调试。4.4 服务启动与租户注册curl 一下就完成入驻所有服务打包成 jar 后启动顺序很重要先启jeewms-nacosNacos 服务再启jeewms-gateway网关它要连 Nacos最后启jeewms-auth、jeewms-order等业务服务租户注册不用进后台页面点点点直接用 curl# 1. 创建租户 curl -X POST http://gateway:8080/api/tenant \ -H Content-Type: application/json \ -d {name:客户A,code:CLIENT-A,status:1} # 2. 创建仓库 curl -X POST http://gateway:8080/api/warehouse \ -H X-Tenant-ID: TNT-abc-2024-06 \ -H Content-Type: application/json \ -d {name:上海仓,code:SH-WH-001,type:SELF_OWNED} # 3. 绑定租户与仓库 curl -X POST http://gateway:8080/api/tenant/warehouse/bind \ -H X-Tenant-ID: TNT-abc-2024-06 \ -H Content-Type: application/json \ -d {tenantId:TNT-abc-2024-06,warehouseId:WH-001,relType:OWNER}看到{code:200,msg:success}就表示客户 A 已经成功入驻可以开始用系统了。整个过程5 分钟搞定。4.5 前端适配Vue 项目里的租户“隐身术”JeeWMS 前端是 Vue3 Element Plus。租户信息对用户是透明的但前端必须参与隔离请求拦截器在src/utils/request.js里所有axios请求都自动加X-Tenant-IDservice.interceptors.request.use(config { const tenantId localStorage.getItem(currentTenantId); if (tenantId) { config.headers[X-Tenant-ID] tenantId; } return config; });仓库切换组件warehouse-selector组件不是简单下拉而是调用/api/tenant/warehouse/list接口传X-Tenant-ID只返回当前租户有权访问的仓库列表。选中后把warehouseId存到 Vuex store并触发全局事件warehouse:change。页面级权限控制用v-permission指令但权限码不是order:create而是order:create:TNT-abc-2024-06。这样A 租户的“新建订单”按钮和 B 租户的本质是两个权限。实操心得前端绝对不能自己拼接tenantId必须从后端接口返回或者从登录成功后的 JWT token 里解析。我们曾经因为前端硬编码tenantId导致测试环境和生产环境混用差点把客户数据导错库。5. 常见问题与排查技巧实录那些深夜救火时的真实记录再完美的设计也架不住现实世界的千奇百怪。我把过去一年里客户现场、压测环境、上线当天遇到的最典型、最高频、最让人抓狂的问题连同我们的排查路径和终极解法毫无保留地列出来。这不是教科书答案这是拿时间、人力、客户投诉换来的经验。5.1 问题速查表5 分钟定位10 分钟解决现象可能原因排查命令/步骤解决方案所有租户都查不到数据Nacos 配置未生效或application.yaml里spring.profiles.active没配prodcurl http://nacos-1:8848/nacos/v1/cs/configs?dataIdapplication.yamlgroupjeewms-default检查 Nacos 控制台确认配置已发布且profiles正确A 租户能看到 B 租户的数据tenant_id字段在某张表里漏加了或 MyBatis-Plus 的tenant插件没生效grep -r tenant_id ./src/main/java/com/jeewms/检查实体类和 XML找到漏加的表补tenant_id字段并在实体类加TableField网关返回 401但X-Tenant-ID明明传了TenantHeaderFilter的filterOrder顺序错了被其他 Filter 拦截了curl -v http://gateway:8080/api/health看响应头是否有X-Tenant-ID回显在TenantHeaderFilter类上加Order(Ordered.HIGHEST_PRECEDENCE)Redis 缓存命中率暴跌CPU 100%某个租户的warehouseId写错了导致 Key 前缀异常缓存雪崩redis-cli --scan --pattern inventory:TNT-*看 Key 数量是否暴增用redis-cli monitor抓包定位异常请求修复前端传参日志里全是tenantIdnullTenantHeaderFilter没被 Spring Cloud Gateway 加载或X-Tenant-ID请求头被 Nginx strip 了curl -H X-Tenant-ID: TNT-abc http://gateway:8080/api/health看日志是否出现检查application.yml里spring.cloud.gateway.routes的filters是否包含TenantHeaderFilter5.2 经典案例复盘一次“假死”背后的三层嵌套问题现象某客户反馈下午 2 点到 4 点所有操作都超时但监控显示 CPU、内存、DB 连接数都正常。排查过程第一层查 SkyWalking发现order-service的createOrder接口平均耗时从 200ms 暴涨到 8s但 DB 查询只有 50ms。第二层查order-service日志发现大量WARNFailed to get tenant config from Nacos, using default。第三层查 Nacos 日志发现nacos-2节点频繁 GCFull GC每分钟一次。根因nacos-2的 JVM 参数-Xmx配得太小只有 2G而该节点恰好承载了 80% 的租户配置。当客户在后台批量修改了 50 个租户的库存阈值Nacos 要加载并解析 50 个 YAML 文件内存瞬间打满GC 频繁响应变慢。order-service每次创建订单都要去 Nacos 拉配置等不及就超时。解法立即扩容nacos-2的 JVM-Xms4g -Xmx4g长期方案在order-service里加本地缓存Caffeine配置 TTL 5 分钟避免每次都打 Nacos运维规范Nacos 集群所有节点JVM 参数必须一致且不低于 4G。踩坑心得多租户系统的瓶颈往往不在业务代码而在基础设施的“木桶短板”。一个节点的配置失误能让整个租户体系瘫痪。所以我们现在的交付清单里第一条就是“Nacos 三节点JVM 参数必须书面确认”。5.3 性能压测实录单库百租户TPS 从 1200 到 3500 的调优路径我们用 JMeter 对 JeeWMS 做了 100 租户、50 仓库、200 并发的混合场景压测下单库存查询波次生成。初始 TPS 只有 1200远低于预期。调优过程如下SQL 层发现inventory_detail表的SELECT COUNT(*)查询慢。原索引是(sku_code)改成(tenant_id, sku_code, warehouse_id)联合索引TPS 15%。缓存层库存查询用的是RedisTemplate.opsForValue().get()改成RedisTemplate.execute()调用 Lua 脚本原子性读取并更新过期时间TPS 22%。网关层Gateway 的reactor.netty.http.server.maxConnections默认 1000调到 5000TPS 8%。JVM 层order-service的-XX:UseG1GC改成-XX:UseZGCJava 17GC 停顿从 120ms 降到 5msTPS 35%。最终TPS 稳定在 3500平均响应时间 180ms。结论很明确多租户性能是数据库、缓存、网关、JVM 四层协同的结果单点优化收益有限。5.4 权限越界漏洞一个PreAuthorize注解引发的血案漏洞描述某客户发现自己账号登录后能查看其他租户的仓库列表。代码定位后端 Controller 里有个接口GetMapping(/list) PreAuthorize(hasRole(ADMIN)) public ResultListWarehouse list() { ... }问题就出在PreAuthorize(hasRole(ADMIN))—— 它只校验了角色没校验租户任何租户的 ADMIN都能看到所有仓库。修复方案方案一推荐用PreAuthorize(tenantSecurityService.hasPermission(#tenantId, WAREHOUSE_VIEW))把租户 ID 作为参数传进去由tenantSecurityService做细粒度校验。方案二在 Service 层list()方法里强制加上WHERE tenant_id ?并用SecurityContextHolder获取当前租户 ID。重要提醒Spring Security 的PreAuthorize是方法级但多租户的权限校验必须是“数据级”。永远不要相信前端传来的tenantId必须从SecurityContext里取那是网关注入的、可信的。6. 扩展与演进当多租户遇上 AI 和边缘计算JeeWMS 的多租户架构不是终点而是起点。我们已经在几个客户现场开始了下一阶段的探索这些方向不是 PPT 上的画饼而是真实跑在产线上的 PoC。6.1 租户级 AI 模型让每个客户都有自己的“库存预测大脑”传统 WMS 的需求预测是全公司一套模型用历史销量拟合。但 JeeWMS 现在支持为每个租户训练独立的 LSTM 模型。流程是每个租户的销售数据通过 Kafka 流式接入Flink 作业按tenantId分组实时计算skuCode的 7 日滚动销量每日凌晨调度系统触发mlflow任务用该租户过去 90 天的数据训练一个专属模型模型预测结果存回 RedisKey 为forecast:{tenantId}:{skuCode}。效果某快消客户单品预测准确率从 68% 提升到 89%缺货率下降 32%。关键是A 租户的模型训练失败完全不影响 B 租户的预测服务。6.2 边缘仓库节点把 WMS “缩小”装进 AGV 的工控机有些客户有微型前置仓比如社区生鲜店只有 2 台 AGV、1 个 PDA。给他们部署全套 Spring Cloud显然杀鸡用牛刀。JeeWMS 的解决方案是边缘轻量版。用 GraalVM 把 je
返回列表