來聊聊一個輕量級的有限狀態機:Cola-StateMachine
簡單研究了一下研究了一下市面上的幾個狀態機框架,包括但不限制于Spring Statemachine以及Cola-StateMachine,考慮到前者上下文會記錄當前狀態機的相關屬性(當前狀態信息、上一次狀態),對此我們就必須要通過工廠模式等方式規避這些問題,很明顯這種方案對于高并發場景下非常不友好。
于是筆者選用了更加輕量級的無狀態狀態機框架Cola-StateMachine,而本文將用常見的下單流程演示一下Cola-StateMachine的基本使用,希望對你有幫助。

一、狀態機基本概念掃盲
狀態機通俗來說就是有限狀態機(Finite-state machine,FSM),我們可以將其理解為一個數學模型,有限狀態以及這些轉臺之間轉移和動作行為的抽象的數學模型。
我們以一個簡單的開關燈為例子簡單介紹一下狀態機的基本概念,當我們點擊開時電燈就會亮起狀態就是open,按照狀態機的幾個核心概念:
- 當我們準備按下開關時,這個準備按下開關也就是需要執行的指令,也就是事件event。
- 實際按下開關的執行動作也就是狀態機中的動作(action)。
- open就是狀態機中的狀態(state)。
- 電燈由暗變亮,這個就是所謂transition也就是狀態的轉換,這就是狀態機的最后一個概念。

二、基于Cola-StateMachine落地下單業務
1. 業務流程說明
現在我們就以一個訂單為例子介紹一下狀態機的進階使用,如下圖:
- 初始狀態下訂單狀態為待支付。
- 用戶點擊付款之后會觸發已付款事件。
- 此時訂單的狀態就會變為代發貨。
- 商家完成發貨之后觸發已發貨事件,此時訂單變為待收貨。
- 最終,買家收獲之后觸發已收貨事件,訂單變為終態已完成。

2. 狀態機落地
基于上述需求我們進行代碼落地,首先定義訂單狀態枚舉,代碼如下所示,該枚舉將交由后續狀態機進行狀態扭轉的和事件的映射配置:
public enum OrderStatusEnum {
WAIT_PAYMENT(0, "待支付"),
WAIT_DELIVER(1, "待發貨"),
WAIT_RECEIVE(2, "待收貨"),
FINISH(3, "完成");
private int code;
private String description;
OrderStatusEnum(int code, String description) {
this.code = code;
this.description = description;
}
//get set......
}然后就是事件枚舉,如上圖所說,筆者分別定義了買家已支付事件、商家已發貨事件和買家已收貨的3個事件枚舉:
public enum OrderEventEnum {
PAYED(0,"已支付"),
DELIVERY(1,"已發貨"),
RECEIVED(2,"已收貨");
private int code;
private String description;
OrderEventEnum(int code, String description) {
this.code = code;
this.description = description;
}
//get set ......
}為了簡潔且方便的演示實際業務持久化的場景,筆者通過一個ConcurrentHashMap模擬數據庫存儲:
@Component
public class OrderMapper {
private Map<Long, Order> orders = new ConcurrentHashMap<>();
public void put(Long id, Order order) {
orders.put(id, order);
}
public Order get(Long id) {
return orders.get(id);
}
public Map<Long, Order> getOrders() {
return orders;
}
}重點來了,接下來就是訂單狀態扭轉和事件的綁定,這里筆者簡單說明一下,Cola-StateMachine進行配置初始化時,是通過內置的StateMachineBuilderFactory進行創建。
我們指明狀態和事件類型為上文所述的OrderStatusEnum和OrderEventEnum,以及實際操作的對象是訂單類order,通過externalTransition進行對應狀態扭轉和事件配置。
我們以第一條配置為例,當我們觸發PAYED即買家已支付事件時,狀態會從WAIT_PAYMENT待支付變為待發貨WAIT_DELIVER,同時為了明確我們的訂單是否是由待支付變為代發貨,筆者通過when函數校驗一下對訂單狀態進行了一下校驗。
明確之后狀態源狀態是待支付之后,這條配置執行perform的動作(Action),即將訂單狀態改為待發貨,然后將訂單最新的狀態持久化入庫。
而其他配置同理,讀者可參考注釋自行了解:
@Component
public class OrderStatusMachineConfig {
@Autowired
private OrderMapper orderMapper;
@Bean
public StateMachine stateMachine() {
StateMachineBuilder<OrderStatusEnum, OrderEventEnum, Order> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(OrderStatusEnum.WAIT_PAYMENT)//從待支付
.to(OrderStatusEnum.WAIT_DELIVER)//變為待發貨
.on(OrderEventEnum.PAYED)//需要通過支付事件
.when(o -> o.getOrderStatus().equals(OrderStatusEnum.WAIT_PAYMENT))//判斷條件為傳入的訂單是待支付的
.perform((f, t, e, o) -> {
System.out.println("將" + JSONUtil.toJsonStr(o) + " 由狀態 " + f.getDescription() + " 變為 " + t.getDescription());
//上述要求符合后執行將狀態修改為代發貨,并持久化
o.setOrderStatus(OrderStatusEnum.WAIT_DELIVER);
orderMapper.put(o.getOrderId(), o);
});
builder.externalTransition()
.from(OrderStatusEnum.WAIT_DELIVER)//從待發貨
.to(OrderStatusEnum.WAIT_RECEIVE)//變為待收獲
.on(OrderEventEnum.DELIVERY)//通過發貨事件
.when(o -> true)//沒有需要考慮的條件
.perform((f, t, e, o) -> {//修改訂單狀態并持久化入庫
o.setOrderStatus(OrderStatusEnum.WAIT_RECEIVE);
orderMapper.put(o.getOrderId(), o);
System.out.println("將" + JSONUtil.toJsonStr(o) + " 由狀態 " + f.getDescription() + " 變為 " + t.getDescription());
});
builder.externalTransition()
.from(OrderStatusEnum.WAIT_RECEIVE)//從待收貨
.to(OrderStatusEnum.FINISH)//到已完成
.on(OrderEventEnum.RECEIVED)//通過收獲事件觸發
.when(o -> true)//無需任何條件校驗
.perform((f, t, e, o) -> {
//修改狀態并持久化
o.setOrderStatus(OrderStatusEnum.FINISH);
orderMapper.put(o.getOrderId(), o);
System.out.println("將" + JSONUtil.toJsonStr(o) + " 由狀態 " + f.getDescription() + " 變為 " + t.getDescription());
});
return builder.build("orderStateMachine");
}
}完成上述配置后,我們就可以在業務代碼上使用這套狀態機了,我們在開發買家支付方法時,只需將狀態機注入,然后調用狀態機的fireEvent方法,傳入訂單的源狀態、事件枚舉、訂單信息,讓狀態機根據我們的狀態和事件進行判斷,并完成狀態修改和持久化:

