1. 外卖试吃订单的业务场景解析
试吃订单是餐饮行业常见的营销手段,通常由商家发起邀请或用户主动申请参与。与常规外卖订单相比,试吃订单具有以下典型特征:
- 参与资格需审核(用户需满足特定条件)
- 订单状态流转更复杂(包含预约、确认、取消等额外环节)
- 业务规则更灵活(如自动取消未确认的订单)
去年参与某连锁餐饮品牌的中台系统重构时,我们发现其试吃订单模块存在状态管理混乱的问题。例如:
- 用户取消订单后仍能收到备餐通知
- 商家端显示"已送达"的订单在用户端却显示"配送中"
- 超时未确认的订单未自动释放库存
这些问题的本质都是状态流转缺乏严格约束。Spring State Machine(后文简称SSM)恰好能解决这类复杂状态管理问题,其核心优势在于:
- 可视化状态流转定义
- 内置并发安全机制
- 可扩展的监听器机制
2. 状态机建模核心设计
2.1 状态枚举定义
首先需要明确定义所有可能的状态。根据业务需求,我们划分出7个主状态和3个子状态:
java复制public enum OrderStates {
// 主状态
INITIAL, // 初始状态
APPLIED, // 已申请
APPROVED, // 审核通过
REJECTED, // 审核拒绝
CONFIRMED, // 用户确认
PREPARING, // 备餐中
DELIVERING, // 配送中
COMPLETED, // 已完成
CANCELLED; // 已取消
// 子状态(用于复合状态)
public enum ApprovalStates {
PENDING,
MANAGER_APPROVED,
ADMIN_APPROVED
}
}
注意:子状态主要用于需要多级审批的场景,如部分高价试吃品需要店长和管理员双重审批。
2.2 事件触发枚举
定义触发状态转移的事件类型:
java复制public enum OrderEvents {
SUBMIT, // 提交申请
AUTO_EXPIRE, // 自动过期
MANUAL_REVIEW, // 人工审核
USER_CONFIRM, // 用户确认
USER_CANCEL, // 用户取消
MERCHANT_CANCEL, // 商家取消
START_PREPARE, // 开始备餐
FINISH_PREPARE, // 完成备餐
START_DELIVERY, // 开始配送
FINISH_DELIVERY // 完成配送
}
2.3 状态机构建配置
使用SSM的Java配置方式定义状态机:
java复制@Configuration
@EnableStateMachineFactory
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states)
throws Exception {
states
.withStates()
.initial(OrderStates.INITIAL)
.state(OrderStates.APPLIED)
.state(OrderStates.APPROVED)
.state(OrderStates.REJECTED)
.state(OrderStates.CONFIRMED)
.state(OrderStates.PREPARING)
.state(OrderStates.DELIVERING)
.state(OrderStates.COMPLETED)
.state(OrderStates.CANCELLED)
.and()
.withStates()
.parent(OrderStates.APPROVED)
.initial(OrderStates.ApprovalStates.PENDING)
.state(OrderStates.ApprovalStates.MANAGER_APPROVED)
.state(OrderStates.ApprovalStates.ADMIN_APPROVED);
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions)
throws Exception {
transitions
// 申请流程
.withExternal()
.source(OrderStates.INITIAL)
.target(OrderStates.APPLIED)
.event(OrderEvents.SUBMIT)
.and()
// 审核流程
.withExternal()
.source(OrderStates.APPLIED)
.target(OrderStates.APPROVED)
.event(OrderEvents.MANUAL_REVIEW)
.and()
// 用户确认
.withExternal()
.source(OrderStates.APPROVED)
.target(OrderStates.CONFIRMED)
.event(OrderEvents.USER_CONFIRM)
.and()
// 超时自动取消
.withExternal()
.source(OrderStates.APPROVED)
.target(OrderStates.CANCELLED)
.event(OrderEvents.AUTO_EXPIRE)
.timerOnce(30 * 60 * 1000) // 30分钟未确认自动取消
.and()
// 备餐流程
.withExternal()
.source(OrderStates.CONFIRMED)
.target(OrderStates.PREPARING)
.event(OrderEvents.START_PREPARE);
}
}
3. 关键业务逻辑实现
3.1 状态机持久化方案
为保证服务重启后状态不丢失,需要实现状态机持久化。我们采用Redis存储方案:
java复制public class RedisStateMachinePersister implements StateMachinePersister<OrderStates, OrderEvents, String> {
private final RedisTemplate<String, byte[]> redisTemplate;
private final StateMachineRuntimePersister<OrderStates, OrderEvents, String> runtimePersister;
@Override
public void persist(StateMachine<OrderStates, OrderEvents> stateMachine, String orderId) {
try {
byte[] serialized = serialize(stateMachine);
redisTemplate.opsForValue().set(buildRedisKey(orderId), serialized);
} catch (Exception e) {
throw new StateMachineException("Persist failed", e);
}
}
private byte[] serialize(StateMachine<OrderStates, OrderEvents> stateMachine) {
// 使用Jackson序列化
}
}
实际项目中需要处理序列化异常和网络抖动问题,建议添加重试机制。
3.2 业务校验拦截器
在状态转换前添加业务校验:
java复制public class OrderStateChangeInterceptor extends StateMachineInterceptorAdapter<OrderStates, OrderEvents> {
@Override
public Message<OrderEvents> preEvent(Message<OrderEvents> message,
StateMachine<OrderStates, OrderEvents> stateMachine) {
String orderId = message.getHeaders().get("orderId", String.class);
Order currentOrder = orderService.findById(orderId);
// 示例:检查用户取消权限
if (message.getPayload() == OrderEvents.USER_CANCEL) {
if (!currentOrder.getUserId().equals(getCurrentUserId())) {
throw new IllegalStateException("无权限取消订单");
}
if (stateMachine.getState().getId() == OrderStates.PREPARING) {
throw new IllegalStateException("备餐开始后不可取消");
}
}
return message;
}
}
3.3 分布式锁处理
防止并发操作导致状态不一致:
java复制@Service
public class OrderStateService {
@Autowired
private RedissonClient redissonClient;
public boolean sendEvent(String orderId, OrderEvents event) {
RLock lock = redissonClient.getLock("order:" + orderId);
try {
if (lock.tryLock(3, 5, TimeUnit.SECONDS)) {
StateMachine<OrderStates, OrderEvents> sm = stateMachineFactory.getStateMachine();
return sm.sendEvent(MessageBuilder.withPayload(event)
.setHeader("orderId", orderId)
.build());
}
} finally {
lock.unlock();
}
return false;
}
}
4. 生产环境问题排查
4.1 典型异常场景处理
| 异常场景 | 现象 | 解决方案 |
|---|---|---|
| 重复事件 | 收到多次相同事件 | 添加幂等处理,记录已处理事件ID |
| 状态死锁 | 无法进行任何状态转换 | 添加后台状态修复任务,定期检查异常状态 |
| 事件丢失 | 事件触发后状态未更新 | 实现事件补偿机制,重要事件添加重试队列 |
4.2 监控指标设计
建议监控以下关键指标:
- 状态转换耗时(95线应<200ms)
- 失败事件比例(阈值<0.1%)
- 自动取消订单数(突增可能表示通知系统异常)
- 状态不一致订单数(需立即告警)
使用Prometheus采集示例:
java复制@Bean
public StateMachineListener<OrderStates, OrderEvents> metricsListener(
MeterRegistry registry) {
return new StateMachineListenerAdapter<OrderStates, OrderEvents>() {
@Override
public void transition(Transition<OrderStates, OrderEvents> transition) {
Timer.builder("order.state.transition")
.tag("source", transition.getSource().getId().name())
.tag("target", transition.getTarget().getId().name())
.register(registry)
.record(transition.getDuration());
}
};
}
4.3 调试技巧
- 可视化当前状态:
bash复制# 获取Redis中存储的状态机数据
redis-cli get "order:state:{orderId}"
- 强制状态修复(仅限紧急情况):
java复制stateMachine.stop();
stateMachine.getStateMachineAccessor()
.doWithAllRegions(access -> access.resetStateMachine(
new DefaultStateMachineContext<>(targetState, null, null, null)));
stateMachine.start();
5. 扩展优化方向
5.1 状态机版本迁移
当业务规则变更需要修改状态机定义时:
- 新版本状态机与旧版并行运行
- 通过订单创建时间路由到对应版本
- 开发迁移工具将旧订单状态转换为新版模型
5.2 可视化监控看板
基于状态机定义自动生成:
- 实时状态分布饼图
- 状态转换路径桑基图
- 异常状态预警列表
5.3 自动化测试方案
构建状态机测试套件:
java复制@StateMachineTest
class OrderStateMachineTests {
@Autowired
private StateMachineFactory<OrderStates, OrderEvents> factory;
private StateMachine<OrderStates, OrderEvents> stateMachine;
@BeforeEach
void setUp() {
stateMachine = factory.getStateMachine();
stateMachine.start();
}
@Test
void shouldExpireWhenNotConfirmed() {
StateMachineTestPlan<OrderStates, OrderEvents> plan =
StateMachineTestPlanBuilder.<OrderStates, OrderEvents>builder()
.defaultAwaitTime(2)
.stateMachine(stateMachine)
.step()
.expectStates(OrderStates.INITIAL)
.and()
.step()
.sendEvent(OrderEvents.SUBMIT)
.expectStateChanged(1)
.expectStates(OrderStates.APPLIED)
.and()
.build();
plan.test();
}
}
在实施过程中,我们发现SSM的Region概念特别适合处理多级审批场景。例如将店长审批和管理员审批划分为不同region,每个region维护自己的子状态机,最终通过join操作合并审批结果。这种设计比传统的if-else逻辑更易于维护和扩展。