Skip to main content

状态机组件 (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.enabledbooleantrue是否启用状态机组件
platform.component.statemachine.state-machine.config-pathstringclasspath:statemachine/状态机配置文件路径
platform.component.statemachine.state-machine.enable-historybooleantrue是否启用状态历史记录
platform.component.statemachine.state-machine.enable-eventsbooleantrue是否启用状态变更事件
规则引擎配置​
配置项类型默认值说明
platform.component.statemachine.rules-engine.skip-on-first-failed-rulebooleanfalse是否跳过第一个失败的规则
platform.component.statemachine.rules-engine.skip-on-first-applied-rulebooleanfalse是否跳过第一个应用的规则
platform.component.statemachine.rules-engine.skip-on-first-non-triggered-rulebooleanfalse是否跳过第一个非触发的规则
platform.component.statemachine.rules-engine.rule-priority-thresholdint0规则优先级阈值,只执行优先级 >= 该值的规则
存储配置​
配置项类型默认值说明
platform.component.statemachine.storage-typeenumASYNC_THREAD存储类型:REDIS(Redis 存储,支持多实例部署)或 ASYNC_THREAD(异步线程池,单实例部署)

REDIS 模式配置(storage-type=REDIS 时生效):

配置项类型默认值说明
platform.component.statemachine.redis-storage.key-prefixstringplatform:statemachineRedis 缓存键前缀,用于归类和管理状态机相关的 Redis key
platform.component.statemachine.redis-storage.timeoutlong604800000历史记录过期时间(单位:毫秒),默认7天,仅对历史记录生效
platform.component.statemachine.redis-storage.redis-stream.enabledbooleanfalse是否启用 Redis Stream 数据库持久化(需要 Redis 5.0+)
platform.component.statemachine.redis-storage.redis-stream.batch-sizeint200批量写入数据库的记录数
platform.component.statemachine.redis-storage.cleanup.enabledbooleanfalse是否启用终态自动清理
platform.component.statemachine.redis-storage.cleanup.fixed-delay-mslong3600000清理任务执行间隔(毫秒),默认1小时
platform.component.statemachine.redis-storage.cleanup.ttl-dayslong30终态保留天数
platform.component.statemachine.redis-storage.cleanup.batch-sizeint500单次清理批次大小

ASYNC_THREAD 模式配置(storage-type=ASYNC_THREAD 时生效):

配置项类型默认值说明
platform.component.statemachine.thread-pool.enabledbooleantrue是否启用异步数据库持久化
platform.component.statemachine.thread-pool.batch-sizeint200批量写入数据库的记录数,达到此数量时触发批量写入
platform.component.statemachine.thread-pool.buffer-capacityint10000最大缓冲条数(触发紧急刷写),当缓冲区达到此大小时立即触发刷写
platform.component.statemachine.thread-pool.flush-interval-mslong2000定时刷新间隔(毫秒),定期检查缓冲区,如果有数据则触发刷写
表达式配置​
配置项类型默认值说明
platform.component.statemachine.expression.slow-threshold-mslong1000慢查询阈值(毫秒),超过该时间会记录警告日志
platform.component.statemachine.expression.enable-security-checkbooleantrue是否启用表达式安全检查
platform.component.statemachine.expression.security-blacklistlist[]自定义表达式黑名单(方法名)
platform.component.statemachine.expression.enable-detailed-logbooleanfalse是否启用详细日志(记录所有表达式执行)

创建状态机配置文件​

状态机配置文件使用 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 前做原子校验,避免并发覆写完结状态
  • ✅ 审计记录:拒绝的事件可记录到日志或历史,便于定位误触

状态机工作原理​

状态转换流程​

状态机执行过程如下:

  1. 接收请求:业务代码调用静态外观类 StateMachine.fire()
  2. 获取当前状态:从 Redis 存储中获取当前状态(如果 Redis 为空且启用 DB 持久化,则从数据库加载并回填 Redis)
  3. 查找转换规则:根据当前状态和事件查找匹配的转换
  4. 终态检查:如果当前状态是 FINAL 或 ERROR,仅允许带有 attributes.reopen=true 的转换
  5. 验证条件:使用 MVEL 表达式检查转换条件是否满足(支持安全检查)
  6. 执行转换:更新状态并执行转换动作(MVEL 表达式)
  7. 保存结果:将新状态保存到 Redis(单数据源),并根据 storage-type 配置选择异步持久化方式
    • REDIS 模式:投递到 Redis Stream 用于异步持久化
    • ASYNC_THREAD 模式:提交到虚拟线程池用于异步持久化
  8. 返回结果:返回转换成功或失败的结果

状态机架构​

┌─────────────────────────────────────────────────────────────┐
│ 业务代码层 │
│ StateMachine.fire(...) │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────────┐
│ 状态机引擎层 │
│ StateMachineEngine + Easy Rules │
│ - 状态转换规则匹配 │
│ - MVEL 表达式评估(条件/动作) │
│ - 规则优先级处理 │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
┌───────▼────────┐ ┌────────▼────────────────────────┐
│ Redis 存储 │ │ 异步持久化层(两种模式) │
│ (单数据源) │ │ - Redis Stream(REDIS 模式) │
└───────┬────────┘ │ - 虚拟线程池(ASYNC_THREAD 模式) │
│ └────────┬────────────────────────┘
│ │
│ ┌───────▼────────┐
│ │ Stream 消费者 │
│ │ (多线程并发) │
│ └───────┬────────┘
│ │
└────────────┬───────────────┘
│
┌────────▼────────┐
│ 数据库持久化 │
│ (MyBatis Plus) │
└─────────────────┘

核心设计原则​

  1. Redis 作为单数据源(Single Source of Truth)

    • 当前状态和历史记录均先写入 Redis
    • 数据库作为异步持久化副本,通过两种方式异步写入(根据 storage-type 配置选择)
    • 缓存预热机制:启动时如果 Redis 为空,自动从数据库加载并回填 Redis
  2. 双模式异步持久化

    • REDIS 模式:状态变更后直接发布 StateSyncMessage 到 Redis Stream(不通过 Spring 事件),消息仅包含同步键(stateMachineName:businessId),消费者从 Redis 读取最新状态和历史
    • ASYNC_THREAD 模式:状态变更后直接提交同步任务到虚拟线程池,使用共享缓冲区批量累积,从 Redis 读取最新状态和历史
  3. 批量处理优化

    • REDIS 模式:StateSyncStreamConsumer 支持多线程并发处理(可配置 concurrency),使用 ThreadLocal 缓冲区批量收集消息,达到 batchSize 时批量刷写到数据库
    • ASYNC_THREAD 模式:使用共享缓冲区(ConcurrentLinkedQueue)批量累积,达到 batchSize 或 bufferCapacity 时批量刷写,支持定时刷新机制

启动初始化流程图​

业务调用层流程图(静态外观 + Redis Stream 异步持久化)​

查询操作流程图(Redis 缓存预热)​

状态转换详细流程(Redis Stream 异步持久化)​

多实例部署场景下的数据流(Redis Stream 分布式消费)​

组件架构图(Redis Stream 异步持久化)​

关键设计点说明​

  1. Redis 作为单数据源(Single Source of Truth):

    • 当前状态和历史记录均先写入 Redis
    • 数据库作为异步持久化副本,通过两种方式异步写入(根据 storage-type 配置选择)
    • 缓存预热机制:启动时如果 Redis 为空,自动从数据库加载并回填 Redis
  2. 双模式异步持久化:

    方式一: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),使用对应的批量插入或更新语法
    • 单实例内批量处理,不支持多实例部署
  3. 批量写入优化:

    • 批量查询已存在记录,减少数据库查询次数
    • 批量插入和更新,减少数据库压力
    • 通过唯一约束去重,避免重复写入
    • ASYNC_THREAD 模式根据数据库类型自动选择最优的批量写入语法
  4. 历史记录过期:

    • 默认保留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
  • ✅ 合理设置过期时间:历史记录默认保留7天,可根据业务需求调整
  • ✅ 批量大小调优:根据数据库性能和业务量调整批量大小
    • REDIS 模式:调整 redis-storage.redis-stream.batch-size 和 Redis Stream 消费者的 count、concurrency
    • ASYNC_THREAD 模式:调整 thread-pool.batch-size、buffer-capacity、flush-interval-ms

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 对发布、消费、落库、清理链路打点与追踪。