状态机组件 (StateMachine)
组件作用
项目开发中,状态管理是一个常见且复杂的业务场景。传统方式通常使用大量的 if-else/switch-case 来处理状态转换逻辑,导致代码复杂度高、难以维护、容易出错,且状态转换规则分散在各处,难以统一管理。
为了解决该问题,我们设计了状态机组件,该组件基于 Easy Rules 4.1.0 构建,提供了统一的状态管理 API,支持配置化状态机定义、MVEL 表达式条件判断和动作执行、Redis 持久化存储、双模式异步数据库持久化等特性。
核心设计原则:
- 配置化状态机:状态转换规则通过 YAML/JSON 配置文件定义,集中管理,易于维护
- ID 模式设计:解耦业务对象,仅需传入业务ID,降低耦合度
- Redis 单数据源:Redis 作为 Single Source of Truth,提供毫秒级查询性能
- 双模式异步持久化:支持两种异步持久化方式,根据部署场景自动选择
- REDIS 模式:通过 Redis Stream 实现数据库异步持久化,支持多实例部署和分布式消费
- ASYNC_THREAD 模式:通过虚拟线程池实现数据库异步持久化,适合单实例部署
- 静态门面 API:
StateMachine静态类提供便捷的调用方式,无需依赖注入 - 类型安全:支持枚举类型的状态机名称和事件,提供编译期约束
适用场景:
- ✅ 订单状态管理(待确认 → 已确认 → 制作中 → 已完成)
- ✅ 支付状态管理(待支付 → 支付中 → 支付成功/支付失败)
- ✅ 审批流程(待审批 → 审批中 → 已通过/已拒绝)
- ✅ 任务管理(待分配 → 进行中 → 已完成/已取消)
- ✅ 库存管理(正常 → 预警 → 缺货 → 补货中)
- ✅ 多状态维度管理(订单状态、支付状态、物流状态等)
不适用场景:
- ❌ 简单的开关状态(启用/禁用)
- ❌ 无转换规则的状态(如用户性别、商品分类)
- ❌ 频繁变化的状态(如实时位置、温度等)
核心特性
🔄 配置化状态管理
- YAML/JSON 配置:状态转换规则通过配置文件定义,集中管理,易于维护
- MVEL 表达式:支持条件判断和动作执行的 MVEL 表达式,提供表达式安全检查和慢查询监控
- 规则优先级:支持规则优先级配置,
priorityThreshold控制规则执行范围 - 规则执行策略:可配置
skipOnFirstFailedRule、skipOnFirstAppliedRule、skipOnFirstNonTriggeredRule
🚀 高性能设计
- Redis 单数据源:Redis 作为 Single Source of Truth,提供毫秒级查询性能(< 10ms)
- 缓存预热机制:启动时如果 Redis 为空,自动从数据库加载并回填 Redis
- 双模式异步持久化:支持两种异步持久化方式,根据部署场景自动选择
- REDIS 模式:通过 Redis Stream 异步写入数据库,支持多实例分布式消费,不影响主流程性能
- ASYNC_THREAD 模式:通过虚拟线程池异步写入数据库,适合单实例场景,轻量级且高效
- 批量处理:两种模式均支持批量写入数据库,提升持久化性能
🛡️ 可靠性保障
- 终态不可变更:FINAL/ERROR 状态默认不可变更,防止误操作(可通过
attributes.reopen=true允许重新打开) - 状态历史追踪:完整的状态变更历史记录,默认保留7天(可配置)
- 并发安全:Redis 分布式锁 + 事务保证原子性,无需手动处理并发
- 双模式数据持久化保障:根据部署场景选择最适合的持久化方式
- REDIS 模式:Redis Stream 消息持久化 + ACK 机制 + 消费组,确保数据不丢失,支持多实例部署
- ASYNC_THREAD 模式:虚拟线程池 + 共享缓冲区 + 优雅关闭,确保数据不丢失,适合单实例场景
- 数据库类型自适应:ASYNC_THREAD 模式自动检测数据库类型(MySQL/PostgreSQL/Oracle),使用对应的批量插入或更新语法
🎯 易用性强
- 静态门面 API:
StateMachine静态类提供便捷的调用方式,无需依赖注入 - ID 模式设计:解耦业务对象,仅需传入业务ID,降低耦合度
- 类型安全:支持枚举类型的状态机名称和事件,提供编译期约束
- Spring Boot 自动配置:开箱即用,零配置启动
📊 多状态机管理
- 多状态维度:支持一个业务对象多个状态维度(如订单状态、支付状态、物流状态)
- 独立管理:每个状态维度独立管理,互不干扰
- 命名空间化:支持命名空间化的状态机名称(如
order.payment、order.fulfillment)
组件用法
引入依赖
在项目所属的 Maven 模块中引入组件坐标:
<dependency>
<groupId>cn.com.rydeen.component</groupId>
<artifactId>rydeen-component-statemachine</artifactId>
</dependency>
<!-- Redis 存储支持(必须) -->
<dependency>
<groupId>cn.com.rydeen.component</groupId>
<artifactId>rydeen-component-cache</artifactId>
</dependency>
<!-- 数据库持久化支持(可选,如需数据库持久化) -->
<dependency>
<groupId>cn.com.rydeen.component</groupId>
<artifactId>rydeen-component-dao</artifactId>
</dependency>
配置文件
状态机组件的配置统一在 Nacos 配置中心管理,所有服务共享同一份配置。
想要理解状态机组件的配置文件,首先需要了解状态机组件的几个基本概念:
- 状态机(State Machine):包含所有状态和转换规则的完整定义,通过 YAML/JSON 配置文件定义
- 状态(State):对象在某一时刻所处的条件或情况,包括初始状态(INITIAL)、普通状态(NORMAL)、终态(FINAL)、错误状态(ERROR)
- 事件(Event):触发状态转换的动作或条件
- 转换(Transition):从一个状态到另一个状态的过程,包含源状态、目标状态、触发事件、转换条件、转换动作
- 数据持久化:状态机组件支持两种数据持久化方式:
- REDIS 模式:使用 Redis 作为状态存储(Single Source of Truth),通过 Redis Stream 异步持久化到数据库,支持多实例部署
- ASYNC_THREAD 模式:直接使用异步线程池(虚拟线程)持久化到数据库,不依赖 Redis Stream,适合单实例部署或 Redis 版本过低的场景
配置示例
状态机组件的配置统一在 Nacos 配置中心管理。根据部署场景选择不同的持久化方式:
方式一:REDIS 模式(推荐,支持多实例部署)
platform:
component:
statemachine:
# 启用状态机组件
enabled: true
# 规则引擎配置
rules-engine:
skip-on-first-failed-rule: false
skip-on-first-applied-rule: false
skip-on-first-non-triggered-rule: false
rule-priority-threshold: 0
# 状态机配置
state-machine:
enable-history: true
enable-events: true
config-path: classpath:statemachine/
# 存储类型:REDIS(使用 Redis 存储,支持多实例部署)
storage-type: REDIS
# Redis 存储配置(仅在 storage-type=REDIS 时生效)
redis-storage:
# Redis 缓存键前缀
key-prefix: platform:statemachine
# 历史记录过期时间(单位:毫秒),默认7天,仅对历史记录生效
timeout: 604800000
# Redis Stream 数据库持久化配置(需要 Redis 5.0+)
redis-stream:
enabled: true # 是否启用 Redis Stream 数据库持久化
batch-size: 200 # 批量写入数据库的记录数
# 终态自动清理配置(可选)
cleanup:
enabled: false # 是否启用终态自动清理
fixed-delay-ms: 3600000 # 清理任务执行间隔(毫秒),默认1小时
ttl-days: 30 # 终态保留天数
batch-size: 500 # 单次清理批次大小
# 表达式配置
expression:
slow-threshold-ms: 1000
enable-security-check: true
enable-detailed-log: false
# Redis Stream 消费者配置(仅在 storage-type=REDIS 且 redis-storage.redis-stream.enabled=true 时需要)
cache:
redis:
stream:
consumers:
enabled: true
configs:
state-sync:
stream-key: platform:statemachine:db:sync
group: statemachine-db-sync-group
consumer: ${HOSTNAME:statemachine-db-sync-consumer}
target-type: cn.com.rydeen.component.statemachine.event.StateSyncMessage
count: 50 # 单次拉取消息数量
concurrency: ${STATEMACHINE_DB_SYNC_CONCURRENCY:8} # 并发处理数(建议=CPU核心数)
auto-ack: true
error-strategy: SKIP
idempotency-enabled: true
auto-start: true
max-len: 10000 # Stream 最大保留消息数
cleanup:
enabled: true # 是否启用自动清理
interval: 1h # 清理间隔
default-max-len: 10000 # Stream 最大保留消息数(全局默认值)
⚠️ 重要提示:部分云服务商的 PAAS 版 Redis 对 Redis Stream(MQ)功能的支持尚不完整,可能会导致消息消费丢失的情况。如遇到此类问题,请切换至
ASYNC_THREAD模式。
方式二:ASYNC_THREAD 模式(单实例部署,不依赖 Redis Stream)
platform:
component:
statemachine:
# 启用状态机组件
enabled: true
# 规则引擎配置
rules-engine:
skip-on-first-failed-rule: false
skip-on-first-applied-rule: false
skip-on-first-non-triggered-rule: false
rule-priority-threshold: 0
# 状态机配置
state-machine:
enable-history: true
enable-events: true
config-path: classpath:statemachine/
# 存储类型:ASYNC_THREAD(直接异步持久化到数据库,不依赖 Redis Stream)
storage-type: ASYNC_THREAD
# 异步数据库持久化配置(仅在 storage-type=ASYNC_THREAD 时生效)
thread-pool:
enabled: true # 是否启用异步数据库持久化
batch-size: 200 # 批量写入数据库的记录数,达到此数量时触发批量写入
buffer-capacity: 10000 # 最大缓冲条数(触发紧急刷写)
flush-interval-ms: 2000 # 定时刷新间隔(毫秒),定期检查缓冲区并触发刷写
# 注意:使用 Java 21 虚拟线程执行器,由 JVM 自动管理,无需配置线程池大小
# 表达式配置
expression:
slow-threshold-ms: 1000
enable-security-check: true
enable-detailed-log: false
两种持久化方式对比:
| 特性 | REDIS 模式 | ASYNC_THREAD 模式 |
|---|---|---|
| 多实例部署 | ✅ 支持(通过 Redis Stream 分布式消费) | ❌ 不支持(单实例内批量处理) |
| Redis 依赖 | ✅ 需要 Redis 5.0+(支持 Redis Stream) | ❌ 不依赖 Redis Stream(但仍需要 Redis 作为状态存储) |
| 持久化机制 | Redis Stream 异步持久化 | 虚拟线程池异步持久化 |
| 性能 | 高(分布式消费,支持多实例并行) | 高(虚拟线程,适合 I/O 密集型) |
| 适用场景 | 生产环境多实例部署 | 单实例部署、Redis 版本过低或云服务商 PAAS Redis 不支持完整 Stream 功能 |
| 配置复杂度 | 较高(需要配置 Redis Stream 消费者) | 较低(只需配置线程池参数) |
| 云服务商兼容性 | ⚠️ 部分云服务商 PAAS Redis 对 Stream 支持不完整,可能导致消息丢失 | ✅ 不依赖 Redis Stream,兼容性更好 |
选择建议:
- 生产环境多实例部署:使用
REDIS模式,通过 Redis Stream 实现分布式消费和异步持久化 - 单实例部署或 Redis 版本过低:使用
ASYNC_THREAD模式,直接通过虚拟线程池异步持久化到数据库 - 云服务商 PAAS Redis:⚠️ 重要:部分云服务商的 PAAS 版 Redis 对 Redis Stream(MQ)功能的支持尚不完整,可能会导致消息消费丢失的情况。如遇到此类问题,请切换至
ASYNC_THREAD模式 - 默认值:
storage-type默认为ASYNC_THREAD,适合大多数单实例场景
配置说明
基础配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
platform.component.statemachine.enabled | boolean | true | 是否启用状态机组件 |
platform.component.statemachine.state-machine.config-path | string | classpath:statemachine/ | 状态机配置文件路径 |
platform.component.statemachine.state-machine.enable-history | boolean | true | 是否启用状态历史记录 |
platform.component.statemachine.state-machine.enable-events | boolean | true | 是否启用状态变更事件 |
规则引擎配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
platform.component.statemachine.rules-engine.skip-on-first-failed-rule | boolean | false | 是否跳过第一个失败的规则 |
platform.component.statemachine.rules-engine.skip-on-first-applied-rule | boolean | false | 是否跳过第一个应用的规则 |
platform.component.statemachine.rules-engine.skip-on-first-non-triggered-rule | boolean | false | 是否跳过第一个非触发的规则 |
platform.component.statemachine.rules-engine.rule-priority-threshold | int | 0 | 规则优先级阈值,只执行优先级 >= 该值的规则 |
存储配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
platform.component.statemachine.storage-type | enum | ASYNC_THREAD | 存储类型:REDIS(Redis 存储,支持多实例部署)或 ASYNC_THREAD(异步线程池,单实例部署) |
REDIS 模式配置(storage-type=REDIS 时生效):
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
platform.component.statemachine.redis-storage.key-prefix | string | platform:statemachine | Redis 缓存键前缀,用于归类和管理状态机相关的 Redis key |
platform.component.statemachine.redis-storage.timeout | long | 604800000 | 历史记录过期时间(单位:毫秒),默认7天,仅对历史记录生效 |
platform.component.statemachine.redis-storage.redis-stream.enabled | boolean | false | 是否启用 Redis Stream 数据库持久化(需要 Redis 5.0+) |
platform.component.statemachine.redis-storage.redis-stream.batch-size | int | 200 | 批量写入数据库的记录数 |
platform.component.statemachine.redis-storage.cleanup.enabled | boolean | false | 是否启用终态自动清理 |
platform.component.statemachine.redis-storage.cleanup.fixed-delay-ms | long | 3600000 | 清理任务执行间隔(毫秒),默认1小时 |
platform.component.statemachine.redis-storage.cleanup.ttl-days | long | 30 | 终态保留天数 |
platform.component.statemachine.redis-storage.cleanup.batch-size | int | 500 | 单次清理批次大小 |
ASYNC_THREAD 模式配置(storage-type=ASYNC_THREAD 时生效):
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
platform.component.statemachine.thread-pool.enabled | boolean | true | 是否启用异步数据库持久化 |
platform.component.statemachine.thread-pool.batch-size | int | 200 | 批量写入数据库的记录数,达到此数量时触发批量写入 |
platform.component.statemachine.thread-pool.buffer-capacity | int | 10000 | 最大缓冲条数(触发紧急刷写),当缓冲区达到此大小时立即触发刷写 |
platform.component.statemachine.thread-pool.flush-interval-ms | long | 2000 | 定时刷新间隔(毫秒),定期检查缓冲区,如果有数据则触发刷写 |
表达式配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
platform.component.statemachine.expression.slow-threshold-ms | long | 1000 | 慢查询阈值(毫秒),超过该时间会记录警告日志 |
platform.component.statemachine.expression.enable-security-check | boolean | true | 是否启用表达式安全检查 |
platform.component.statemachine.expression.security-blacklist | list | [] | 自定义表达式黑名单(方法名) |
platform.component.statemachine.expression.enable-detailed-log | boolean | false | 是否启用详细日志(记录所有表达式执行) |
创建状态机配置文件
状态机配置文件使用 YAML 或 JSON 格式,默认扫描 classpath:statemachine/**/*.yml|yaml|json。
订单状态机示例
# src/main/resources/statemachine/order-statemachine.yml
name: order
description: 订单状态机
# 状态定义
states:
- name: PENDING
description: 待确认
type: INITIAL
- name: CONFIRMED
description: 已确认
type: NORMAL
- name: PREPARING
description: 制作中
type: NORMAL
- name: COMPLETED
description: 已完成
type: FINAL
- name: CANCELLED
description: 已取消
type: ERROR
# 转换定义
transitions:
- name: confirm
description: 确认订单
fromState: PENDING
toState: CONFIRMED
event: CONFIRM
# MVEL 条件表达式:可通过 context 访问上下文属性
condition: "context.getAttribute('amount') != null && (Double)context.getAttribute('amount') > 0"
# MVEL 动作表达式:可执行业务逻辑
action: "context.setAttribute('confirmedTime', java.time.LocalDateTime.now())"
- name: start_prepare
description: 开始制作
fromState: CONFIRMED
toState: PREPARING
event: START_PREPARE
action: "context.setAttribute('prepareTime', java.time.LocalDateTime.now())"
- name: complete
description: 完成订单
fromState: PREPARING
toState: COMPLETED
event: COMPLETE
action: "context.setAttribute('completedTime', java.time.LocalDateTime.now())"
- name: cancel
description: 取消订单
fromState: PENDING
toState: CANCELLED
event: CANCEL
action: "context.setAttribute('cancelReason', context.getAttribute('reason'))"
定义枚举类型(推荐)
枚举类型提供编译期约束和类型安全,枚举名称必须与 YAML 配置中的名称完全一致:
// 状态机名称枚举
public enum OrderSm {
order; // 必须与 YAML 配置中的 name 一致
}
// 事件枚举(必须与 YAML 中的 event 名称一致)
public enum OrderEvent {
CONFIRM,
START_PREPARE,
COMPLETE,
CANCEL
}
// 状态枚举(必须与 YAML 中的 state 名称一致)
public enum OrderState {
PENDING,
CONFIRMED,
PREPARING,
COMPLETED,
CANCELLED
}
使用状态机
方式一:使用静态门面(推荐,无需注入)
import cn.com.rydeen.component.statemachine.StateMachine;
import cn.com.rydeen.component.statemachine.engine.StateTransitionResult;
import cn.com.rydeen.component.statemachine.storage.StateHistory;
@Service
public class OrderService {
/**
* 确认订单(基础用法)
*/
public void confirmOrder(String orderId) {
// 使用静态门面,传入业务ID和事件即可
StateTransitionResult result = StateMachine.fire(
OrderSm.order, // 状态机名称(枚举)
OrderEvent.CONFIRM, // 事件(枚举)
orderId // 业务ID
);
if (result.isSuccess()) {
log.info("订单确认成功: {}", orderId);
} else {
throw new RuntimeException("订单确认失败: " + result.getErrorMessage());
}
}
/**
* 确认订单(带上下文属性)
*/
public void confirmOrderWithContext(String orderId, String operator, Double amount) {
// 传入上下文属性,可在 MVEL 表达式中使用
Map<String, Object> attributes = Map.of(
"operator", operator,
"amount", amount,
"channel", "WEB"
);
StateTransitionResult result = StateMachine.fire(
OrderSm.order,
OrderEvent.CONFIRM,
orderId,
attributes
);
if (result.isSuccess()) {
log.info("订单确认成功: {}", orderId);
} else {
throw new RuntimeException("订单确认失败: " + result.getErrorMessage());
}
}
/**
* 获取订单当前状态
*/
public OrderState getOrderStatus(String orderId) {
// 获取当前状态(枚举类型)
return StateMachine.getCurrentState(
OrderSm.order,
orderId,
OrderState.class
);
}
/**
* 获取订单状态历史
*/
public List<StateHistory> getOrderHistory(String orderId) {
return StateMachine.getStateHistory(OrderSm.order, orderId);
}
/**
* 检查是否可以执行事件
*/
public boolean canConfirm(String orderId) {
return StateMachine.canTransitionTo(
OrderSm.order,
OrderEvent.CONFIRM,
orderId
);
}
}
方式二:使用依赖注入(传统方式)
@Service
public class OrderService {
@Autowired
private StateMachineEngine stateMachineEngine;
public void confirmOrder(String orderId) {
StateTransitionResult result = stateMachineEngine.fire(
OrderSm.order,
OrderEvent.CONFIRM,
orderId
);
// ...
}
}
设计原则
Redis 单数据源原则
Redis 作为 Single Source of Truth:
- ✅ 当前状态和历史记录均先写入 Redis:状态变更后直接写入 Redis,提供毫秒级查询性能
- ✅ 数据库作为异步持久化副本:通过两种方式异步写入数据库,确保数据不丢失
- ✅ 缓存预热机制:启动时如果 Redis 为空,自动从数据库加载并回填 Redis
- ✅ 历史记录过期:默认保留7天(可配置),自动过期,避免 Redis 数据量过大
异步持久化原则
状态机组件支持两种异步持久化方式,根据部署场景自动选择:
方式一:Redis Stream 异步持久化(REDIS 模式)
适用场景:生产环境多实例部署,需要分布式消费和异步持久化
核心特性:
- ✅ 状态变更后直接发布同步消息:状态变更后直接发布
StateSyncMessage到 Redis Stream(不通过 Spring 事件) - ✅ 消息仅包含同步键:消息仅包含同步键(
stateMachineName:businessId),不包含状态数据 - ✅ 消费者从 Redis 读取最新状态:消费者从 Redis 读取最新状态和历史,确保数据一致性
- ✅ 多线程并发消费:
StateSyncStreamConsumer支持多线程并发处理(可配置 concurrency) - ✅ 批量处理:使用 ThreadLocal 缓冲区批量收集消息,达到 batchSize 时批量刷写到数据库
- ✅ 分布式消费组:支持多实例部署,通过 Redis Stream 消费组实现分布式消费
- ✅ 需要 Redis 5.0+:依赖 Redis Stream 功能
方式二:虚拟线程池异步持久化(ASYNC_THREAD 模式)
适用场景:单实例部署或 Redis 版本过低(不支持 Redis Stream)
核心特性:
- ✅ 虚拟线程执行器:使用 Java 21 虚拟线程,适合 I/O 密集型任务,提供更好的资源利用率
- ✅ 共享缓冲区:所有虚拟线程共享一个线程安全的缓冲区(
ConcurrentLinkedQueue),支持跨线程批量累积 - ✅ 批量写入:达到批量大小时自动刷写,提升数据库写入性能
- ✅ 紧急刷写:缓冲区达到最大容量时立即刷写,避免溢出
- ✅ 定时刷新:定期检查缓冲区,如果有数据则触发刷写,确保数据及时持久化
- ✅ 优雅关闭:服务关闭时自动刷写剩余消息,确保数据不丢失
- ✅ 数据库类型自适应:自动检测数据库类型(MySQL/PostgreSQL/Oracle),使用对应的批量插入或更新语法
- ✅ 单实例内批量处理:不支持多实例部署,适合单实例场景
ID 模式设计原则
解耦业务对象,仅需传入业务ID:
- ✅ 解耦业务对象:状态机组件不依赖业务对象的具体结构
- ✅ 降低耦合度:业务对象变更不影响状态机组件
- ✅ 性能优化:避免序列化/反序列化业务对象
- ✅ 灵活性:支持任意类型的业务ID(String、Long、UUID等)
- ✅ 多状态维度:支持一个业务对象多个状态维度
终态不可变更原则
FINAL/ERROR 状态默认不可变更:
- ✅ 终态保护:FINAL/ERROR 状态默认不允许转换,防止误操作
- ✅ 白名单机制:如需重新打开,需在转换定义中设置
attributes.reopen: true - ✅ 一致性保障:在写入 Redis 前做原子校验,避免并发覆写完结状态
- ✅ 审计记录:拒绝的事件可记录到日志或历史,便于定位误触
状态机工作原理
状态转换流程
状态机执行过程如下:
- 接收请求:业务代码调用静态外观类
StateMachine.fire() - 获取当前状态:从 Redis 存储中获取当前状态(如果 Redis 为空且启用 DB 持久化,则从数据库加载并回填 Redis)
- 查找转换规则:根据当前状态和事件查找匹配的转换
- 终态检查:如果当前状态是 FINAL 或 ERROR,仅允许带有
attributes.reopen=true的转换 - 验证条件:使用 MVEL 表达式检查转换条件是否满足(支持安全检查)
- 执行转换:更新状态并执行转换动作(MVEL 表达式)
- 保存结果:将新状态保存到 Redis(单数据源),并根据
storage-type配置选择异步持久化方式- REDIS 模式:投递到 Redis Stream 用于异步持久化
- ASYNC_THREAD 模式:提交到虚拟线程池用于异步持久化
- 返回结果:返回转换成功或失败的结果
状态机架构
┌─────────────────────────────────────────────────────────────┐
│ 业务代码层 │
│ StateMachine.fire(...) │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────────┐
│ 状态机引擎层 │
│ StateMachineEngine + Easy Rules │
│ - 状态转换规则匹配 │
│ - MVEL 表达式评估(条件/动作) │
│ - 规则优先级处理 │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
┌───────▼────────┐ ┌────────▼────────────────────────┐
│ Redis 存储 │ │ 异步持久化层(两种模式) │
│ (单数据源) │ │ - Redis Stream(REDIS 模式) │
└───────┬────────┘ │ - 虚拟线程池(ASYNC_THREAD 模式) │
│ └────────┬────────────────────────┘
│ │
│ ┌───────▼────────┐
│ │ Stream 消费者 │
│ │ (多线程并发) │
│ └───────┬────────┘
│ │
└────────────┬───────────────┘
│
┌────────▼────────┐
│ 数据库持久化 │
│ (MyBatis Plus) │
└─────────────────┘
核心设计原则
-
Redis 作为单数据源(Single Source of Truth)
- 当前状态和历史记录均先写入 Redis
- 数据库作为异步持久化副本,通过两种方式异步写入(根据
storage-type配置选择) - 缓存预热机制:启动时如果 Redis 为空,自动从数据库加载并回填 Redis
-
双模式异步持久化
- REDIS 模式:状态变更后直接发布
StateSyncMessage到 Redis Stream(不通过 Spring 事件),消息仅包含同步键(stateMachineName:businessId),消费者从 Redis 读取最新状态和历史 - ASYNC_THREAD 模式:状态变更后直接提交同步任务到虚拟线程池,使用共享缓冲区批量累积,从 Redis 读取最新状态和历史
- REDIS 模式:状态变更后直接发布
-
批量处理优化
- REDIS 模式:
StateSyncStreamConsumer支持多线程并发处理(可配置 concurrency),使用 ThreadLocal 缓冲区批量收集消息,达到 batchSize 时批量刷写到数据库 - ASYNC_THREAD 模式:使用共享缓冲区(ConcurrentLinkedQueue)批量累积,达到 batchSize 或 bufferCapacity 时批量刷写,支持定时刷新机制
- REDIS 模式:
启动初始化流程图
业务调用层流程图(静态外观 + Redis Stream 异步持久化)
查询操作流程图(Redis 缓存预热)
状态转换详细流程(Redis Stream 异步持久化)
多实例部署场景下的数据流(Redis Stream 分布式消费)
组件架构图(Redis Stream 异步持久化)
关键设计点说明
-
Redis 作为单数据源(Single Source of Truth):
- 当前状态和历史记录均先写入 Redis
- 数据库作为异步持久化副本,通过两种方式异步写入(根据
storage-type配置选择) - 缓存预热机制:启动时如果 Redis 为空,自动从数据库加载并回填 Redis
-
双模式异步持久化:
方式一:Redis Stream 异步持久化(REDIS 模式):
- 状态变更后直接发布
StateSyncMessage到 Redis Stream(不通过 Spring 事件) - 消息仅包含同步键(stateMachineName:businessId),不包含状态数据
- 消费者从 Redis 读取最新状态和历史,确保数据一致性
StateSyncStreamConsumer支持多线程并发处理(可配置 concurrency)- 使用 ThreadLocal 缓冲区批量收集消息,达到 batchSize 时批量刷写到数据库
- 支持多实例部署,通过 Redis Stream 消费组实现分布式消费
方式二:虚拟线程池异步持久化(ASYNC_THREAD 模式):
- 状态变更后直接提交同步任务到虚拟线程池(不通过 Redis Stream)
- 使用 Java 21 虚拟线程执行器,适合 I/O 密集型任务
- 所有虚拟线程共享一个线程安全的缓冲区(
ConcurrentLinkedQueue) - 达到批量大小时自动刷写,或缓冲区达到最大容量时紧急刷写
- 定时刷新机制:定期检查缓冲区,如果有数据则触发刷写
- 优雅关闭:服务关闭时自动刷写剩余消息
- 数据库类型自适应:自动检测数据库类型(MySQL/PostgreSQL/Oracle),使用对应的批量插入或更新语法
- 单实例内批量处理,不支持多实例部署
- 状态变更后直接发布
-
批量写入优化:
- 批量查询已存在记录,减少数据库查询次数
- 批量插入和更新,减少数据库压力
- 通过唯一约束去重,避免重复写入
- ASYNC_THREAD 模式根据数据库类型自动选择最优的批量写入语法
-
历史记录过期:
- 默认保留7天(可配置)
- 自动过期,避免 Redis 数据量过大
架构优势
✅ 灵活的部署模式:
- REDIS 模式:支持多实例部署,通过 Redis Stream 消费者组自动负载均衡,避免重复消费,支持水平扩展
- ASYNC_THREAD 模式:适合单实例部署,使用虚拟线程池,轻量级且高效,不依赖 Redis Stream
✅ 高性能:
- Redis 作为单数据源,查询延迟低(毫秒级)
- 两种模式均支持批量处理,减少数据库压力
- REDIS 模式支持多线程并发消费,ASYNC_THREAD 模式使用虚拟线程,均能提升吞吐量
✅ 可靠性:
- REDIS 模式:Redis Stream 消息持久化 + ACK 机制 + 消费组,确保数据不丢失
- ASYNC_THREAD 模式:虚拟线程池 + 共享缓冲区 + 优雅关闭,确保数据不丢失
- 缓存预热机制确保数据一致性
- 错误处理策略可配置(SKIP/RETRY)
- 数据库类型自适应,使用最优的批量写入语法
最佳实践
1. 状态定义
- ✅ 明确定义所有状态:包括初始状态(INITIAL)、普通状态(NORMAL)、终态(FINAL)、错误状态(ERROR)
- ✅ 使用枚举类型:定义状态枚举和事件枚举,提供编译期约束和类型安全
- ✅ 终态保护:FINAL/ERROR 状态默认不可变更,如需重新打开需设置
attributes.reopen: true
2. 转换规则
- ✅ 明确的事件定义:为每个状态转换定义明确的事件
- ✅ 条件判断:使用 MVEL 表达式进行条件判断,避免非法状态转换
- ✅ 动作执行:在转换时执行必要的业务逻辑(如记录时间、更新属性等)
- ✅ 规则优先级:合理使用规则优先级,控制规则执行顺序
3. 表达式使用
- ✅ 使用 context 对象:在 MVEL 表达式中使用
context.getAttribute()和context.setAttribute()访问上下文 - ✅ 避免危险方法:不要使用表达式黑名单中的方法(如
System.exit、Runtime.exec等) - ✅ 性能优化:避免在表达式中执行耗时操作,超过
slow-threshold-ms会记录警告
4. 存储与持久化
- ✅ 根据部署场景选择持久化方式:
- 多实例部署:使用
storage-type: REDIS,通过 Redis Stream 实现分布式消费和异步持久化 - 单实例部署:使用
storage-type: ASYNC_THREAD,通过虚拟线程池实现异步持久化,配置更简单 - ⚠️ 云服务商 PAAS Redis 限制:部分云服务商的 PAAS 版 Redis 对 Redis Stream(MQ)功能的支持尚不完整,可能会导致消息消费丢失的情况。如遇到此类问题,请切换至
ASYNC_THREAD模式
- 多实例部署:使用
- ✅ 生产环境使用 Redis:Redis 作为状态存储的 Single Source of Truth,提供毫秒级查询性能
- ✅ 启用数据库持久化:生产环境建议启用数据库持久化,确保数据不丢失
- REDIS 模式:设置
redis-storage.redis-stream.enabled: true - ASYNC_THREAD 模式:设置
thread-pool.enabled: true
- REDIS 模式:设置
- ✅ 合理设置过期时间:历史记录默认保留7天,可根据业务需求调整
- ✅ 批量大小调优:根据数据库性能和业务量调整批量大小
- REDIS 模式:调整
redis-storage.redis-stream.batch-size和 Redis Stream 消费者的count、concurrency - ASYNC_THREAD 模式:调整
thread-pool.batch-size、buffer-capacity、flush-interval-ms
- REDIS 模式:调整
5. 错误处理
- ✅ 检查转换结果:始终检查
StateTransitionResult.isSuccess() - ✅ 记录错误日志:记录状态转换失败的原因,便于问题排查
- ✅ 异常处理:妥善处理状态转换异常,避免影响主流程
常见问题
Q1: 为什么使用 ID 模式而不是传入业务对象?
A: ID 模式的优势:
- ✅ 解耦业务对象:状态机组件不依赖业务对象的具体结构
- ✅ 降低耦合度:业务对象变更不影响状态机组件
- ✅ 性能优化:避免序列化/反序列化业务对象
- ✅ 灵活性:支持一个业务对象多个状态维度
Q2: Redis 和数据库的关系是什么?
A:
- Redis 是单数据源(Single Source of Truth):当前状态和历史记录均先写入 Redis,提供毫秒级查询性能
- 数据库是异步持久化副本:通过两种方式异步写入数据库,确保数据不丢失
- REDIS 模式:通过 Redis Stream 异步写入数据库,支持多实例部署和分布式消费
- ASYNC_THREAD 模式:通过虚拟线程池异步写入数据库,适合单实例部署
- 缓存预热机制:启动时如果 Redis 为空,自动从数据库加载并回填 Redis
- 数据一致性:消费者/线程池从 Redis 读取最新状态和历史,确保数据库与 Redis 一致
Q3: 状态机性能如何?
A:
- ✅ Redis 查询延迟:毫秒级(< 10ms),两种模式均使用 Redis 作为状态存储
- ✅ 缓存预热机制:启动时自动从数据库加载,避免冷启动问题
- ✅ 异步持久化:数据库写入异步进行,不影响主流程性能
- REDIS 模式:通过 Redis Stream 异步持久化,支持多线程并发消费,吞吐量高
- ASYNC_THREAD 模式:通过虚拟线程池异步持久化,适合 I/O 密集型任务,资源利用率高
- ✅ 批量处理:两种模式均支持批量写入数据库,减少数据库压力
- REDIS 模式:Redis Stream 消费者支持批量处理,使用 ThreadLocal 缓冲区批量收集消息
- ASYNC_THREAD 模式:使用共享缓冲区(ConcurrentLinkedQueue)批量累积,达到批量大小时自动刷写
Q4: 使用云服务商的 PAAS Redis 时,REDIS 模式是否会有问题?
A:
⚠️ 重要提示:部分云服务商的 PAAS 版 Redis 对 Redis Stream(MQ)功能的支持尚不完整,可能会导致以下问题:
- 消息消费丢失:Redis Stream 消息可能无法正常消费,导致数据库持久化失败
- 消费组异常:消费组功能可能不稳定,导致多实例部署时出现重复消费或消息丢失
- ACK 机制失效:消息确认机制可能不完整,导致消息重复处理或丢失
解决方案:
如遇到上述问题,请切换至 ASYNC_THREAD 模式:
platform:
component:
statemachine:
storage-type: ASYNC_THREAD # 切换为 ASYNC_THREAD 模式
thread-pool:
enabled: true
batch-size: 200
buffer-capacity: 10000
flush-interval-ms: 2000
ASYNC_THREAD 模式不依赖 Redis Stream,直接通过虚拟线程池异步持久化到数据库,兼容性更好,适合云服务商 PAAS Redis 环境。
Q5: 如何实现终态不可变更?
A:
- FINAL/ERROR 状态默认不可变更
- 如需重新打开,需在转换定义中设置
attributes.reopen: true - 示例:
transitions:
- name: reopen
fromState: COMPLETED
toState: PENDING
event: REOPEN
attributes:
reopen: true # 允许从终态转换
Q6: 如何实现多状态机管理?
A:
- 一个业务对象可以管理多个状态维度(如订单状态、支付状态、物流状态)
- 每个状态维度使用独立的状态机配置
- 使用不同的状态机名称区分不同的状态维度
Q7: MVEL 表达式中如何使用上下文数据?
A:
- 使用
context.getAttribute(key)获取上下文属性 - 使用
context.setAttribute(key, value)设置上下文属性 - 示例:
condition: "context.getAttribute('amount', 0) > 100"
action: "context.setAttribute('confirmedTime', java.time.LocalDateTime.now())"
MVEL 表达式使用:
MVEL 表达式用于条件判断和动作执行,可通过 context 访问上下文:
可用变量:
context:完整上下文对象(StateContext)currentState:当前状态字符串previousState:前一状态字符串event:当前触发的事件名context.getAttribute(key):获取上下文属性context.setAttribute(key, value):设置上下文属性
条件表达式示例:
condition: "context.getAttribute('amount') != null && (Double)context.getAttribute('amount') > 0"
condition: "context.getAttribute('operatorRole') == 'ADMIN'"
condition: "(Boolean)context.getAttribute('canCancel')"
动作表达式示例:
action: "context.setAttribute('confirmedTime', new java.time.LocalDateTime())"
action: "context.setAttribute('operator', context.getAttribute('operator'))"
枚举(必须与 YAML/JSON 名称一致)
- 目的:提供编译期约束,避免字符串拼写错误;同时便于 IDE 自动完成。
- 要求:枚举名称需与 YAML/JSON 中的
states[].name与transitions[].event完全一致;状态机名称也建议使用枚举以匹配静态外观签名。
// 状态机名称(建议使用枚举以匹配 StateMachine 静态门面签名)
public enum PaymentSm { payment }
public enum PaymentEvent { START_PAYMENT, PAYMENT_SUCCESS, PAYMENT_FAILED, REOPEN }
public enum PaymentState { PENDING, PROCESSING, SUCCESS, FAILED, REOPENED }
使用静态外观类 StateMachine(推荐)
import cn.com.rydeen.component.statemachine.StateMachine;
// 1) 触发状态变更(基础版)
StateMachine.fire(PaymentSm.payment, PaymentEvent.START_PAYMENT, orderId);
// 2) 触发状态变更(带上下文属性)
Map<String, Object> attrs = Map.of("operator", userId, "channel", "WEB");
StateMachine.fire(PaymentSm.payment, PaymentEvent.PAYMENT_SUCCESS, orderId, attrs);
// 3) 触发状态变更(显式传入当前状态,避免一次读取)
StateMachine.fire(PaymentSm.payment, PaymentEvent.PAYMENT_FAILED, orderId, PaymentState.PROCESSING);
// 4) 触发状态变更(同时具备上下文与当前状态,低延迟路径)
StateMachine.fire(PaymentSm.payment, PaymentEvent.REOPEN, orderId, attrs, PaymentState.SUCCESS);
// 5) 获取当前状态(字符串)
String current = StateMachine.getCurrentState(PaymentSm.payment, orderId);
// 6) 获取当前状态(枚举)
PaymentState currEnum = StateMachine.getCurrentState(PaymentSm.payment, orderId, PaymentState.class);
// 7) 能否触发某事件(按钮可用/前置校验)
boolean can = StateMachine.canTransitionTo(PaymentSm.payment, PaymentEvent.PAYMENT_SUCCESS, orderId);
// 8) 获取状态历史(审计/追踪)
List<StateHistory> history = StateMachine.getStateHistory(PaymentSm.payment, orderId);
使用时机建议:
- 使用 1):简单触发,不关心上下文,读一次当前状态可接受。
- 使用 2):条件/动作依赖上下文(操作者、原因、来源渠道等)。
- 使用 3):业务方已持有当前状态,需规避一次读开销(如内存缓存命中)。
- 使用 4):对延迟敏感,且既需要上下文又已知当前状态。
getCurrentState:展示/判断当前状态,或作为多状态联合检索的输入。canTransitionTo:前端按钮可点击态、服务端幂等校验/灰度放行前置判断。getStateHistory:问题排查、用户可视化时间线、审计导出。
条件表达式与动作表达式(MVEL)使用指南
MVEL 用于解析 YAML/JSON 中每条 transition 的 condition 与 action。表达式可使用以下变量(由 StateContext 注入):
context:完整上下文对象(可读写attributes)currentState:当前状态字符串previousState:前一状态字符串event:当前触发的事件名- 通过
attributes注入的键值对(如operator、channel、amount等)
示例变量注入(业务触发时传入 attributes):
Map<String, Object> attrs = Map.of(
"operator", userId,
"operatorRole", role,
"channel", "WEB",
"amount", orderAmount
);
StateMachine.fire(PaymentSm.payment, PaymentEvent.PAYMENT_SUCCESS, orderId, attrs);
常见条件表达式示例(condition)
transitions:
- name: pay_success_admin_only
fromState: PROCESSING
toState: SUCCESS
event: PAYMENT_SUCCESS
# 仅管理员或金额小于等于 500 的场景允许成功
condition: "operatorRole == 'ADMIN' || amount <= 500"
- name: reopen_window
fromState: SUCCESS
toState: REOPENED
event: REOPEN
# 完成后 24 小时内允许重开
condition: "context.getAttribute('completedAt') != null &&
java.time.Duration.between(
(java.time.LocalDateTime)context.getAttribute('completedAt'),
java.time.LocalDateTime.now()
).toHours() <= 24"
- name: guarded_transition
fromState: PENDING
toState: PROCESSING
event: START_PAYMENT
# 需要渠道为 WEB 且金额>0
condition: "channel == 'WEB' && amount > 0"
说明:表达式返回 true 才会放行;表达式异常按不满足处理(视为 false)。
常见动作表达式示例(action)
动作在条件通过后执行,适合设置上下文标记、采集审计信息、轻量副作用(不建议在表达式中做外部 IO)。
transitions:
- name: stamp_complete_time
fromState: PROCESSING
toState: SUCCESS
event: PAYMENT_SUCCESS
condition: "amount > 0"
# 将业务完成时间放入上下文,消费者可据此写入 updated_at
action: "context.setAttribute('completedAt', java.time.LocalDateTime.now())"
- name: mark_reason
fromState: PROCESSING
toState: FAILED
event: PAYMENT_FAILED
action: "context.setAttribute('failReason', 'GATEWAY_TIMEOUT')"
- name: enrich_audit
fromState: PENDING
toState: PROCESSING
event: START_PAYMENT
action: "context.setAttribute('audit', 'start by ' + operator + ' via ' + channel)"
失败行为与幂等说明
- 条件为
false或执行异常:不触发转换,返回失败结果;不会写入current/history、不会发布 Spring 事件与 Redis 同步消息。 - 无匹配转换:直接失败("没有找到匹配的转换规则")。
- 成功路径:引擎会写入当前状态,按需写入历史与发布事件/同步消息;
updated_at由消费者按历史业务时间刷新。
最佳实践
- 表达式只做"轻逻辑":复杂校验与外部依赖下沉到代码,表达式聚焦布尔门禁与轻量动作。
- 避免 IO:动作里不要做 RPC/DB/网络访问;可设置
context.attributes供后续链路使用。 - 显式可读:优先使用简单可读的条件;多条件用括号明确优先级。
- 安全输入:仅暴露可信数据到
attributes;不要把原始外部输入直接暴露给表达式。
持久化与一致性
- 引擎在状态变更成功后,发布
StateSyncMessage(syncKey)到 Redis Stream(只包含同步键,如payment:ORDER123)。 - 消费者从 Redis 读取"当前最新状态 + 历史",按批次落库:
- 当前表:
statemachine_state_current(state_machine, business_id, current_state, updated_at) - 历史表:
statemachine_state_history(id, state_machine, business_id, prev_state, curr_state, event_name, occurred_at)
- 当前表:
updated_at采用业务含义时间:取该业务键最新一条历史记录的occurred_at;若不存在历史则回退到LocalDateTime.now()。
完结状态不可变更(支持白名单重开)
原则
- 类型为
FINAL或ERROR的状态视为"完结状态",默认不再允许普通转换离开。 - 仅当显式声明"重开/撤销"类转换时,才允许从完结状态跳转(白名单)。
- 在转换定义中设置
attributes.reopen: true即可允许从 FINAL/ERROR 状态转换。
YAML 约定(示例)
示例一:支付退款场景
name: payment
states:
- name: SUCCESS
type: FINAL
- name: FAILED
type: ERROR
- name: REFUNDING
type: NORMAL
- name: REFUNDED
type: FINAL
transitions:
# 正常流转到终态
- name: payment_success
fromState: PROCESSING
toState: SUCCESS
event: PAYMENT_SUCCESS
# 显式重开(白名单转换):从支付成功状态开始退款
- name: start_refund
fromState: SUCCESS
toState: REFUNDING
event: START_REFUND
condition: "context.getAttribute('canRefund') != null && (Boolean)context.getAttribute('canRefund')"
# 关键:设置 attributes.reopen: true 允许从终态转换
attributes:
reopen: true
action: "context.setAttribute('refundStartTime', new java.time.LocalDateTime())"
示例二:订单退单场景(已完成订单退单)
name: order
states:
- name: PENDING
type: INITIAL
- name: CONFIRMED
type: NORMAL
- name: COMPLETED
type: FINAL # 已完成状态(终态)
- name: CLOSED
type: FINAL # 订单关闭状态(退单后的终态)
transitions:
# 正常流转到完成状态
- name: complete
description: 完成订单
fromState: CONFIRMED
toState: COMPLETED
event: COMPLETE
action: "context.setAttribute('completedTime', new java.time.LocalDateTime())"
# 已完成订单退单:需要设置 reopen: true 允许从终态转换
- name: refund_from_completed
description: 已完成订单退单
fromState: COMPLETED # FINAL 状态
toState: CLOSED
event: REFUND
# 关键:设置 attributes.reopen: true 允许从终态转换
attributes:
reopen: true
# 可选:添加条件判断,比如只能在一定时间内退单
condition: "context.getAttribute('canRefund') != null && (Boolean)context.getAttribute('canRefund')"
action: "context.setAttribute('refundTime', new java.time.LocalDateTime()); context.setAttribute('refundReason', context.getAttribute('reason'))"
使用方式(订单退单示例)
@Service
public class OrderService {
/**
* 已完成订单退单
*/
public void refundCompletedOrder(String orderId, boolean canRefund, String reason) {
Map<String, Object> attributes = new HashMap<>();
attributes.put("canRefund", canRefund);
attributes.put("reason", reason);
attributes.put("operator", "admin");
StateTransitionResult result = StateMachine.fire(
OrderSm.order, // 状态机名称
OrderEvent.REFUND, // 退单事件
orderId, // 订单ID
attributes // 上下文属性
);
if (result.isSuccess()) {
log.info("订单退单成功: {}", orderId);
} else {
throw new RuntimeException("订单退单失败: " + result.getErrorMessage());
}
}
}
关键点说明
attributes.reopen: true必须在转换定义中设置,而不是在调用时传入。- 该设置允许从 FINAL/ERROR 状态转换,是白名单机制。
- 可配合条件表达式,控制何时允许退单(如时间限制、权限检查等)。
- 引擎会检查:当前状态是否为 FINAL/ERROR,匹配的转换是否设置了
attributes.reopen: true,只有设置了reopen: true的转换才会被允许执行。
引擎行为
- 变更前校验:若当前为完结状态且本次事件未标记为
attributes.reopen: true,则直接拒绝(fail fast)。 - 一致性保障:在写入 Redis 前做原子校验(如 Lua/CAS),避免并发覆写完结状态。
- 审计:拒绝的事件可记录到日志或历史,便于定位误触。
命名与唯一性约定(state_machine 与 business_id)
命名空间化推荐
- 使用有语义的全局唯一
state_machine名称以区分业务与维度:- 订单相关:
order.payment、order.fulfillment、order.delivery - 支付信息表:
payinfo.payment
- 订单相关:
- 这样
(state_machine, business_id)足以唯一定位同一业务 ID 在不同维度下的状态。
示例:命名空间化的多状态机 YAML
# src/main/resources/statemachine/order-payment.yml
name: order.payment
states:
- { name: PENDING, type: INITIAL }
- { name: SUCCESS, type: FINAL }
transitions:
- { name: start, fromState: PENDING, toState: SUCCESS, event: START }
# src/main/resources/statemachine/order-fulfillment.yml
name: order.fulfillment
states:
- { name: PENDING, type: INITIAL }
- { name: READY, type: NORMAL }
- { name: COMPLETED, type: FINAL }
transitions:
- { name: confirm, fromState: PENDING, toState: READY, event: CONFIRM }
- { name: complete, fromState: READY, toState: COMPLETED, event: COMPLETE }
# src/main/resources/statemachine/payinfo-payment.yml
name: payinfo.payment
states:
- { name: PENDING, type: INITIAL }
- { name: SUCCESS, type: FINAL }
transitions:
- { name: pay_ok, fromState: PENDING, toState: SUCCESS, event: PAYMENT_SUCCESS }
若无法保证命名唯一(可选扩展)
- 扩展列方案(需要修改表与唯一约束):新增
aggregate_type/business_type,主键改为(state_machine, aggregate_type, business_id);历史表的唯一约束同步扩展。
示例:可选扩表示意(DDL 方向,非默认)
# 仅示意:如需扩展,请在自有模块维护迁移
- addColumn:
tableName: statemachine_state_current
columns:
- column:
name: aggregate_type
type: varchar(64)
remarks: 业务聚合类型(如 order、payinfo)
- dropPrimaryKey:
tableName: statemachine_state_current
- addPrimaryKey:
tableName: statemachine_state_current
columnNames: state_machine, aggregate_type, business_id
constraintName: pk_statemachine_state_current_ext
表结构与注释(由 Liquibase 维护)
statemachine_state_current- 复合主键:(state_machine, business_id)
updated_at默认CURRENT_TIMESTAMP,更新时按业务发生时间覆盖。
statemachine_state_history- 唯一约束:(state_machine, business_id, occurred_at)
- 常用索引:业务维度与时间维度(见
003-indexes.yaml)。
Redis Stream 运维
- 批量拉取:
count可配置;并发:concurrency建议=CPU核心数。 - 清理:
- 全局
cleanup.default-max-len;或在消费者配置max-len覆盖。 - 使用
XTRIM MAXLEN ~近似清理,性能友好;无需长度预检。 - 服务级分布式锁:同一服务的多个实例仅一个执行清理,不同服务互不影响。
- 全局
筛选与分页(订单场景)
单状态筛选与分页(最常见)
- 需求:按某一状态机维度(如
order.payment)的状态与更新时间过滤订单,并分页返回。 - 推荐索引:
statemachine_state_current(state_machine, current_state, updated_at, business_id)
写法一:JOIN(直观)
SELECT o.*
FROM orders o
JOIN statemachine_state_current s
ON s.business_id = o.order_id
AND s.state_machine = 'order.payment'
AND s.current_state IN ('SUCCESS')
WHERE s.updated_at >= :from
ORDER BY o.created_at DESC
LIMIT :limit OFFSET :offset;
写法二:EXISTS(大表友好)
SELECT o.*
FROM orders o
WHERE EXISTS (
SELECT 1 FROM statemachine_state_current s
WHERE s.business_id = o.order_id
AND s.state_machine = 'order.payment'
AND s.current_state IN ('SUCCESS')
AND s.updated_at >= :from
)
ORDER BY o.created_at DESC
LIMIT :limit OFFSET :offset;
写法三:两步法(先取ID,再回表;分页最稳)
-- Step 1: 先在状态表分页取 business_id
SELECT s.business_id
FROM statemachine_state_current s
WHERE s.state_machine = 'order.payment'
AND s.current_state IN ('SUCCESS')
AND s.updated_at >= :from
ORDER BY s.updated_at DESC
LIMIT :limit OFFSET :offset;
-- Step 2: 回表订单
SELECT o.*
FROM orders o
WHERE o.order_id IN (:businessIds)
ORDER BY o.created_at DESC;
提示:MyBatis Plus 可直接两步实现:Mapper A 查 business_id 列表,Mapper B 用 IN 分页回表;或用 XML 自定义 JOIN/EXISTS。
按多状态筛选与分页
- 业务需求:按两个(或多个)状态机维度过滤订单,并结合状态时间或订单时间分页返回。
- 推荐索引(见
003-indexes.yaml):在statemachine_state_current建立复合索引(state_machine, current_state, updated_at, business_id)- 仅按时间筛选时可用
(state_machine, updated_at, business_id)
方案一:自连接 current 表(直观)
SELECT o.*
FROM orders o
JOIN statemachine_state_current s_pay
ON s_pay.business_id = o.order_id
AND s_pay.state_machine = 'order.payment'
AND s_pay.current_state IN ('SUCCESS')
JOIN statemachine_state_current s_fulfill
ON s_fulfill.business_id = o.order_id
AND s_fulfill.state_machine = 'order.fulfillment'
AND s_fulfill.current_state IN ('READY','COMPLETED')
WHERE s_pay.updated_at >= :pay_from
AND s_fulfill.updated_at >= :fulfill_from
ORDER BY o.created_at DESC
LIMIT :limit OFFSET :offset;
适用:状态维度较少;需要一次性回表并排序。注意使用上面的复合索引避免放大扫描。
方案二:双 EXISTS(避免结果集放大)
SELECT o.*
FROM orders o
WHERE EXISTS (
SELECT 1 FROM statemachine_state_current s
WHERE s.business_id = o.order_id
AND s.state_machine = 'order.payment'
AND s.current_state IN ('SUCCESS')
AND s.updated_at >= :pay_from
)
AND EXISTS (
SELECT 1 FROM statemachine_state_current s
WHERE s.business_id = o.order_id
AND s.state_machine = 'order.fulfillment'
AND s.current_state IN ('READY','COMPLETED')
AND s.updated_at >= :fulfill_from
)
ORDER BY o.created_at DESC
LIMIT :limit OFFSET :offset;
适用:订单表大、单条匹配率低;EXISTS 可良好利用索引并短路。
方案三:两步法(先取ID交集,再回表;分页最稳定)
-- Step 1: 取符合两个状态条件的业务ID(仅取一页)
WITH ids AS (
SELECT s1.business_id
FROM statemachine_state_current s1
JOIN statemachine_state_current s2
ON s2.business_id = s1.business_id
AND s2.state_machine = 'order.fulfillment'
AND s2.current_state IN ('READY','COMPLETED')
AND s2.updated_at >= :fulfill_from
WHERE s1.state_machine = 'order.payment'
AND s1.current_state IN ('SUCCESS')
AND s1.updated_at >= :pay_from
ORDER BY GREATEST(s1.updated_at, s2.updated_at) DESC
LIMIT :limit OFFSET :offset
)
SELECT o.*
FROM orders o
JOIN ids ON ids.business_id = o.order_id
ORDER BY o.created_at DESC;
适用:需要严格的跨表时间排序或减少一次 JOIN 的列传输;亦便于应用层用 MyBatis Plus 先查 business_id 列表,再分批回表。
索引与实现提示
statemachine_state_current- 建议索引名:
idx_current_sm_state_updated_bid(state_machine, current_state, updated_at, business_id) - 备选:
idx_current_sm_updated_bid(state_machine, updated_at, business_id)
- 建议索引名:
statemachine_state_history(如需时间回溯查询)idx_history_sm_occurred_bid(state_machine, occurred_at, business_id)
- MyBatis Plus:
- JOIN/EXISTS 建议使用 Mapper XML,自定义 SQL;
- 或采用"两步法":先查交集
business_id,再IN回表分页(注意批量大小与顺序稳定性)。
多状态机最佳实践
- YAML 管模型、代码管语义:YAML 定义状态/转换,枚举提供编译期约束,两者名称保持一致。
- 以 Redis 为准:任何持久化异常不影响引擎的实时读写;消费者具备幂等与补偿能力。
- 批处理优先:调大
count与并发以提升吞吐,并结合 DB 批量写策略。 - 业务时间优先:确保上游事件产生的业务时间正确写入历史,消费者即可据此刷新
updated_at。 - 终态不可变更:默认锁定
FINAL/ERROR,仅白名单重开;避免"已完成被回滚"。 - 监控与告警:结合 Micrometer/OpenTelemetry 对发布、消费、落库、清理链路打点与追踪。