那些年,我们见过的 Java 服务端“问题”
有限状态机介绍
概念
有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的一个数学模型。
要素
状态机可归纳为4个要素:现态、条件、动作、次态。

现态:指当前流程所处的状态,包括起始、中间、终结状态。
条件:也可称为事件;当一个条件被满足时,将会触发一个动作并执行一次状态的迁移。
动作:当条件满足后要执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。
次态:当条件满足后要迁往的状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。
状态状态表示流程中的持久状态,流程图上的每一个圈代表一个状态。
初始状态: 流程开始时的某一状态;
中间状态: 流程中间过程的某一状态;
终结状态: 流程完成时的某一状态。
使用建议:
- 状态必须是一个持久状态,而不能是一个临时状态;
- 终结状态不能是中间状态,不能继续进行流程流转;
- 状态划分合理,不要把多个状态强制合并为一个状态;
- 状态尽量精简,同一状态的不同情况可以用其它字段表示。
动作
动作的三要素:角色、现态、次态,流程图上的每一条线代表一个动作。
角色: 谁发起的这个操作,可以是用户、定时任务等;
现态: 触发动作时当前的状态,是执行动作的前提条件;
次态: 完成动作后达到的状态,是执行动作的最终目标。
使用建议:
- 每个动作执行前,必须检查当前状态和触发动作状态的一致性;
- 状态机的状态更改,只能通过动作进行,其它操作都是不符合规范的;
- 需要添加分布式锁保证动作的原子性,添加数据库事务保证数据的一致性;
- 类似的动作(比如操作用户、请求参数、动作含义等)可以合并为一个动作,并根据动作执行结果转向不同的状态。
系统间交互不科学
直接通过数据库交互
在一些项目中,系统间交互不通过接口调用和消息队列,而是通过数据库直接访问。问其原因,回答道:”项目工期太紧张,直接访问数据库,简单又快捷”。
还是以上面的采购流程为例——采购订单由库管系统发起,由采购系统负责采购,采购完成后通知库管系统,库管系统进入入库操作。采购系统采购完成后,通知库管系统数据库的代码如下:
/** 执行回流动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void executeBackflow(PurchaseOrder order) {
// 完成原始采购单
rawPurchaseOrderDAO.setStatus(order.getRawId(), RawPurchaseOrderStatus.FINISHED.getValue());
// 设置回流状态
purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue());
}
其中,通过rawPurchaseOrderDAO(原始采购单DAO)直接访问库管系统的数据库表,并设置原始采购单状态为已完成。
一般情况下,直接通过数据访问的方式是不会有问题的。但是,一旦发生竞态,就会导致数据不同步。有人会说,可以考虑使用同一分布式锁解决该问题。是的,这种解决方案没有问题,只是又在系统间共享了分布式锁。
直接通过数据库交互的缺点:
- 直接暴露数据库表,容易产生数据安全问题;
- 多个系统操作同一数据库表,容易造成数据库表数据混乱;
- 操作同一个数据库表的代码,分布在不同的系统中,不便于管理和维护;
- 具有数据库表这样的强关联,无法实现系统间的隔离和解耦。
通过Dubbo接口交互
由于采购系统和库管系统都是内部系统,可以通过类似Dubbo的RPC接口进行交互。库管系统代码:
/** 采购单服务接口 */
public interface PurchaseOrderService {
/** 完成采购单函数 */
public void finishPurchaseOrder(Long orderId);
}
/** 采购单服务实现 */
@Service("purchaseOrderService")
public class PurchaseOrderServiceImpl implements PurchaseOrderService {
/** 完成采购单函数 */
@Override
@Transactional(rollbackFor = Exception.class)
public void finishPurchaseOrder(Long orderId) {
// 相关处理
...
// 完成采购单
purchaseOrderService.finishPurchaseOrder(order.getRawId());
}
}
其中,库管系统通过Dubbo把PurchaseOrderServiceImpl(采购单服务实现)以PurchaseOrderService(采购单服务接口)定义的接口服务暴露给采购系统。这里,省略了Dubbo开发服务接口相关配置。
采购系统代码:
/** 执行回流动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void executeBackflow(PurchaseOrder order) {
// 完成采购单
purchaseOrderService.finishPurchaseOrder(order.getRawId());
// 设置回流状态
purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue());
}
其中,purchaseOrderService(采购单服务)为库管系统PurchaseOrderService(采购单服务)在采购系统中的Dubbo服务客户端存根,通过该服务调用库管系统的服务接口函数finishPurchaseOrder(完成采购单函数)。
这样,采购系统和库管系统自己的强关联,通过Dubbo就简单地实现了系统隔离和解耦。当然,除了采用Dubbo接口外,还可以采用HTTPS、HSF、WebService等同步接口调用方式,也可以采用MetaQ等异步消息通知方式。
常见系统间交互协议
同步接口调用
同步接口调用是以一种阻塞式的接口调用机制。常见的交互协议有:
- HTTP/HTTPS接口;
- WebService接口;
- Dubbo/HSF接口;
- CORBA接口。
异步消息通知
异步消息通知是一种通知式的信息交互机制。当系统发生某种事件时,会主动通知相应的系统。常见的交互协议有:
- MetaQ的消息通知;
- CORBA消息通知。
