Spring StateMachine状态机引擎在项目中的应用(六)-外部调用&事务

说明

前面基本上涵盖了一个项目中配置简单状态机的相关实现,不过还有一个关键点,就是外部代码如何调用状态机,以及如何让状态机的持久化与业务逻辑代码在同一个事务中,避免状态机中状态与实际订单中不一致,造成脏数据。

调用方式

外部调用状态机引擎,需要以下三步:

  1. 通过创建/读取的方式获取当前订单对应的状态机引擎实例
  2. 构造message。
  3. 发送message。

需要注意以下几点:

  1. 在状态机发送完message之后,spring statemachine会通过ActionListener来监听,同时判断需要走到哪个Action中
  2. 只有在sendMessage完成之后,状态机的当前状态才会更新为target状态

所以对于调用状态机,做了以下代码封装:

接口:
/**
 * 存在状态机做串联时,统一的事务处理,将状态机实例持久化也囊括在统一的事务中
 */
public interface StateMachineSendEventManager {

    /**
     * 发送状态机event,调用bizManagerImpl中具体实现,同时处理状态机持久化
     * <p>
     * 用于订单的状态变更
     *
     * @param request
     * @param operationTypeEnum
     * @param eventEnum
     * @return
     * @throws BusinessException
     */
    OrderBaseResponse sendStatusChangeEvent(BizOrderStatusRequest request,
                                            BizOrderOperationTypeEnum operationTypeEnum,
                                            BizOrderStatusChangeEventEnum eventEnum) throws Exception;


    /**
     * 同上,不过是用于订单创建场景
     *
     * @param request
     * @param operationTypeEnum
     * @param eventEnum
     * @return
     * @throws Exception
     */
    BizOrderCreateResponse sendOrderCreateEvent(BizOrderCreateRequest request,
                                                BizOrderOperationTypeEnum operationTypeEnum,
                                                BizOrderStatusChangeEventEnum eventEnum) throws Exception;

}
对应实现
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.StateMachinePersister;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Slf4j
@Component("stateMachineSendEventManager")
public class StateMachineSendEventManagerImpl implements StateMachineSendEventManager {

    @Autowired
    private BizOrderRepository bizOrderRepository;

    @Autowired
    private BizOrderStateMachineBuildFactory bizOrderStateMachineBuildFactory;

    @Autowired
    @Qualifier("bizOrderRedisStateMachinePersister")
    private StateMachinePersister<BizOrderStatusEnum,BizOrderStatusChangeEventEnum,String> bizOrderRedisStateMachinePersister;

