肖哥弹架构 跟大家“弹弹” Spring Statemachine设计与实战应用,需要代码关注
欢迎 点赞,关注,转发。
关注公号Solomon肖哥弹架构获取更多精彩内容
历史热点文章
- MyCat应用实战:分布式数据库中间件的实践与优化(篇幅一)
- 图解深度剖析:MyCat 架构设计与组件协同 (篇幅二)
- 一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
- 写代码总被Dis:5个项目案例带你掌握SOLID技巧,代码有架构风格
- 里氏替换原则在金融交易系统中的实践,再不懂你咬我
Spring Statemachine 作为复杂业务流程管理的核心组件,其高级功能能够显著提升系统的健壮性和可维护性。本文系统性地解析了四大高级应用:
- 任务调度:通过定时任务实现超时处理、延迟事件触发、周期性状态检查等业务逻辑,提供通用代码示例与典型应用场景。
- 性能监控:针对高并发、复杂流程和分布式系统,介绍指标采集、链路追踪及动态采样等监控技巧,确保状态机高效运行。
- 分布式集成:解析跨服务状态协同、高可用与水平扩展的配置方案,结合 Redis 和分布式锁实现状态一致性。
- 安全控制:基于 Spring Security 实现角色权限、动态规则与操作审计,保障状态转换的合规性与防篡改能力。
1. 状态机任务调度
1.2 状态机任务调度通用示例
public class StateMachineScheduler {
@Autowired
private StateMachine<String, String> stateMachine;
@Scheduled(fixedRate = 5000)
public void scheduleStateMachineTask() {
if (stateMachine.getState().getId().equals("WAITING")) {
stateMachine.sendEvent("TIMEOUT_EVENT");
}
}
}
1.2 状态机任务调度场景
1.2.1. 超时自动处理
- 典型场景:
- 订单支付超时(
WAITING_PAYMENT → CANCELLED
) - 审批流程超时(
PENDING_APPROVAL → AUTO_REJECTED
)
- 订单支付超时(
- 实现逻辑:
@Scheduled(fixedDelay = 30000) // 每30秒扫描一次 void checkTimeoutOrders() { List<Order> timeoutOrders = orderRepo.findByStateAndCreateTimeBefore( "WAITING_PAYMENT", LocalDateTime.now().minusMinutes(30)); // 30分钟未支付 timeoutOrders.forEach(order -> stateMachine.sendEvent(order.getId(), "PAYMENT_TIMEOUT")); }
1.2.2. 延迟事件触发
- 典型场景:
- 发货后延迟24小时自动确认收货(
SHIPPED → COMPLETED
) - 试用期到期自动转正式(
TRIAL → OFFICIAL
)
- 发货后延迟24小时自动确认收货(
- 实现逻辑:
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行 void autoCompleteDeliveries() { List<Delivery> deliveries = deliveryRepo.findByStateAndShippedTimeBefore( "SHIPPED", LocalDateTime.now().minusDays(1)); // 发货超过24小时 deliveries.forEach(delivery -> stateMachine.sendEvent(delivery.getId(), "AUTO_CONFIRM")); }
1.2.3. 周期性状态检查
- 典型场景:
- 每5分钟检查库存预占状态(
RESERVED → RELEASED
若未支付) - 定期扫描待重试任务(
FAILED → RETRYING
)
- 每5分钟检查库存预占状态(
- 实现逻辑:
@Scheduled(fixedRate = 300000) // 5分钟间隔 void checkInventoryReservation() { inventoryRepo.findByStateAndReservedTimeBefore( "RESERVED", LocalDateTime.now().minusMinutes(10)) // 预占超过10分钟 .forEach(item -> stateMachine.sendEvent(item.getId(), "RESERVATION_EXPIRE")); }
1.2.4. 补偿性任务
- 典型场景:
- 处理卡在中间状态的异常流程(如
PROCESSING → FAILED
) - 补发丢失的事件(如MQ消息消费失败后的重试)
- 处理卡在中间状态的异常流程(如
- 实现逻辑:
@Scheduled(initialDelay = 10000, fixedRate = 60000) // 启动延迟10秒,每分钟执行 void recoverStuckProcesses() { processRepo.findByStateAndUpdateTimeBefore( "PROCESSING", LocalDateTime.now().minusHours(1)) // 超过1小时未更新 .forEach(process -> stateMachine.sendEvent(process.getId(), "FORCE_RETRY")); }
1.2.5. 状态同步与一致性维护
- 典型场景:
- 定期同步分布式系统的状态差异
- 修复因系统崩溃导致的状态不一致
- 实现逻辑:
@Scheduled(cron = "0 */10 * * * ?") // 每10分钟执行 void syncDistributedStates() { List<Payment> payments = paymentRepo.findByState("PROCESSING"); payments.forEach(payment -> { PaymentStatus externalStatus = paymentService.getExternalStatus(payment.getId()); if ("SUCCESS".equals(externalStatus)) { stateMachine.sendEvent(payment.getId(), "EXTERNAL_CONFIRM"); } }); }
2. 状态机性能监控
2.1何时需要状态机性能监控?
2.1.1. 高并发流程控制
- 场景:电商秒杀、支付清算等高频状态转换场景
- 监控重点:
- 单个状态转换耗时(
transition.time
) - 并发事件处理吞吐量(
events.processed.count
)
- 单个状态转换耗时(
2.1.2. 复杂业务状态机
- 场景:多级审批流、保险理赔流程等包含大量状态和守卫条件的系统
- 监控重点:
- 守卫条件执行时间(
guard.evaluation.time
) - 状态停留时长(
state.duration
)
- 守卫条件执行时间(
2.1.3. 超时敏感型业务
- 场景:金融交易(如30秒内必须完成支付状态变更)
- 监控重点:
- 状态转换SLA达标率(如
transition.time > 30s
的异常计数)
- 状态转换SLA达标率(如
2.1.4. 分布式系统调试
- 场景:跨服务的状态同步(如订单→库存→物流)
- 监控重点:
- 跨服务事件延迟(
external.event.latency
) - 状态一致性校验失败次数(
state.mismatch.count
)
- 跨服务事件延迟(
2.1.5. 资源瓶颈诊断
- 场景:状态机实例池耗尽、线程阻塞等问题
- 监控重点:
- 状态机实例使用率(
instance.usage.ratio
) - 事件队列积压量(
event.queue.size
)
- 状态机实例使用率(
2.2 如何实施监控
2.2.1. 基础指标采集
// 注册到Prometheus的监控配置
@Bean
StateMachineMonitor<String, String> monitor(MeterRegistry registry) {
return new StateMachineMonitor<String, String>() {
// 记录状态转换耗时
@Override
public void transition(StateMachine<String, String> sm,
Transition<String, String> transition, long duration) {
Timer.builder("statemachine.transition.time")
.tags("source", transition.getSource().getId(),
"target", transition.getTarget().getId(),
"status", transition.getKind().name())
.register(registry)
.record(duration, TimeUnit.MILLISECONDS);
}
// 记录事件处理异常
@Override
public void eventNotAccepted(Message<String> event) {
Counter.builder("statemachine.event.rejected")
.tag("event", event.getPayload())
.register(registry)
.increment();
}
};
}
2.2.2. 关键监控指标清单
指标名称 | 类型 | 用途 |
---|---|---|
statemachine.transition.time | Timer | 状态转换耗时分布(分状态统计) |
statemachine.guard.time | Timer | 守卫条件执行时间 |
statemachine.state.duration | Gauge | 各状态停留时长(用于发现卡住的状态) |
statemachine.event.queue.size | Gauge | 待处理事件积压量 |
statemachine.error.count | Counter | 状态机异常次数(按异常类型分类) |
2.3 高级监控技巧
2.3.1. 上下文增强监控
// 在监控器中记录业务ID等上下文
transition(sm, transition, duration) {
String orderId = sm.getExtendedState().get("orderId", String.class);
registry.timer("statemachine.transition.time")
.tag("orderType", getOrderType(orderId)) // 自定义业务标签
.record(duration);
}
2.3.2. 链路追踪集成
// 与OpenTelemetry结合
transition(sm, transition, duration) {
Span span = tracer.spanBuilder("stateTransition")
.setAttribute("sourceState", transition.getSource().getId())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 状态转换逻辑
} finally {
span.end();
}
}
2.3.3. 动态采样控制
# 通过配置控制监控粒度
statemachine:
monitoring:
sampling-rate: 0.1 # 10%的请求上报详细指标
enable-heavy-guards-monitoring: true
3. 分布式状态机
3.1 何时需要分布式状态机?
3.1.1. 跨服务状态协同
- 场景:订单状态变更需同步触发库存锁定、物流调度等跨系统操作
- 问题解决:确保多个服务间的状态变更原子性(如订单支付成功时,必须同时扣减库存)
3.1.2. 高可用需求
- 场景:金融交易系统要求 7x24 小时不间断运行
- 问题解决:状态机实例崩溃后,其他节点可接管并恢复状态
3.1.3. 水平扩展
- 场景:大促期间需要动态扩容处理海量订单状态流转
- 问题解决:多个节点共同处理状态事件,通过分布式锁避免冲突
3.2 开启分布式状态机
3.2.1. 基础配置
@Configuration
@EnableStateMachine
public class DistributedConfig extends StateMachineConfigurerAdapter<String, String> {
@Autowired
private StateMachineRuntimePersister<String, String, String> persister;
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withDistributed()
.enabled(true) // 启用分布式模式
.runtimePersister(persister); // 设置状态持久化器
}
}
3.2.2. 必须实现的组件
组件 | 作用 | 推荐实现方案 |
---|---|---|
StateMachineRuntimePersister | 持久化状态机运行时数据(状态、上下文等) | Redis/JDBC/Zookeeper |
分布式锁 | 保证同一状态机实例在集群中只有一个节点能处理事件 | Redisson/Apache Curator |
事件广播 | 跨节点同步状态变更事件 | Spring Cloud Stream/Kafka |
3.2.3. 配置示例
/**
* 分布式状态机配置类
* 1. 配置Redis作为状态机上下文的持久化存储
* 2. 构建支持分布式的状态机实例
*/
@Configuration
public class DistributedStateMachineConfig {
/**
* 创建Redis状态机上下文持久化器
* @param connectionFactory Redis连接工厂(Spring Boot自动配置)
* @return 持久化器实例,用于保存/恢复状态机运行时上下文
*/
@Bean
public StateMachineRuntimePersister<String, String, String> redisPersister(
RedisConnectionFactory connectionFactory) {
return new RedisStateMachineContextRepository<>(connectionFactory)
// 设置Redis键前缀,避免与其他业务数据冲突
// 格式:sm:<状态机ID>:context
.withKeyPrefix("sm:")
// 使用Jackson实现JSON序列化
// 注意:需确保状态/事件类型可序列化
.withSerializationContext(
new Jackson2JsonSerializationContext()
);
}
/**
* 创建分布式状态机实例
* @param factory 状态机工厂(Spring自动注入)
* @param persister 上文定义的持久化器
* @return 支持分布式协作的状态机实例
*/
@Bean
public DistributedStateMachine<String, String> distributedStateMachine(
StateMachineFactory<String, String> factory,
StateMachineRuntimePersister<String, String, String> persister) {
return new DistributedStateMachine<>(
// 基础状态机工厂,用于创建原始状态机实例
factory,
// 状态持久化器,实现集群间状态同步
persister,
// 分布式锁服务(使用Redisson实现)
// 保证同一状态机实例在集群中只有一个节点能处理事件
new RedissonDistributedLockService() {
@Override
public DistributedLock getLock(String stateMachineId) {
// 示例:为每个状态机实例创建独立的锁
// 实际使用需配置RedissonClient
return redissonClient.getLock("sm-lock:" + stateMachineId);
}
}
);
}
}
关键组件详解:
RedisStateMachineContextRepository
配置方法 | 作用 |
---|---|
withKeyPrefix("sm:") | 定义Redis键前缀,避免键冲突(最终键格式:sm:order123:context ) |
withSerializationContext() | 指定序列化方案,Jackson支持复杂对象的序列化/反序列化 |
DistributedStateMachine
构造参数
参数 | 类型 | 职责 |
---|---|---|
factory | StateMachineFactory | 创建基础状态机实例,包含状态/事件定义等原始配置 |
persister | StateMachineRuntimePersister | 实现状态机上下文的跨节点持久化(核心方法:save() /restore() ) |
lockService | DistributedLockService | 提供分布式锁,确保同一状态机实例的事件处理互斥(如Redisson/Z |
5. 安全集成
5.1 何时需要使用状态机安全集成?
5.1.1. 操作权限控制
- 场景:不同角色对状态转换的权限不同
- 例如:只有「财务专员」能触发
APPROVE_PAYMENT
事件
- 例如:只有「财务专员」能触发
- 传统问题:在业务代码中写大量
if-else
权限判断
5.1.2. 数据敏感操作
- 场景:关键状态变更需二次验证
- 例如:从
PRODUCTION
状态切换到MAINTENANCE
需要管理员+动态令牌
- 例如:从
5.1.3. 合规性要求
- 场景:满足审计要求(如SOX、GDPR)
- 需要记录「谁在什么时间触发了什么状态变更」
5.1.4. 防篡改保护
- 场景:防止恶意用户直接调用API强制修改状态
- 例如:禁止从
CANCELLED
状态直接跳回PAID
- 例如:禁止从
5.2 如何配置安全集成?
5.2.1. 基础配置(基于Spring Security)
/**
* 状态机安全配置类
* 功能:集成Spring Security实现状态转换的细粒度权限控制
* 核心能力:
* 1. 基于角色的事件触发权限控制
* 2. 支持SpEL表达式的复杂安全规则
* 3. 审计日志记录
*/
@Configuration
@EnableStateMachine // 启用Spring状态机自动配置
public class SecurityConfig extends StateMachineConfigurerAdapter<String, String> {
/**
* 配置状态机安全特性
*/
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withSecurity()
.enabled(true) // 必须显式启用安全模块,默认关闭
/**
* 设置访问决策管理器:
* - 决定是否允许状态转换
* - 可组合多个AccessDecisionVoter实现
*/
.transitionAccessDecisionManager(accessDecisionManager())
/**
* 设置事件安全元数据源:
* - 定义事件与安全规则的映射关系
* - 支持动态从数据库加载规则
*/
.eventSecurityMetadataSource(eventSecurityMetadataSource());
}
/**
* 创建访问决策管理器Bean
* 采用AffirmativeBased策略(任一投票器通过即放行)
* 可选策略:
* - ConsensusBased(多数通过)
* - UnanimousBased(全票通过)
*/
@Bean
public TransitionAccessDecisionManager accessDecisionManager() {
return new AffirmativeBased(Arrays.asList(
new RoleVoter(), // 基于角色的投票器(检查hasRole表达式)
new ExpressionVoter() // 支持SpEL表达式的投票器
));
}
/**
* 创建事件安全规则源
* 示例采用静态规则配置,实际生产建议:
* 1. 从数据库动态加载
* 2. 结合缓存提高性能
*/
@Bean
public EventSecurityMetadataSource eventSecurityMetadataSource() {
return new MapEventSecurityMetadataSource(
Map.ofEntries(
// 审批事件需要FINANCE_MANAGER角色
entry("APPROVE", "hasRole('FINANCE_MANAGER')"),
// 删除事件需要同时满足:
// 1. ADMIN角色
// 2. 来自192.168.1.0/24网段
entry("DELETE", "hasRole('ADMIN') && hasIpAddress('192.168.1.0/24')")
)
);
}
}
关键组件说明:
组件 | 作用 | 实现建议 |
---|---|---|
TransitionAccessDecisionManager | 最终决策是否允许状态转换 | 常用AffirmativeBased (一票通过即允许) |
EventSecurityMetadataSource | 定义事件与安全规则的映射关系 | 可从数据库加载动态规则 |
StateMachineSecurityInterceptor | 实际执行安全拦截的AOP切面(自动注入) | 通常无需手动配置 |
5.2.2. 扩展配置选项
config.withSecurity()
.runAsManager(runAsManager()) // 临时切换执行身份
.afterTransitionCall(auditService::logTransition); // 安全审计回调
5.3 实际应用
5.3.1. 基于角色的权限控制
// 在状态机配置中定义守卫条件+安全规则
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) {
transitions
.withExternal()
.source("DRAFT")
.target("SUBMITTED")
.event("SUBMIT")
.guard(securityGuard.requirePermission("ORDER_SUBMIT"));
}
// 自定义Guard实现
@Component
public class SecurityGuard {
public Guard<String, String> requirePermission(String permission) {
return ctx -> SecurityContextHolder.getContext()
.getAuthentication()
.getAuthorities()
.stream()
.anyMatch(a -> a.getAuthority().equals(permission));
}
}
5.3.2. 动态权限规则(从数据库加载)
@Bean
public EventSecurityMetadataSource dynamicMetadataSource(RuleRepository repo) {
return machineId -> repo.findRulesByMachineId(machineId).stream()
.collect(Collectors.toMap(
Rule::getEvent,
Rule::getExpression
));
}
5.3.3. 操作审计集成
@Bean
public TransitionAuditListener auditListener() {
return new TransitionAuditListener() {
@Override
public void audit(Transition<String, String> transition, Authentication auth) {
auditLog.save(
transition.getSource().getId(),
transition.getTarget().getId(),
auth.getName(),
LocalDateTime.now()
);
}
};
}
5.4 与其他模块的集成
5.4.1. 安全规则设计原则
模式 | 示例表达式 | 适用场景 |
---|---|---|
角色控制 | hasRole('ADMIN') | 基础权限划分 |
时间限制 | isBetween('09:00','18:00') | 防止非工作时间操作 |
IP白名单 | hasIpAddress('192.168.1.100') | 敏感操作限制来源IP |
双因素认证 | hasAuthority('OTP_VERIFIED') | 关键状态变更需二次验证 |
5.4.2. 与Spring Security整合
@PreAuthorize("hasPermission(#machineId, 'START')")
public void startMachine(String machineId) {
stateMachine.start();
}