COLA-statemachine事务失效踩坑
# 背景
cola-statemachine
是阿里开源项目COLA
(opens new window)中的轻量级状态机组件。最大的特点是无状态、采用纯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");
}
2
3
4
5
6
7
8
2
3
4
5
6
7
8
上述代码翻译过来是
从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);
}
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
上述代码在纯Java
环境可以很好的运行,一般来说,开发者会进一步结合Spring
来完善多个状态机的获取
过程中通常会将状态机进行@Bean
注入,将passCondition()
和passAction()
独立出Service
以期望在后续操作中更好的利用Spring
的特性
简单改造后的状态机代码可能如下
// 以下代码并不是定义状态机的正确做法,错误的使用方法造成了事务失效,后面在方法二中解析
@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");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
假设ConditionService
的实现为
当上下文不为空就满足条件,为空则不满足条件
@Service
public class ConditionServiceImpl implements ConditionService {
/**
* 通过条件
*
* @return Condition
*/
@Override
public Condition<Context> passCondition() {
return context -> {
if (context!=null) {
return true;
}
return false;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
假设ActionService
的实现为
更新金额,同时更新状态,之后推送通知事件进行后续异步操作
@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);
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NoticeListener
监听者
假设这里只是记录操作日志
@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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
上述代码正常运行时没有问题,但这时候有的同学就会想到,想要金额和状态的更新具有一致性,不能更新了金额之后更新状态失败了。
想要保证两个操作的一致性,最简单的方式就是加上@Transactional
注解,使得两个操作要么一起成功,要么一起失败
于是ActionService
的代码在改动后可能是这样的
@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);
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
对应的NoticeListener
改为@TransactionalEventListener
,以适应在上文事务提交后再执行
@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
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
修改完成后在单测中发现了2个现象
- 如果其中一个更新失败了,另外一个并没有回滚
- 如果两个都没有更新失败,
NoticeListener
并没有成功监听到事件
在确认ActionService
和NoticeListener
无配置遗漏的地方,无典型事务失效场景,搜索半天@TransactionalEventListener
监听不起作用的原因无果后,我又仔细检查了StateMachine
类中when
和perform
的调用,也都是通过@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());
};
}
2
3
4
5
6
7
8
2
3
4
5
6
7
8
实际上非匿名方法写法等价于
@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;
}
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
可以看到匿名方法实际为execute
我在状态机的使用过程中并没有直接调用该方法,所以只能是由框架内部调用的。
# 问题剖析
重新回到状态机开始执行的地方
public void fire(State state, Event event, Context context) {
StateMachine<State, Event, Context> stateMachine = StateMachineFactory.get("testMachine");
stateMachine.fireEvent(state, event, context);
}
2
3
4
2
3
4
跟进去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);
}
}
2
3
4
5
6
2
3
4
5
6
单个流转
@Override
public void perform(Action<S, E, C> action) {
transition.setAction(action);
}
2
3
4
2
3
4
代码很简单,注意函数签名都为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");
}
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
在这里actionService.passAction()
看上去是一次service
调用,实际上并没有实际调用execute
方法
passAction
的接口定义为Action<State, Event, Context>
,这里仅仅是将定义好的action
函数通过perform
接口赋值到状态机内部而已。真正的执行,需要在fireEvent
之后。
# 解决方法
在了解了问题所在之后,便是想办法进行解决。
通常来说一个AOP
自调用的解决方法可以为如下2点
- 在自调用类中注入自己(仅限低版本
Springboot
,在高版本中会有循环依赖检测) - 采用
AopContext.currentProxy()
获取当前类的代理对象,用代理对象进行自身方法的调用
很可惜,两种方法在当前场景都不适用,因为自调用在COLA
框架内部,如果为了解决这个问题去再包装框架就有点大动干戈了。
# 方法一
既然没有声明式事务,直接采用编程式事务就好了
改进后的Action
代码如下
@Service
public class ActionServiceImpl implements ActionService {
@Autowired
private PriceManager priceManager;
@Autowired
private StatusManager statusManager;
@Autowired
private DataSourceTransactionManager dataSourceManager;
/**
* 通过执行动作
*
* @return Action
*/
@Override
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);
}
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
需要注意的是,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]
2
3
4
2
3
4
# 方法二
回顾上面的状态机定义,我假定的是你是这样实现的状态机
@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");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
其中ConditionService和ActionService定义了Condition和Action接口的返回,然后在内部实现了匿名类
public interface ConditionService {
Condition<AuditContext> passOrRejectCondition();
Condition<AuditContext> doneCondition();
}
2
3
4
5
6
2
3
4
5
6
public interface ActionService {
Action<AuditState, AuditEvent, AuditContext> passOrRejectAction();
Action<AuditState, AuditEvent, AuditContext> doneAction();
}
2
3
4
5
6
2
3
4
5
6
但其实正确的做法是直接实现Condition或Action的接口,将实现类定义为Bean,传递这个Bean到状态机定义中,从根本上解决事务失效问题
上述代码应该转化为
@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");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
对应的接口实现
@Component
public class ConditionImpl implements Condition<Context> {
@Override
public boolean isSatisfied(Context context) {
return false;
}
}
2
3
4
5
6
7
8
2
3
4
5
6
7
8
@Component
public class ActionImpl implements Action<State, Event, Context> {
@Override
@Transactional
public void execute(State from, State to, Event event, Context context) {
}
}
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
由于传递的直接是Bean,所以就不再存在匿名类自调用的问题,在Action或Condition的实现方法execute
或isSatisfied
上增加@Transactional
即可让事务生效
# 总结
有的时候Spring
代码写多了,看起来代码和平时没区别,实际上在特殊场景还是会踩坑,当事务和其他框架结合时一定要注意潜在的事务问题,做好单元测试。
另外,状态机具有天生幂等的特点,不仅仅可以用于这种场景重Condition或Action的场景,在DDD中它可以作为维护某个状态的方法,用于充血模型
- 01
- SpringCache基本配置类05-16
- 03
- Rpamis-security-原理解析12-13