    /**
     * 发送状态机event,调用bizManagerImpl中具体实现,同时处理状态机持久化
     * <p>
     * 这里会send stateMachine event,从而跳转到对应的action --> bizManagerImpl,出现事务嵌套的情况
     * <p>
     * 不过事务传播默认是TransactionDefinition.PROPAGATION_REQUIRED,所以还是同一个事务中,
     * 只是事务范围扩大至stateMachine的持久化场景了,不要修改默认的传播机制
     *
     * @param request
     * @return
     * @throws BusinessException
     */
    @Override
    @Transactional(value = "finOrderocTransactionManager", rollbackFor = {BusinessException.class, Exception.class})
    public OrderBaseResponse sendStatusChangeEvent(BizOrderStatusRequest request,
                                                   BizOrderOperationTypeEnum operationTypeEnum,
                                                   BizOrderStatusChangeEventEnum eventEnum) throws Exception {

        // 获取状态机信息
        StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> stateMachine =
                getStateMachineFromStatusReq(request, operationTypeEnum);


        boolean result = statusChangeCommonOps(stateMachine, request, eventEnum);

        OrderBaseResponse resp = new OrderBaseResponse();
        if (!result) {
            resp.setResultCode(BizOrderErrorCode.ORDER_STATE_MACHINE_EXECUTE_ERR.name());
            resp.setMsg("订单状态操作异常");
        }

        log.info("order status change resp is {}", resp);

        // 更新redis中数据
        // 发送event写log的动作还是放在业务里面,这里无法囊括所有业务数据
        if (result) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    // 将数据持久化到redis中,以bizOrderId作为对应Key
                    try {
                        bizOrderRedisStateMachinePersister.persist(stateMachine, request.getBizCode());
                    } catch (Exception e) {
                        log.error("Persist bizOrderStateMachine error", e);
                    }
                }
            });
        }

        return resp;
    }

    /**
     * 同上,不过是用于订单创建场景,请求为BizOrderCreateRequest
     *
     * @param bizOrderCreateRequest
     * @param operationTypeEnum
     * @param eventEnum
     * @return
     * @throws Exception
     */
    @Override
    @Transactional(value = "finOrderocTransactionManager", rollbackFor = {BusinessException.class, Exception.class})
    public BizOrderCreateResponse sendOrderCreateEvent(BizOrderCreateRequest bizOrderCreateRequest,
                                                       BizOrderOperationTypeEnum operationTypeEnum,
                                                       BizOrderStatusChangeEventEnum eventEnum) throws Exception {


        // 获取对应的stateMachine
        StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> stateMachine =
                getStateMachineFromCreateReq(bizOrderCreateRequest, operationTypeEnum);

        Message<BizOrderStatusChangeEventEnum> eventMsg = MessageBuilder.withPayload(eventEnum)
                // key 与 status change 时不同,对应的model也不同
                .setHeader(BizOrderConstants.BIZORDER_CONTEXT_CREATE_KEY, bizOrderCreateRequest)
                // 根据传递过来的订单状态决定后续choice跳转
                .setHeader(BizOrderConstants.FINAL_STATUS_KEY, bizOrderCreateRequest.getBizOrderCreateModel().getOrderStatus())
                .build();

        BizOrderCreateResponse createResponse = new BizOrderCreateResponse();

        boolean sendResult = false;

        if (BizOrderStateMachineUtils.acceptEvent(stateMachine, eventMsg)) {
            sendResult = stateMachine.sendEvent(eventMsg);
            log.info("order statemachine send event={},result={}", eventMsg, sendResult);
        } else {
            createResponse.setResultCode(BizOrderErrorCode.NO_ORDER_STATE_MACHINE_TRANSTION_ERR.name());
            createResponse.setMsg("当前订单无法执行请求动作");
        }

        if (sendResult) {
            createResponse.setBizOrderId(bizOrderCreateRequest.getBizOrderCreateModel().getBizOrderId());
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    // 将数据持久化到redis中,以bizOrderId作为对应Key
                    try {
                        bizOrderRedisStateMachinePersister.persist(stateMachine,
                                createResponse.getBizOrderId());
                    } catch (Exception e) {
                        throw new BusinessException(BizOrderErrorCode.ORDER_STATE_MACHINE_EXECUTE_ERR, "状态机持久化失败");
                    }
                }
            });
        }


        return createResponse;
    }

    /**
     * 状态处理的通用操作抽取
     *
     * @param stateMachine  状态机
     * @param statusRequest 状态变更请求
     * @return 执行结果
     * @throws Exception 异常
     */
    private boolean statusChangeCommonOps(
            StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> stateMachine,
            BizOrderStatusRequest statusRequest,
            BizOrderStatusChangeEventEnum eventEnum) {

        log.info("order statemachine send event={}", eventEnum);


        // 执行引擎,sendEvent,result为执行结果,通过actionListener跳转到对应的Action
        Message<BizOrderStatusChangeEventEnum> eventMsg = MessageBuilder.
                withPayload(eventEnum)
                .setHeader(BizOrderConstants.BIZORDER_CONTEXT_KEY, statusRequest)
                // 只有在需要判断(choice)的场景才用得到,guard实现中使用
                .setHeader(BizOrderConstants.FINAL_STATUS_KEY, statusRequest.getBizOrderStatusModel().getTargetOrderStatus())
                .build();

        // 取到对应的状态机,判断是否可以执行
        boolean result = false;

        // 状态机的当前状态,只有在执行结束后才会变化,也就是节点对应的action执行完才会变更
        // 所以在result=true的情况下,更新状态机的持久化状态才有效
        if (BizOrderStateMachineUtils.acceptEvent(stateMachine, eventMsg)) {
            result = stateMachine.sendEvent(eventMsg);
            log.info("order statemachine send event={},result={}", eventMsg, result);
        } else {
            throw new BusinessException(BizOrderErrorCode.NO_ORDER_STATE_MACHINE_TRANSTION_ERR, "当前订单无法执行请求动作");
        }
        return result;

    }

    /**
     * 从statusRequest中获取statemachine实例
     *
     * @param statusRequest     状态请求
     * @param operationTypeEnum 操作类型
     * @return 状态机实例
     * @throws Exception 异常
     */
    private StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum>
    getStateMachineFromStatusReq(BizOrderStatusRequest statusRequest,
                                 BizOrderOperationTypeEnum operationTypeEnum) throws Exception {
        log.info("Order status change request={},operationType={}", statusRequest, operationTypeEnum);

        if (!StringUtils.equals(statusRequest.getBizCode(), statusRequest.getBizOrderStatusModel().getBizOrderId())) {
            throw new BusinessException(BizOrderErrorCode.ORDER_COMMON_ILLEGAL_ARGUMENT, "请求数据异常");
        }

        // 查询订单,判断请求数据是否合法
        BizOrder bizOrder = bizOrderRepository.selectByBizPrimaryKey(statusRequest.getBizCode());
        if (null == bizOrder
                || !StringUtils.equals(bizOrder.getBizType(), statusRequest.getBizOrderStatusModel().getBizType())
                || !StringUtils.equals(bizOrder.getOrderStatus(), statusRequest.getBizOrderStatusModel().getCurrentOrderStatus())
                ) {
            throw new BusinessException(BizOrderErrorCode.ORDER_COMMON_ILLEGAL_ARGUMENT, "请求数据与订单实际数据不符");
        }

        // 构造状态机模板
        StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> srcStateMachine =
                bizOrderStateMachineBuildFactory.createStateMachine(statusRequest.getBizOrderStatusModel().getBizType(),
                        statusRequest.getBizOrderStatusModel().getSubBizType());

        // 从redis中获取对应的statemachine,并判断当前节点是否可以满足,如果无法从redis中获取对应的的statemachine,则取自DB
        StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> stateMachine
                = bizOrderRedisStateMachinePersister.restore(srcStateMachine, statusRequest.getBizCode());

        // 由于DB中已持久化,基本上不太可能出现null的情况,目前唯一能想到会出现的情况就是缓存击穿,先抛错
        if (null == stateMachine) {
            throw new BusinessException(BizOrderErrorCode.NO_CORRESPONDING_STATEMACHINE_ERR, "不存在订单对应的状态机实例");
        }
        log.info("order stateMachine info is {}", srcStateMachine);

        return stateMachine;
    }

    /**
     * 获取statemachine实例
     *
     * @param createRequest     状态请求
     * @param operationTypeEnum 操作类型
     * @return 状态机实例
     * @throws Exception 异常
     */
    private StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> getStateMachineFromCreateReq(BizOrderCreateRequest createRequest,
                                                                                                         BizOrderOperationTypeEnum operationTypeEnum) throws Exception {
        log.info("Order create request={},operationType={}", createRequest, operationTypeEnum);

        // 构造状态机模板
        StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> srcStateMachine =
                bizOrderStateMachineBuildFactory.createStateMachine(createRequest.getBizOrderCreateModel().getBizType(),
                        createRequest.getBizOrderCreateModel().getSubBizType());

        if (null == srcStateMachine) {
            throw new BusinessException(BizOrderErrorCode.NO_CORRESPONDING_STATEMACHINE_ERR, "不存在订单对应的状态机实例");
        }

        // 如果是sign,表示订单已存在,需要额外判断并restore状态机;如果是直接create,则不需要处理这些判断
        if (StringUtils.equalsIgnoreCase(BizOrderOperationTypeEnum.SIGN.getOperationType(),
                createRequest.getOperationType())) {
            if (!StringUtils.equals(createRequest.getBizCode(), createRequest.getBizOrderCreateModel().getBizOrderId())) {
                throw new BusinessException(BizOrderErrorCode.ORDER_COMMON_ILLEGAL_ARGUMENT, "请求数据异常");
            }

            // 查询订单,判断请求数据是否合法
            BizOrder bizOrder = bizOrderRepository.selectByBizPrimaryKey(createRequest.getBizCode());
            if (null == bizOrder
                    || !StringUtils.equals(bizOrder.getBizType(), createRequest.getBizOrderCreateModel().getBizType())
                    ) {
                throw new BusinessException(BizOrderErrorCode.ORDER_COMMON_ILLEGAL_ARGUMENT, "请求数据与订单实际数据不符");
            }

            // 从redis中获取对应的statemachine,并判断当前节点是否可以满足,如果无法从redis中获取对应的的statemachine,则取自DB
            StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> stateMachine
                    = bizOrderRedisStateMachinePersister.restore(srcStateMachine, createRequest.getBizOrderCreateModel().getBizOrderId());

            // 由于DB中已持久化,基本上不太可能出现null的情况,目前唯一能想到会出现的情况就是缓存击穿,先抛错
            if (null == stateMachine) {
                throw new BusinessException(BizOrderErrorCode.NO_CORRESPONDING_STATEMACHINE_ERR, "不存在订单对应的状态机实例");
            }

            return stateMachine;
        }

        log.info("order stateMachine info is {}", srcStateMachine);

        return srcStateMachine;
    }
}

