COLA-statemachine事务失效踩坑

2023-10-31 14:30

本文主要是介绍COLA-statemachine事务失效踩坑,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

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

问题场景

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

  • 创建状态机
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()内的逻辑

  • 执行状态机
/*** 根据当前状态、事件、上下文,进行状态流转** @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的特性

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

@Component
public class StateMachine {@Autowiredprivate ConditionService conditionService;@Autowiredprivate ActionService actionService;@Beanpublic 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的实现为

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

@Service
public class ConditionServiceImpl implements ConditionService {/*** 通过条件** @return Condition*/@Overridepublic Condition<Context> passCondition() {return context -> {if (context!=null) {return true;}return false;};}

假设ActionService的实现为

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

@Service
public class ActionServiceImpl implements ActionService {@Autowiredprivate PriceManager priceManager;@Autowiredprivate StatusManager statusManager;@Autowiredprivate ApplicationEventPublisher applicationEventPublisher;/*** 通过执行动作** @return Action*/@Overridepublic 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监听者

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

@Component
public class NoticeListener {@Autowiredprivate 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的代码在改动后可能是这样的

@Service
public class ActionServiceImpl implements ActionService {@Autowiredprivate PriceManager priceManager;@Autowiredprivate 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,以适应在上文事务提交后再执行

@Component
public class NoticeListener {@Autowiredprivate 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逻辑可以看出写法是采用的匿名方法形式

@Override
@Transactional(rollbackFor = Exception.class)
public Action<State, Event, Context> passAction() {return (from, to, event, context) -> {priceManager.updatePrice(context.getPrice());statusManager.updateStatus(to.getCode());};
}

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

@Override
@Transactional(rollbackFor = Exception.class)
public Action<State, Event, Context> passAction() {Action<State, Event, Context> action = new Action<>() {@Overridepublic void execute(State from, State to, Event event, Context context) {priceManager.updatePrice(context.getPrice());statusManager.updateStatus(to.getCode());}}return action;
}

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

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

问题剖析

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

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个地方使用到了,一个在批量的状态流转的实现类中,一个在单个的状态流转的实现类中

在这里插入图片描述

批量流转

@Override
public void perform(Action<S, E, C> action) {for(Transition transition : transitions){transition.setAction(action);}
}

单个流转

@Override
public void perform(Action<S, E, C> action) {transition.setAction(action);
}

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

@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代码如下

@Service
public class ActionServiceImpl implements ActionService {@Autowiredprivate PriceManager priceManager;@Autowiredprivate StatusManager statusManager;@Autowiredprivate 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的动作,后续的监听者无法监听一个已释放的事务。

对应的控制台日志为

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]

总结

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

这篇关于COLA-statemachine事务失效踩坑的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/316002

相关文章

Goland debug失效详细解决步骤(合集)

《Golanddebug失效详细解决步骤(合集)》今天用Goland开发时,打断点,以debug方式运行,发现程序并没有断住,程序跳过了断点,直接运行结束,网上搜寻了大量文章,最后得以解决,特此在这... 目录Bug:Goland debug失效详细解决步骤【合集】情况一:Go或Goland架构不对情况二:

MYSQL事务死锁问题排查及解决方案

《MYSQL事务死锁问题排查及解决方案》:本文主要介绍Java服务报错日志的情况,并通过一系列排查和优化措施,最终发现并解决了服务假死的问题,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录问题现象推测 1 - 客户端无错误重试配置推测 2 - 客户端超时时间过短推测 3 - mysql 版本问

mysql外键创建不成功/失效如何处理

《mysql外键创建不成功/失效如何处理》文章介绍了在MySQL5.5.40版本中,创建带有外键约束的`stu`和`grade`表时遇到的问题,发现`grade`表的`id`字段没有随着`studen... 当前mysql版本:SELECT VERSION();结果为:5.5.40。在复习mysql外键约

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

oracle数据库索引失效的问题及解决

《oracle数据库索引失效的问题及解决》本文总结了在Oracle数据库中索引失效的一些常见场景,包括使用isnull、isnotnull、!=、、、函数处理、like前置%查询以及范围索引和等值索引... 目录oracle数据库索引失效问题场景环境索引失效情况及验证结论一结论二结论三结论四结论五总结ora

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

SpringBoot嵌套事务详解及失效解决方案

《SpringBoot嵌套事务详解及失效解决方案》在复杂的业务场景中,嵌套事务可以帮助我们更加精细地控制数据的一致性,然而,在SpringBoot中,如果嵌套事务的配置不当,可能会导致事务不生效的问题... 目录什么是嵌套事务?嵌套事务失效的原因核心问题:嵌套事务的解决方案方案一:将嵌套事务方法提取到独立类

MySQL的索引失效的原因实例及解决方案

《MySQL的索引失效的原因实例及解决方案》这篇文章主要讨论了MySQL索引失效的常见原因及其解决方案,它涵盖了数据类型不匹配、隐式转换、函数或表达式、范围查询、LIKE查询、OR条件、全表扫描、索引... 目录1. 数据类型不匹配2. 隐式转换3. 函数或表达式4. 范围查询之后的列5. like 查询6

MySql 事务练习

事务(transaction) -- 事务 transaction-- 事务是一组操作的集合,是一个不可分割的工作单位,事务会将所有的操作作为一个整体一起向系统提交或撤销请求-- 事务的操作要么同时成功,要么同时失败-- MySql的事务默认是自动提交的,当执行一个DML语句,MySql会立即自动隐式提交事务-- 常见案例:银行转账-- 逻辑:A给B转账1000:1.查询

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因