來聊聊一個輕量級的有限狀態(tài)機:Cola-StateMachine
簡單研究了一下研究了一下市面上的幾個狀態(tài)機框架,包括但不限制于Spring Statemachine以及Cola-StateMachine,考慮到前者上下文會記錄當(dāng)前狀態(tài)機的相關(guān)屬性(當(dāng)前狀態(tài)信息、上一次狀態(tài)),對此我們就必須要通過工廠模式等方式規(guī)避這些問題,很明顯這種方案對于高并發(fā)場景下非常不友好。
于是筆者選用了更加輕量級的無狀態(tài)狀態(tài)機框架Cola-StateMachine,而本文將用常見的下單流程演示一下Cola-StateMachine的基本使用,希望對你有幫助。
一、狀態(tài)機基本概念掃盲
狀態(tài)機通俗來說就是有限狀態(tài)機(Finite-state machine,FSM),我們可以將其理解為一個數(shù)學(xué)模型,有限狀態(tài)以及這些轉(zhuǎn)臺之間轉(zhuǎn)移和動作行為的抽象的數(shù)學(xué)模型。
我們以一個簡單的開關(guān)燈為例子簡單介紹一下狀態(tài)機的基本概念,當(dāng)我們點擊開時電燈就會亮起狀態(tài)就是open,按照狀態(tài)機的幾個核心概念:
- 當(dāng)我們準(zhǔn)備按下開關(guān)時,這個準(zhǔn)備按下開關(guān)也就是需要執(zhí)行的指令,也就是事件event。
- 實際按下開關(guān)的執(zhí)行動作也就是狀態(tài)機中的動作(action)。
- open就是狀態(tài)機中的狀態(tài)(state)。
- 電燈由暗變亮,這個就是所謂transition也就是狀態(tài)的轉(zhuǎn)換,這就是狀態(tài)機的最后一個概念。
二、基于Cola-StateMachine落地下單業(yè)務(wù)
1. 業(yè)務(wù)流程說明
現(xiàn)在我們就以一個訂單為例子介紹一下狀態(tài)機的進階使用,如下圖:
- 初始狀態(tài)下訂單狀態(tài)為待支付。
- 用戶點擊付款之后會觸發(fā)已付款事件。
- 此時訂單的狀態(tài)就會變?yōu)榇l(fā)貨。
- 商家完成發(fā)貨之后觸發(fā)已發(fā)貨事件,此時訂單變?yōu)榇肇洝?/li>
- 最終,買家收獲之后觸發(fā)已收貨事件,訂單變?yōu)榻K態(tài)已完成。
2. 狀態(tài)機落地
基于上述需求我們進行代碼落地,首先定義訂單狀態(tài)枚舉,代碼如下所示,該枚舉將交由后續(xù)狀態(tài)機進行狀態(tài)扭轉(zhuǎn)的和事件的映射配置:
public enum OrderStatusEnum {
WAIT_PAYMENT(0, "待支付"),
WAIT_DELIVER(1, "待發(fā)貨"),
WAIT_RECEIVE(2, "待收貨"),
FINISH(3, "完成");
private int code;
private String description;
OrderStatusEnum(int code, String description) {
this.code = code;
this.description = description;
}
//get set......
}
然后就是事件枚舉,如上圖所說,筆者分別定義了買家已支付事件、商家已發(fā)貨事件和買家已收貨的3個事件枚舉:
public enum OrderEventEnum {
PAYED(0,"已支付"),
DELIVERY(1,"已發(fā)貨"),
RECEIVED(2,"已收貨");
private int code;
private String description;
OrderEventEnum(int code, String description) {
this.code = code;
this.description = description;
}
//get set ......
}
為了簡潔且方便的演示實際業(yè)務(wù)持久化的場景,筆者通過一個ConcurrentHashMap模擬數(shù)據(jù)庫存儲:
@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;
}
}
重點來了,接下來就是訂單狀態(tài)扭轉(zhuǎn)和事件的綁定,這里筆者簡單說明一下,Cola-StateMachine進行配置初始化時,是通過內(nèi)置的StateMachineBuilderFactory進行創(chuàng)建。
我們指明狀態(tài)和事件類型為上文所述的OrderStatusEnum和OrderEventEnum,以及實際操作的對象是訂單類order,通過externalTransition進行對應(yīng)狀態(tài)扭轉(zhuǎn)和事件配置。
我們以第一條配置為例,當(dāng)我們觸發(fā)PAYED即買家已支付事件時,狀態(tài)會從WAIT_PAYMENT待支付變?yōu)榇l(fā)貨WAIT_DELIVER,同時為了明確我們的訂單是否是由待支付變?yōu)榇l(fā)貨,筆者通過when函數(shù)校驗一下對訂單狀態(tài)進行了一下校驗。
明確之后狀態(tài)源狀態(tài)是待支付之后,這條配置執(zhí)行perform的動作(Action),即將訂單狀態(tài)改為待發(fā)貨,然后將訂單最新的狀態(tài)持久化入庫。
而其他配置同理,讀者可參考注釋自行了解:
@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)//變?yōu)榇l(fā)貨
.on(OrderEventEnum.PAYED)//需要通過支付事件
.when(o -> o.getOrderStatus().equals(OrderStatusEnum.WAIT_PAYMENT))//判斷條件為傳入的訂單是待支付的
.perform((f, t, e, o) -> {
System.out.println("將" + JSONUtil.toJsonStr(o) + " 由狀態(tài) " + f.getDescription() + " 變?yōu)?" + t.getDescription());
//上述要求符合后執(zhí)行將狀態(tài)修改為代發(fā)貨,并持久化
o.setOrderStatus(OrderStatusEnum.WAIT_DELIVER);
orderMapper.put(o.getOrderId(), o);
});
builder.externalTransition()
.from(OrderStatusEnum.WAIT_DELIVER)//從待發(fā)貨
.to(OrderStatusEnum.WAIT_RECEIVE)//變?yōu)榇斋@
.on(OrderEventEnum.DELIVERY)//通過發(fā)貨事件
.when(o -> true)//沒有需要考慮的條件
.perform((f, t, e, o) -> {//修改訂單狀態(tài)并持久化入庫
o.setOrderStatus(OrderStatusEnum.WAIT_RECEIVE);
orderMapper.put(o.getOrderId(), o);
System.out.println("將" + JSONUtil.toJsonStr(o) + " 由狀態(tài) " + f.getDescription() + " 變?yōu)?" + t.getDescription());
});
builder.externalTransition()
.from(OrderStatusEnum.WAIT_RECEIVE)//從待收貨
.to(OrderStatusEnum.FINISH)//到已完成
.on(OrderEventEnum.RECEIVED)//通過收獲事件觸發(fā)
.when(o -> true)//無需任何條件校驗
.perform((f, t, e, o) -> {
//修改狀態(tài)并持久化
o.setOrderStatus(OrderStatusEnum.FINISH);
orderMapper.put(o.getOrderId(), o);
System.out.println("將" + JSONUtil.toJsonStr(o) + " 由狀態(tài) " + f.getDescription() + " 變?yōu)?" + t.getDescription());
});
return builder.build("orderStateMachine");
}
}
完成上述配置后,我們就可以在業(yè)務(wù)代碼上使用這套狀態(tài)機了,我們在開發(fā)買家支付方法時,只需將狀態(tài)機注入,然后調(diào)用狀態(tài)機的fireEvent方法,傳入訂單的源狀態(tài)、事件枚舉、訂單信息,讓狀態(tài)機根據(jù)我們的狀態(tài)和事件進行判斷,并完成狀態(tài)修改和持久化:
對應(yīng)代碼如下,讀者參考注釋閱讀:
@Service
public class OrderService {
@Autowired
StateMachine<OrderStatusEnum, OrderEventEnum, Order> stateMachine;
private AtomicLong id = new AtomicLong(0);
@Autowired
private OrderMapper orderMapper;
public Order create() {
//創(chuàng)建訂單
Order order = new Order();
//初始化狀態(tài)為待支付
order.setOrderStatus(OrderStatusEnum.WAIT_PAYMENT);
//分配id
order.setOrderId(id.incrementAndGet());
orderMapper.put(order.getOrderId(), order);
System.out.println("訂單創(chuàng)建成功:" + JSONUtil.toJsonStr(order));
return order;
}
public void pay(long id) {
//查詢訂單
Order order = orderMapper.get(id);
System.out.println("準(zhǔn)備下單,訂單號:" + id);
//生成事件消息,希望將訂單狀態(tài)改為已支付,并存入當(dāng)前訂單數(shù)據(jù)
stateMachine.fireEvent(order.getOrderStatus(), OrderEventEnum.PAYED, order);
}
public void deliver(long id) {
Order order = orderMapper.get(id);
System.out.println("準(zhǔn)備給訂單發(fā)貨,訂單號:" + id);
//傳入訂單,并觸發(fā)發(fā)貨事件,成功后訂單狀態(tài)會改為待收貨
stateMachine.fireEvent(order.getOrderStatus(), OrderEventEnum.DELIVERY, order);
}
public void receive(long id) {
Order order = orderMapper.get(id);
System.out.println("嘗試收貨,訂單號:" + id);
//傳入訂單,并觸發(fā)收貨事件,將訂單修改為已完成
stateMachine.fireEvent(order.getOrderStatus(), OrderEventEnum.RECEIVED, order);
}
public Map<Long, Order> getOrders() {
return orderMapper.getOrders();
}
}
3. 最終效果演示
最后我們給出并發(fā)的測試用例:
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()));
可以看到訂單都完成了:
訂單創(chuàng)建成功:{"orderId":1,"orderStatus":"WAIT_PAYMENT"}
訂單創(chuàng)建成功:{"orderId":2,"orderStatus":"WAIT_PAYMENT"}
準(zhǔn)備下單,訂單號:1
準(zhǔn)備下單,訂單號:2
將{"orderId":1,"orderStatus":"WAIT_PAYMENT"} 由狀態(tài) 待支付 變?yōu)?待發(fā)貨
將{"orderId":2,"orderStatus":"WAIT_PAYMENT"} 由狀態(tài) 待支付 變?yōu)?待發(fā)貨
準(zhǔn)備給訂單發(fā)貨,訂單號:1
準(zhǔn)備給訂單發(fā)貨,訂單號:2
將{"orderId":1,"orderStatus":"WAIT_RECEIVE"} 由狀態(tài) 待發(fā)貨 變?yōu)?待收貨
將{"orderId":2,"orderStatus":"WAIT_RECEIVE"} 由狀態(tài) 待發(fā)貨 變?yōu)?待收貨
嘗試收貨,訂單號:2
嘗試收貨,訂單號:1
將{"orderId":2,"orderStatus":"FINISH"} 由狀態(tài) 待收貨 變?yōu)?完成
將{"orderId":1,"orderStatus":"FINISH"} 由狀態(tài) 待收貨 變?yōu)?完成
訂單處理完成:{"1":{"orderId":1,"orderStatus":"FINISH"},"2":{"orderId":2,"orderStatus":"FINISH"}}
三、小結(jié)
自此我們通過Cola-StateMachine完成一個簡單的案例快速入門了狀態(tài)機的使用,希望對你有幫助。