☰
生鲜SaaS系统多租户架构设计与实践
2026/11/4 5:07:17 网站建设 项目流程

1. 项目概述

"升鲜宝"是一款面向生鲜零售行业的SaaS收银系统,采用"每租户独立数据库+主库"的混合架构模式。这套系统专门为连锁生鲜门店设计,解决多门店统一管理和独立运营的矛盾需求。我在实际部署中发现,生鲜行业对数据隔离和实时性有着特殊要求——不同门店的商品价格、促销策略需要独立配置,但总部又需要实时汇总销售数据。这种业务特性使得传统的单数据库架构难以满足需求。

系统基于Spring Boot框架开发,部署在阿里云RDS+ECS环境,通过Nginx实现负载均衡。核心功能包括租户生命周期管理、套餐定价、自动计费、模块授权等,特别针对生鲜行业设计了库存预警、批次追踪等特色功能模块。数据库设计采用Oracle兼容方案,既满足企业级事务需求,又保留了向其他数据库迁移的可能性。

2. 核心需求解析

2.1 生鲜行业特殊需求

生鲜门店的运营有三个显著特点:一是商品保质期短导致价格变动频繁,二是需要严格的批次追溯,三是早晚市流量波动大。这就要求收银系统必须:

  1. 支持动态价格策略(如临期商品自动折扣)
  2. 完整的进销存追溯能力
  3. 高并发下的稳定结算

我们在tenant表中特别设计了status字段(1正常 2试用 3冻结 4欠费停用),就是为了应对生鲜门店可能突然停业、临时续费等场景。比如当门店因装修暂停营业时,可以立即冻结系统访问而不丢失历史数据。

2.2 多租户架构选择

常见的SaaS多租户方案有:

  • 共享表(通过tenant_id区分)
  • 共享数据库独立schema
  • 完全独立数据库

经过压力测试,最终选择"独立数据库+主库"方案是因为:

  1. 生鲜交易数据量大(单店日均2000+订单)
  2. 各门店业务完全独立
  3. 符合连锁企业数据主权要求

主库(saas_master_db)仅存放租户元数据和跨店汇总信息,各门店业务数据存储在独立的租户库中。这种设计在阿里云RDS上实现成本可控,单个租户库月费用约150元。

3. 数据库详细设计

3.1 租户域核心表

