摘要
SaaS系统必须保证不同租户的数据与配置严格隔离。规则引擎作为业务决策的核心组件,其租户隔离设计直接影响系统的安全性与可扩展性。本文以JVS-Rules为例,详细阐述其在多租户场景下的规则隔离方案,包括租户ID绑定、元数据隔离、规则执行上下文、性能优化及数据权限控制,并提供架构图与代码示例。
1. 多租户规则隔离的挑战
在多租户SaaS系统中,每个租户可能有完全不同的业务规则(折扣、审批流程、风控阈值)。规则引擎需要满足:
-
数据隔离:租户A的规则不能被租户B看到或执行。
-
配置独立:每个租户可独立管理自己的决策表、规则流,互不影响。
-
性能可扩展:租户数量增加时,规则查找与执行不应线性下降。
-
安全合规:租户间禁止跨租户数据泄露。

2. JVS-Rules的多租户架构
JVS-Rules采用"共享引擎实例+租户级规则仓库"的架构:
text
┌─────────────────────────────────────────────┐ │ JVS-Rules 引擎实例 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 租户A │ │ 租户B │ │ 租户C │ │ │ │ 规则缓存 │ │ 规则缓存 │ │ 规则缓存 │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ └────────────┼────────────┘ │ │ ↓ │ │ 规则仓库(按tenant_id分表/分区) │ └─────────────────────────────────────────────┘
核心设计点:
-
租户识别:每个API请求携带租户ID(JWT或Header)。
-
规则存储 :数据库所有规则表包含
tenant_id字段,并建立复合索引。 -
规则缓存:按租户ID隔离的本地缓存(Caffeine/Redis)。
-
执行隔离:规则编译后的Rete网络按租户独立存储,避免跨租户影响。
3. 数据库隔离设计
采用共享表+租户ID区分方案(适合租户数量<10000的SaaS):
sql
CREATE TABLE decision_table (
id VARCHAR(32) PRIMARY KEY,
tenant_id VARCHAR(32) NOT NULL,
name VARCHAR(128),
...
INDEX idx_tenant (tenant_id)
);
CREATE TABLE decision_rule (
id VARCHAR(32) PRIMARY KEY,
table_id VARCHAR(32),
tenant_id VARCHAR(32) NOT NULL,
...
);
所有查询自动加上WHERE tenant_id = ?条件,通过MyBatis Plus拦截器实现。
4. 规则编译与缓存隔离
java
@Component
public class TenantRuleEngine {
private final Map<String, RuleSet> tenantRuleCache = new ConcurrentHashMap<>();
public List<RuleResult> fire(String tenantId, String tableName, Map<String, Object> facts) {
RuleSet ruleSet = tenantRuleCache.computeIfAbsent(tenantId + "_" + tableName,
key -> loadRuleSet(tenantId, tableName));
return ruleEngine.fire(ruleSet, facts);
}
private RuleSet loadRuleSet(String tenantId, String tableName) {
// 从数据库加载该租户的决策表,编译为RuleSet
DecisionTable table = decisionTableMapper.selectByTenantAndName(tenantId, tableName);
return DecisionTableCompiler.compile(table);
}
}
缓存失效策略:当租户修改规则时,发送MQ消息或直接清除对应租户的缓存条目。
5. 规则执行时的上下文安全
规则脚本中可能引用租户特定的数据源或服务。需要确保脚本只能访问授权资源:
groovy
// 规则脚本示例(租户A)
def amount = fact.order.amount
def vipThreshold = tenantConfigService.getConfig("vip_threshold") // 租户级配置
if (amount > vipThreshold) {
result.discount = 0.2
}
为防止脚本跨租户访问,可在脚本沙箱中注入租户ID校验:
java
// 脚本执行前绑定租户上下文
ThreadLocal<String> currentTenant = new ThreadLocal<>();
currentTenant.set(tenantId);
// 脚本中通过工具类获取租户ID,并校验任何数据库查询都带上该ID
6. 性能优化
| 优化点 | 实现方式 |
|---|---|
| 规则预编译 | 租户首次请求时编译并缓存,后续直接使用 |
| 租户级缓存隔离 | 使用Caffeine的CacheBuilder为每个租户独立缓存,避免大租户占满内存 |
| 数据库连接池 | 租户信息通过注解传递,MyBatis插件自动注入租户ID,避免SQL拼接开销 |
| 批量规则执行 | 支持批量事实(Facts)传入,减少网络往返 |
压力测试(100租户,每租户50条规则,并发50):
-
命中缓存时:P99延迟<8ms。
-
缓存未命中需加载编译:P99延迟<50ms。
7. 数据权限的进一步扩展
规则引擎除了规则隔离,还可能需要控制规则执行后返回的数据范围。例如,租户A下的不同部门只能查看本部门的数据。JVS-Rules支持在规则动作中调用数据权限服务,对结果集进行过滤。
8. 总结
JVS-Rules的多租户设计兼顾了隔离性、性能与易用性。通过租户ID绑定、缓存隔离、脚本沙箱等机制,实现了安全、高效的SaaS级规则引擎。对于正在构建SaaS产品的团队,这套方案具有较高的参考价值。
免费体验Demo:https://rules.bctools.cn/
标签:#规则引擎 #多租户 #SaaS #JVS-Rules