Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >COLA-statemachine事务失效踩坑

COLA-statemachine事务失效踩坑

作者头像
benym
修改于 2024-07-19 06:52:56
修改于 2024-07-19 06:52:56
1.3K00
代码可运行
举报
文章被收录于专栏:后端知识体系后端知识体系
运行总次数:0
代码可运行

背景

cola-statemachine是阿里开源项目COLA (opens new window)中的轻量级状态机组件。最大的特点是无状态、采用纯Java实现,用Fluent Interface(连贯接口)定义状态和事件,可用于管理状态转换场景。比如:订单状态、支付状态等简单有限状态场景。在实际使用的过程中我曾发现状态机内事务不生效的问题,经过排查得到解决,以此记录一下。

问题场景

一个简单的基于cola的状态机可能如下

  • 创建状态机
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public StateMachine<State, Event, Context> stateMachine() {
    StateMachineBuilder<State, Event, Context> builder = StateMachineBuilderFactory.create();
    builder.externalTransition().from(State.TEST).to(State.DEPLOY)
            .on(Event.PASS)
            .when(passCondition())
            .perform(passAction());
    return builder.build("testMachine");
}

上述代码翻译过来是

State.TEST状态转化到State.DEPLOY状态,在Event.PASS事件下,当满足passCondition()条件时,执行passAction()内的逻辑

  • 执行状态机
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 根据当前状态、事件、上下文,进行状态流转
 *
 * @param State 当前状态
 * @param Event 当前事件
 * @param Context 当前上下文
 */
public void fire(State state, Event event, Context context) {
    StateMachine<State, Event, Context> stateMachine = StateMachineFactory.get("testMachine");
    stateMachine.fireEvent(state, event, context);
}

上述代码在纯Java环境可以很好的运行,一般来说,开发者会进一步结合Spring来完善多个状态机的获取

过程中通常会将状态机进行@Bean注入,将passCondition()passAction()独立出Service以期望在后续操作中更好的利用Spring的特性

简单改造后的状态机代码可能如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 以下代码并不是定义状态机的正确做法,错误的使用方法造成了事务失效,后面在方法二中解析
@Component
public class StateMachine {

    @Autowired
    private ConditionService conditionService;

    @Autowired
    private ActionService actionService;

    @Bean
    public StateMachine<State, Event, Context> stateMachine() {
        StateMachineBuilder<State, Event, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition().from(State.TEST).to(State.DEPLOY)
                .on(Event.PASS)
                .when(conditionService.passCondition())
                .perform(actionService.passAction());
        return builder.build("testMachine");
    }
}

假设ConditionService的实现为