tenant表关键设计点:
CREATE TABLE tenant ( id BIGINT PRIMARY KEY COMMENT '租户ID', tenant_code VARCHAR(64) NOT NULL COMMENT '租户编码', tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称', status INT NOT NULL DEFAULT 1 COMMENT '1正常 2试用 3冻结 4欠费停用', trial_end_time BIGINT COMMENT '试用结束时间', current_plan_id BIGINT COMMENT '当前套餐ID', expire_time BIGINT COMMENT '到期时间' ) ENGINE=InnoDB;

特别说明几个字段的业务含义:

  • tenant_code采用"区域+门店编号"规则(如BJ-001)
  • status=3(冻结)时仍可查询历史数据但不能新增交易
  • expire_time精确到分钟级,避免生鲜早市开始时出现服务中断
tenant_admin表安全设计:
CREATE TABLE tenant_admin ( password_hash VARCHAR(255) NOT NULL COMMENT 'PBKDF2WithHmacSHA256加密', phone VARCHAR(32) COMMENT '短信验证绑定', enabled TINYINT DEFAULT 1 COMMENT '0表示禁用' );

采用PBKDF2算法加密密码,配合短信二次验证。实测显示,这种方案在保证安全性的同时,登录耗时控制在800ms内,不影响收银效率。

3.2 套餐与定价模型

生鲜行业的套餐设计需要特别考虑:

  1. 基础功能(收银、库存)
  2. 增值模块(配送、会员营销)
  3. 用量计费(API调用、打印次数)

saas_plan表关键字段:

billing_cycle INT NOT NULL COMMENT '1月付 2年付', base_price DECIMAL(18,2) NOT NULL COMMENT '基础价格', trial_days INT DEFAULT 0 COMMENT '试用天数'

典型套餐配置示例:

  • 基础版:包含POS+库存,月付299元
  • 专业版:增加批次追溯,年付2999元(省25%)

价格组件表(saas_price_component)支持三种计费方式:

  1. 订阅式:固定周期收费
  2. 用量式:按实际使用量
  3. 一次性:如开通费

3.3 订阅与计费流程

生鲜行业特有的账单周期问题:

  • 多数门店选择自然月结算
  • 但新店开业常在月中,需要按比例计费

tenant_subscription表处理逻辑:

CREATE TABLE tenant_subscription ( start_time BIGINT NOT NULL COMMENT '精确到毫秒', end_time BIGINT NOT NULL, billing_cycle INT NOT NULL COMMENT '1月付 2年付', auto_renew TINYINT DEFAULT 0 );

续费流程特别处理:

  1. 提前7天扫描到期租户
  2. 生成invoice时计算闰月等情况
  3. 支持"冻结期"(欠费后3天内可恢复)

4. 关键业务实现

4.1 自动续费任务

采用Spring Scheduler实现每日凌晨2点执行:

@Scheduled(cron = "0 0 2 * * ?") public void autoRenewJob() { // 查询7天内到期的租户 List<Tenant> expiringTenants = tenantMapper.selectExpiring(); expiringTenants.forEach(tenant -> { // 生成发票 Invoice invoice = billingService.generateInvoice(tenant); if (tenant.getAutoRenew() == 1) { // 自动扣款 paymentService.autoCharge(tenant, invoice); } }); }

实际运营中发现,生鲜门店更喜欢上午续费,因此将执行时间调整为早6点,成功率提升18%。

4.2 配额控制实现

tenant_entitlement_snapshot表的quota_json存储格式:

{ "POS_TERMINAL": 2, "REPORT_EXPORT": 50, "INVENTORY_ALERT": true }

在Spring Boot中通过AOP实现配额校验:

@Around("@annotation(quotaCheck)") public Object checkQuota(ProceedingJoinPoint joinPoint) { String feature = quotaCheck.value(); int currentUsage = getCurrentUsage(feature); int limit = getQuotaLimit(feature); if (currentUsage >= limit) { throw new QuotaExceededException(); } return joinPoint.proceed(); }

5. 性能优化实践

5.1 数据库分片策略

租户数据库按地区分片:

  • 华北区域:tenant_db_hb_001 ~ 100
  • 华东区域:tenant_db_hd_001 ~ 100

通过ShardingSphere实现路由:

spring: shardingsphere: datasource: names: ds-master,ds-hb-001,ds-hd-001 sharding: tables: tenant: actual-data-nodes: ds-master.tenant order: actual-data-nodes: ds-hb-$->{0..99}.order,ds-hd-$->{0..99}.order database-strategy: standard: precise-algorithm-class-name: com.xfresh.db.region.PreciseShardingAlgorithm range-algorithm-class-name: com.xfresh.db.region.RangeShardingAlgorithm

5.2 缓存设计

采用多级缓存策略:

  1. 本地缓存(Caffeine):存储租户基础信息,TTL=5分钟
  2. Redis集群:存储套餐详情、配额数据,TTL=1小时
  3. 数据库:最终数据源

缓存更新策略:

  • 租户信息变更时主动失效缓存
  • 套餐数据采用定时刷新

6. 踩坑与解决方案

6.1 时间戳问题

初期使用LocalDateTime存储时间,在跨时区部署时出现计费错误。改为统一存储UTC时间戳:

expire_time BIGINT COMMENT 'UTC毫秒时间戳'

在Java端处理:

public long calculateExpireTime(int billingCycle) { Instant now = Instant.now(); if (billingCycle == MONTHLY) { return now.plus(30, ChronoUnit.DAYS).toEpochMilli(); } else { return now.plus(365, ChronoUnit.DAYS).toEpochMilli(); } }

6.2 账单精度问题

生鲜行业常有0.99元等定价,初期使用float导致金额误差。全部改为DECIMAL(18,2):

total_amount DECIMAL(18,2) NOT NULL COMMENT '精确到分'

在Java中使用BigDecimal进行计算:

BigDecimal price = new BigDecimal("9.99"); BigDecimal quantity = new BigDecimal("3"); BigDecimal total = price.multiply(quantity); // 29.97

7. 企业级扩展

7.1 生鲜特色模块

在saas_plan_module中增加行业专用模块:

  • 批次追溯(TRACE)
  • 临期预警(EXPIRE_ALERT)
  • 产地溯源(ORIGIN_TRACE)

配置示例:

INSERT INTO saas_plan_module VALUES (1, 101, 'TRACE', 1), (2, 101, 'EXPIRE_ALERT', 1);

7.2 硬件集成方案

通过quota_json控制外设数量:

{ "SCALE": 2, "PRINTER": 1, "CARD_READER": 1 }

实际部署中发现,生鲜门店常用设备包括:

  • 电子秤(通过串口通信)
  • 小票打印机(ESC/POS指令)
  • 扫码枪(HID设备)

8. 监控与运维

8.1 关键指标监控

  1. 租户数据库空间使用率(生鲜数据增长快)
  2. 日结账单生成耗时(晚高峰时段)
  3. 电子秤通信延迟(影响称重效率)

通过Prometheus+Granfa实现看板:

- job_name: 'xfresh_saas' metrics_path: '/actuator/prometheus' static_configs: - targets: ['10.0.0.1:8080']

8.2 灾备方案

针对生鲜行业早高峰特点设计:

  1. 每日4:00进行数据库快照
  2. 交易数据实时同步到备库
  3. 出现故障时5分钟内切换备用POS机

备份策略示例:

# 每天凌晨全量备份 0 4 * * * pg_dump -U postgres -d tenant_db_001 > /backup/tenant_001_$(date +\%Y\%m\%d).sql

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询