这里需要关注BizOrderStateMachineUtils.acceptEvent,相当于在执行之前判断是否可以执行,其实sendEvent中存在一样判断是否可执行的代码,不过这里抽取出来做了一个事前判断,实现如下:

import org.springframework.messaging.Message;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.support.StateMachineUtils;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.trigger.DefaultTriggerContext;
import org.springframework.statemachine.trigger.Trigger;

public class BizOrderStateMachineUtils {

    /**
     * 判断是否可以执行对应的event
     *
     * @param stateMachine
     * @param eventMsg
     * @return
     */
    public static boolean acceptEvent(StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> stateMachine,
                                      Message<BizOrderStatusChangeEventEnum> eventMsg) {
        State<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> cs = stateMachine.getState();

        for (Transition<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> transition : stateMachine.getTransitions()) {
            State<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> source = transition.getSource();
            Trigger<BizOrderStatusEnum, BizOrderStatusChangeEventEnum> trigger = transition.getTrigger();

            if (cs != null && StateMachineUtils.containsAtleastOne(source.getIds(), cs.getIds())) {
                if (trigger != null && trigger.evaluate(new DefaultTriggerContext<>(eventMsg.getPayload()))) {
                    return true;
                }
            }
        }
        return false;
    }

}

其他说明:

  1. 关注getStateMachineFromXXX,这里其实就是调用了builderFactory创建了对应的实例。
  2. 还有个遗留问题,即每次从redis/db中restore时,都需要生成一个最初状态的状态机实例,然后传入进去,转化成当前状态,这种方式可能会造成比较大的资源消耗。
  3. 在sendXXXEvent方法上,都加上了@Transactional注解,这里代码中的注释也说的很清楚,会跟AbstractBizManager中的@Tranactional形成嵌套,所以事务传播方式只能是默认的TransactionDefinition.PROPAGATION_REQUIRED,不能修改,否则会问题。
  4. 至于再外部调用,直接调用StateMachineSendEventManager即可,与状态机无关了。
  5. 至此,完结。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容