当上下文不为空就满足条件,为空则不满足条件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class ConditionServiceImpl implements ConditionService {

    /**
     * 通过条件
     *
     * @return Condition
     */
    @Override
    public Condition<Context> passCondition() {
        return context -> {
            if (context!=null) {
                return true;
            }
            return false;
        };
    }

假设ActionService的实现为

更新金额,同时更新状态,之后推送通知事件进行后续异步操作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class ActionServiceImpl implements ActionService {
    
    @Autowired
    private PriceManager priceManager;
    
    @Autowired
    private StatusManager statusManager;
    
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    /**
     * 通过执行动作
     *
     * @return Action
     */
    @Override
    public Action<State, Event, Context> passAction() {
        return (from, to, event, context) -> {
            priceManager.updatePrice(context.getPrice());
            statusManager.updateStatus(to.getCode());
            NoticeEvent noticeEvent = context.toNoticeEvent();
            applicationEventPublisher.publishEvent(noticeEvent);
        };
    }
}

NoticeListener监听者

假设这里只是记录操作日志

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
public class NoticeListener {

    @Autowired
    private LogManager logManager;

    @Async(value = "EventExecutor")
    @EventListener(classes = NoticeEvent.class)
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void noticeEventAction(NoticeEvent noticeEvent) {
        logManager.log(noticeEvent);
    }
}

上述代码正常运行时没有问题,但这时候有的同学就会想到,想要金额和状态的更新具有一致性,不能更新了金额之后更新状态失败了。

想要保证两个操作的一致性,最简单的方式就是加上@Transactional注解,使得两个操作要么一起成功,要么一起失败

于是ActionService的代码在改动后可能是这样的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class ActionServiceImpl implements ActionService {
    
    @Autowired
    private PriceManager priceManager;
    
    @Autowired
    private StatusManager statusManager;

    /**
     * 通过执行动作
     *
     * @return Action
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Action<State, Event, Context> passAction() {
        return (from, to, event, context) -> {
            priceManager.updatePrice(context.getPrice());
            statusManager.updateStatus(to.getCode());
            NoticeEvent noticeEvent = context.toNoticeEvent();
            applicationEventPublisher.publishEvent(noticeEvent);
        };
    }
}

对应的NoticeListener改为@TransactionalEventListener,以适应在上文事务提交后再执行

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
public class NoticeListener {

    @Autowired
    private LogManager logManager;

    @Async(value = "EventExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, classes = NoticeEvent.class)
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void noticeEventAction(NoticeEvent noticeEvent) {
        logManager.log(noticeEvent);
    }
}

修改完成后在单测中发现了2个现象

  1. 如果其中一个更新失败了,另外一个并没有回滚
  2. 如果两个都没有更新失败,NoticeListener并没有成功监听到事件

在确认ActionServiceNoticeListener无配置遗漏的地方,无典型事务失效场景,搜索半天@TransactionalEventListener监听不起作用的原因无果后,我又仔细检查了StateMachine类中whenperform的调用,也都是通过@Autowired的类进行调用的,没有产生AOP的自调用问题。代码改造后看起来很正常,按理来说不应该出现这个问题。

在百思不得其解的时候,我发现本地的日志输出稍微和平时有些不一样,在执行上述Action逻辑时,没有mybatis-plus的事务相关日志。于是想到可能@Transactional根本没有切到Action方法。

再仔细扫了眼Action逻辑可以看出写法是采用的匿名方法形式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
@Transactional(rollbackFor = Exception.class)
public Action<State, Event, Context> passAction() {
    return (from, to, event, context) -> {
        priceManager.updatePrice(context.getPrice());
        statusManager.updateStatus(to.getCode());
    };
}

实际上非匿名方法写法等价于

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
@Transactional(rollbackFor = Exception.class)
public Action<State, Event, Context> passAction() {
    Action<State, Event, Context> action = new Action<>() {
        @Override
        public void execute(State from, State to, Event event, Context context) {
			priceManager.updatePrice(context.getPrice());
        	statusManager.updateStatus(to.getCode());
        }
    }
    return action;
}

可以看到匿名方法实际为execute

我在状态机的使用过程中并没有直接调用该方法,所以只能是由框架内部调用的。

问题剖析

重新回到状态机开始执行的地方

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void fire(State state, Event event, Context context) {
    StateMachine<State, Event, Context> stateMachine = StateMachineFactory.get("testMachine");
    stateMachine.fireEvent(state, event, context);
}

跟进去fireEvent方法,可以看到第36行判断当前的状态、时间、上下文是否能够转移,如果能够进行转移则进入到第43

之后便是校验的逻辑,当我们的action不为空的时候,便执行91行的action.execute()

这时候我们可以看到此时的action实际上就是ActionSeriveImpl,而真正的execute实现也在ActionSeriveImpl中,于是产生了AOP自调用问题,由于无法获取到代理对象事务切面自然就不会生效了

这里的action变量则是由状态机定义时所赋值的,点击setAction方法,全局只有2个地方使用到了,一个在批量的状态流转的实现类中,一个在单个的状态流转的实现类中

批量流转

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
public void perform(Action<S, E, C> action) {
    for(Transition transition : transitions){
        transition.setAction(action);
    }
}

单个流转

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
public void perform(Action<S, E, C> action) {
    transition.setAction(action);
}

代码很简单,注意函数签名都为perform,这就是状态机定义时的连贯接口

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Bean
public StateMachine<State, Event, Context> stateMachine() {
    StateMachineBuilder<State, Event, Context> builder = StateMachineBuilderFactory.create();
    builder.externalTransition().from(State.TEST).to(State.DEPLOY)
        .on(Event.PASS)
        .when(conditionService.passCondition())
        .perform(actionService.passAction());
    return builder.build("testMachine");
}

在这里actionService.passAction()看上去是一次service调用,实际上并没有实际调用execute方法

passAction的接口定义为Action<State, Event, Context>,这里仅仅是将定义好的action函数通过perform接口赋值到状态机内部而已。真正的执行,需要在fireEvent之后。

解决方法

在了解了问题所在之后,便是想办法进行解决。

通常来说一个AOP自调用的解决方法可以为如下2点

  1. 在自调用类中注入自己(仅限低版本Springboot,在高版本中会有循环依赖检测)
  2. 采用AopContext.currentProxy()获取当前类的代理对象,用代理对象进行自身方法的调用

很可惜,两种方法在当前场景都不适用,因为自调用在COLA框架内部,如果为了解决这个问题去再包装框架就有点大动干戈了。

方法一

既然没有声明式事务,直接采用编程式事务就好了

改进后的Action代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class ActionServiceImpl implements ActionService {
    
    @Autowired
    private PriceManager priceManager;
    
    @Autowired
    private StatusManager statusManager;
    
    @Autowired
    private DataSourceTransactionManager dataSourceManager;

    /**
     * 通过执行动作
     *
     * @return Action
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Action<State, Event, Context> passAction() {
        return (from, to, event, context) -> {
            TransactionStatus begin = dataSourceManager.getTransaction(new DefaultTransactionAttribute());
            try {
                priceManager.updatePrice(context.getPrice());
                statusManager.updateStatus(to.getCode());
                NoticeEvent noticeEvent = context.toNoticeEvent();
                applicationEventPublisher.publishEvent(noticeEvent);
                dataSourceManager.commit(begin);
            } catch (Exception e) {
                dataSourceManager.rollback(begin);
            }
        };
    }
}

需要注意的是,applicationEventPublisher.publishEvent(noticeEvent);需要放在dataSourceManager.commit(begin);前,这样@TransactionalEventListener才能正确监听到,如果放在commit之后,上文事务会做完提交和释放SqlSession的动作,后续的监听者无法监听一个已释放的事务。

对应的控制台日志为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@295854a]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@295854a]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@295854a]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@295854a]

方法二

回顾上面的状态机定义,我假定的是你是这样实现的状态机

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
public class StateMachine {
​
    @Autowired
    private ConditionService conditionService;
​
    @Autowired
    private ActionService actionService;
​
    @Bean
    public StateMachine<State, Event, Context> stateMachine() {
        StateMachineBuilder<State, Event, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition().from(State.TEST).to(State.DEPLOY)
                .on(Event.PASS)
                .when(conditionService.passCondition())
                .perform(actionService.passAction());
        return builder.build("testMachine");
    }
}

其中ConditionService和ActionService定义了Contion和Action接口的返回,然后在内部实现了匿名类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface ConditionService {
​
    Condition<AuditContext> passOrRejectCondition();
​
    Condition<AuditContext> doneCondition();
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface ActionService {
​
    Action<AuditState, AuditEvent, AuditContext> passOrRejectAction();
​
    Action<AuditState, AuditEvent, AuditContext> doneAction();
}

但其实正确的做法是直接实现Condition或Action的接口,将实现类定义为Bean,传递这个Bean到状态机定义中,从根本上解决事务失效问题

上述代码应该转化为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
public class StateMachine {
​
    @Resource
    @Qualifier("conditionImpl")
    private Condition<Context> conditionImpl;
​
    @Resource
    @Qualifier("actionImpl")
    private Action<State, Event, Context> actionImpl;
​
    @Bean
    public StateMachine<State, Event, Context> stateMachine() {
        StateMachineBuilder<State, Event, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition().from(State.TEST).to(State.DEPLOY)
                .on(Event.PASS)
                .when(conditionImpl)
                .perform(actionImpl);
        return builder.build("testMachine");
    }
}

对应的接口实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
public class ConditionImpl implements Condition<Context> {
​
    @Override
    public boolean isSatisfied(Context context) {
        return false;
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
public class ActionImpl implements Action<State, Event, Context> {
​
    @Override
    @Transactional
    public void execute(State from, State to, Event event, Context context) {}
}

由于传递的直接是Bean,所以就不再存在匿名类自调用的问题,在Action或Condition的实现方法executeisSatisfied上增加@Transactional即可让事务生效

总结

有的时候Spring代码写多了,看起来代码和平时没区别,实际上在特殊场景还是会踩坑,当事务和其他框架结合时一定要注意潜在的事务问题,做好单元测试

另外,状态机具有天生幂等的特点,不仅仅可以用于这种场景重Condition或Action的场景,在DDD中它可以作为维护某个状态的方法,用于充血模型

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-05-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
管理订单状态,该上状态机吗?轻量级状态机COLA StateMachine保姆级入门教程
在平常的后端项目开发中,状态机模式的使用其实没有大家想象中那么常见,笔者之前由于不在电商领域工作,很少在业务代码中用状态机来管理各种状态,一般都是手动get/set状态值。去年笔者进入了电商领域从事后端开发。电商领域,状态又多又复杂,如果仍然在业务代码中东一块西一块维护状态值,很容易陷入出了问题难于Debug,难于追责的窘境。
蛮三刀酱
2022/06/01
3.7K0
github上fork2.4k,star8.7k的这款状态机,原来长这样!
上一篇文章《关于状态机的技术选型,最后一个真心好》我跟大家聊了一下关于”状态机“的话题。从众多技术选型中我也推荐了一款阿里开源的状态机—“cola-statemachine”。
陶朱公Boy
2023/01/07
1K0
github上fork2.4k,star8.7k的这款状态机,原来长这样!
聊聊Cola-StateMachine轻量级状态机的实现
在分析Seata的saga模式实现时,实在是被其复杂的 json 状态语言定义文件劝退,我是有点没想明白为啥要用这么来实现状态机;盲猜可能是基于可视化的状态机设计器来定制化流程,更方便快捷且上手快吧,毕竟可以通过UI直接操作,设计状态流转图,但我暂时不太能get到。对于Saga模式的实现,之前的博文中已经阐述了基于状态机模式实现Saga,是比较常见且合适的做法,因此了解了下Java中的状态机实现方案,以后有相关的业务场景也可以直接上手使用状态机。
Ryan_OVO
2023/10/19
1.1K0
聊聊Cola-StateMachine轻量级状态机的实现
Cola-StateMachine状态机的实战使用
在电商领域,很多业务对象都是有状态的,且这些对象的状态又多又复杂。硬编码的方式已经不适合管理当前复杂业务对象的状态。为了适配复杂多变的业务,可以使用状态机来管理状态,统一定义业务对象状态和状态的流转。接下来,本文会重点介绍状态机相关的概念和使用场景。
政采云前端团队
2023/10/24
6.8K0
Cola-StateMachine状态机的实战使用
状态机入门实践
状态机是“有限状态自动机”的简称,是一种描述和处理事物状态变化的数学模型。本质上来讲,就是一种比if...else结构更加优雅,并具备可扩展性的状态转移处理机制。有多种实现方案,如:枚举,Spring Statemachine,cola state machine。
编程随笔
2024/03/30
1940
状态机入门实践
状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02
营销自动化平台支持多种不同类型运营活动策略(比如:短信推送策略、微信图文推送策略、App Push推送策略),每种活动类型都有各自不同的执行流程和活动状态。比如短信活动的活动执行流程如下:
2020labs小助手
2022/04/11
1.2K0
状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02
COLA-statemachine在多级审核业务中的实践
在实际的项目开发中,开发者经常会遇见类似多级审核之类的开发需求,比如某个文件审核,需要经过申请->直系领导审核->总经理审核等多个步骤。如果是一次动作触发整个审核过程,开发者可能会想到使用责任链模式来进行开发。但如果多级审核的间隔时间长,审核触发的条件不一样,责任链模式会不太能够解耦这项需求。如果采用平铺直叙式开发,无疑会将审核状态转移过程散落在系统间各个位置,前后两个状态之间的关系没有直观进行维护,同时状态转移时的条件、执行的方式和状态之间的逻辑关系很容易让开发者写出“面条代码”。在项目开发初期可能还好,随着需求的增量变化,平铺直叙式开发将使得状态转移逻辑和业务逻辑高度混合,且每增加一级节点审核,就要新增对应的审核状态及状态转移的逻辑,长此以往变得难以阅读和维护。所以,在这种情况下使用状态机这样建模方式就显得尤为必要。
benym
2023/10/18
1.5K1
COLA-statemachine在多级审核业务中的实践
关于状态机的技术选型,最后一个真心好!
今天想跟大家分享一个关于“状态机”的话题。状态属性在我们的现实生活中无处不在。比如电商场景会有一系列的订单状态(待支付、待发货、已发货、超时、关闭);员工提交请假申请会有申请状态(已申请、审核中、审核成功、审核拒绝、结束);差旅报销单会有单据审核状态(已提交、审核中、审核成功、退回、打款中、打款成功、打款失败、结束)等等。
陶朱公Boy
2023/01/07
3.1K0
关于状态机的技术选型,最后一个真心好!
Spring Statemachine的应用
在开发中总会遇到这样的场景,比如工单状态,流程状态,通过状态判断该执行的操作,不断改动的需求导致永无止境的 IF、ELSE 和 BREAK 子句的层次结构,当事情开始看起来太复杂时,简直就像面满池子的海洋球。
张云飞Vir
2023/05/01
8350
Spring-statemachine实现订单状态机
每次用到的时候新创建一个状态机,太奢侈了,官方文档里面也提到过这点。而且创建出来的实例,其状态也跟当前订单的不符;spring statemachine暂时不支持每次创建时指定当前状态,所以对状态机引擎实例的持久化,就成了必须要考虑的问题。
才疏学浅的木子
2023/10/17
1.1K0
设计模式如何提升 vivo 营销自动化业务扩展性 | 引擎篇01
在《vivo 营销自动化技术解密 |开篇》中,我们从整体上介绍了vivo营销自动化平台的业务架构、核心业务模块功能、系统架构和几大核心技术设计。
2020labs小助手
2021/10/11
6970
设计模式如何提升 vivo 营销自动化业务扩展性 | 引擎篇01
啪!啪!@Transactional 注解的12种失效场景,这坑我踩个遍
在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。
程序员小富
2021/09/24
3.9K1
啪!啪!@Transactional 注解的12种失效场景,这坑我踩个遍
数据库事务提交后才发送MQ消息解决方案
在项目开发中常常会遇到在一个有数据库操作的方法中,发送MQ消息,如果这种情况消息队列效率比较快,就会出现数据库事务还没提交,消息队列已经执行业务,导致不一致问题。举个应用场景,我们提交一个订单,将流水号放在MQ里,MQ监听到后就会查询订单去做其它业务,如果这时候数据库事务还没提交,也就是没生成订单流水,MQ监听到消息就去执行业务,查询订单,肯定会出现业务不一致问题
SmileNicky
2023/11/03
1.2K0
数据库事务提交后才发送MQ消息解决方案
Spring事务为什么会失效?
如果对AOP的实现不太熟悉的话可以看我之前的文章,或者到我网站www.javashitang.com上查看系列文章
Java识堂
2022/05/19
5450
Spring事务为什么会失效?
Spring事务详解
最近在项目组的业务技术分析会上,有同事遇到事务的失效的场景导致线上业务不可用。如果对Spring事务的@Transactional理解有限的话,确实很容易在开发中忽视一些细节问题,导致业务不可用的Bug。既然发生了问题,那么必然是要总结和反省的,然后我今天这里有时间总结一下各种事务失效的问题。
麋鹿大哥
2020/11/09
9390
java 事务嵌套_Java事务以及嵌套事务[通俗易懂]
2、如果事务中存在异常,只要对异常进行捕获和处理,都为执行成功,否则都不会执行成功。
全栈程序员站长
2022/11/07
3K0
状态机
下面的内容基于https://github.com/alibaba/COLA。COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。目前COLA已经发展到COLA v4。
路行的亚洲
2023/02/28
1.2K0
状态机
Spring事务失效场景
如果@Transactional 没有特别指定,Spring 只会在遇到运行时异常RuntimeException或者error时进行回滚,而IOException等检查异常不会影响回滚。
科技新语
2022/12/22
4320
Spring事务失效场景
SpringBoot集成Spring Statemachine(状态机)示例
本文将以电商项目中的订单状态转换这个典型的场景。从订单的创建到支付、发货、完成等状态来使用状态机进行管理。如果使用传统的if-else或者switch语句来管理这些状态,代码会变得非常臃肿且难以维护。而状态机提供了一种更加结构化和可维护的方式来管理这些状态转换。
程序员波特
2024/04/05
2K0
超轻量级有限状态机Mini-FSM
有限状态机(Finite State Machine,简称FSM),表示有限个状态以及在这些状态之间的转移和动作等行为的处理模型。在任何给定的时间点,有限状态机都处于某一特定状态,并且可以根据当前状态和输入条件,从当前状态转移到另一个状态。有限状态机相关的核心概念主要包括:
Yiwenwu
2024/05/18
9220
超轻量级有限状态机Mini-FSM
相关推荐
管理订单状态,该上状态机吗?轻量级状态机COLA StateMachine保姆级入门教程
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验