對應代碼如下,讀者參考注釋閱讀:
@Service
public class OrderService {
@Autowired
StateMachine<OrderStatusEnum, OrderEventEnum, Order> stateMachine;
private AtomicLong id = new AtomicLong(0);
@Autowired
private OrderMapper orderMapper;
public Order create() {
//創建訂單
Order order = new Order();
//初始化狀態為待支付
order.setOrderStatus(OrderStatusEnum.WAIT_PAYMENT);
//分配id
order.setOrderId(id.incrementAndGet());
orderMapper.put(order.getOrderId(), order);
System.out.println("訂單創建成功:" + JSONUtil.toJsonStr(order));
return order;
}
public void pay(long id) {
//查詢訂單
Order order = orderMapper.get(id);
System.out.println("準備下單,訂單號:" + id);
//生成事件消息,希望將訂單狀態改為已支付,并存入當前訂單數據
stateMachine.fireEvent(order.getOrderStatus(), OrderEventEnum.PAYED, order);
}
public void deliver(long id) {
Order order = orderMapper.get(id);
System.out.println("準備給訂單發貨,訂單號:" + id);
//傳入訂單,并觸發發貨事件,成功后訂單狀態會改為待收貨
stateMachine.fireEvent(order.getOrderStatus(), OrderEventEnum.DELIVERY, order);
}
public void receive(long id) {
Order order = orderMapper.get(id);
System.out.println("嘗試收貨,訂單號:" + id);
//傳入訂單,并觸發收貨事件,將訂單修改為已完成
stateMachine.fireEvent(order.getOrderStatus(), OrderEventEnum.RECEIVED, order);
}
public Map<Long, Order> getOrders() {
return orderMapper.getOrders();
}
}3. 最終效果演示
最后我們給出并發的測試用例:
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
orderService.create();
orderService.pay(1L);
orderService.deliver(1L);
orderService.receive(1L);
countDownLatch.countDown();
}).start();
new Thread(() -> {
orderService.create();
orderService.pay(2L);
orderService.deliver(2L);
orderService.receive(2L);
countDownLatch.countDown();
}).start();
countDownLatch.await();
System.out.println("訂單處理完成:" + JSONUtil.toJsonStr(orderService.getOrders()));可以看到訂單都完成了:
訂單創建成功:{"orderId":1,"orderStatus":"WAIT_PAYMENT"}
訂單創建成功:{"orderId":2,"orderStatus":"WAIT_PAYMENT"}
準備下單,訂單號:1
準備下單,訂單號:2
將{"orderId":1,"orderStatus":"WAIT_PAYMENT"} 由狀態 待支付 變為 待發貨
將{"orderId":2,"orderStatus":"WAIT_PAYMENT"} 由狀態 待支付 變為 待發貨
準備給訂單發貨,訂單號:1
準備給訂單發貨,訂單號:2
將{"orderId":1,"orderStatus":"WAIT_RECEIVE"} 由狀態 待發貨 變為 待收貨
將{"orderId":2,"orderStatus":"WAIT_RECEIVE"} 由狀態 待發貨 變為 待收貨
嘗試收貨,訂單號:2
嘗試收貨,訂單號:1
將{"orderId":2,"orderStatus":"FINISH"} 由狀態 待收貨 變為 完成
將{"orderId":1,"orderStatus":"FINISH"} 由狀態 待收貨 變為 完成
訂單處理完成:{"1":{"orderId":1,"orderStatus":"FINISH"},"2":{"orderId":2,"orderStatus":"FINISH"}}三、小結
自此我們通過Cola-StateMachine完成一個簡單的案例快速入門了狀態機的使用,希望對你有幫助。

































