框架qilu-state详解
框架qilu-state详解
背景
详细的交易一致性问题,请看<交易一致的由来>.
交易一致性的问题,解决的思路只有一个,那就是本地事务和远程调用的分离.虽然spring帮我们实现了在任意代码里面可以使用事务,但是一旦事务中包含远程调用,上述文章的内容就无法避免.
有的公司也使用"流程引擎"来解决,但是很不幸,如果引入"流程引擎",因为涉及到状态补单,"流程引擎"还有自己的保存事务,业务的订单也有自己的保存事务,引入"流程引擎"会因为额外的事务引入,会让情况变的更加复杂.qilu-state被设计成没有存储的最主要原因.
qilu-state本质上来说,是和分布式事务平台在解决相同的问题.但从对系统侵入性和适用性来看,qilu-state在这2个方面无与伦比.我非常推荐所有的核心交易类的处理,使用qilu-state来处理.
qilu-state的原则就是借助于业务的状态机,从根本上分离本地事务和远程调用的逻辑从而实现高可靠性.qilu-state在运行效果上,类似于内存状态机,在剥离本地事务和远程调用以后,借助于本地事务的ACID特性,实现高可靠的特性.
在介绍以前,希望各位明白
- 不是qilu-state抽象的复杂,是原本简单的业务,在高可靠性的要求下,就会变得非常复杂.框架的意义可以减少重复代码,但是不能减少有效代码,只是代码被编排在了不同的地方.
- 使用qilu-state的前提,一定是定义完整的状态机,有状态机才有流程.
- qilu-state的实现不利于开发人员在分享代码,串联逻辑,但是可以利用编程的技巧还弥补.
- 让高可靠性通用化和模式化,一些危险的代码也可以交给经验比较缺乏的同学来完成.
- qilu-state具备流程的含义,流程都有一个无法忽视的优点----只要测试完善,系统会超稳定
- 和没有框架比较起来,qilu-state可以节省重试部分的代码
- 和分布式事务平台比较起来,qilu-state轻量且注重内在(分布式事务平台需要借助于外在的平台功能完成内部逻辑的一致性)
使用
maven依赖
<dependency>
<groupId>com.9istock.base</groupId>
<artifactId>qilu-state</artifactId>
<version>版本号</version>
</dependency>配置
spring配置
qilu-state状态配置文件的载入
<bean class="com.istock.base.state.config.StateInitilizer" init-method="init">
<property name="location">
<list>
<value>classpath*:state-config-orderInfo.xml</value>
</list>
</property>
</bean>执行器的初始化
<bean name="stateExecutor" class="com.istock.base.state.execute.StateExecutor">
<property name="template" ref="transactionTemplate" />
<property name="dataSource" ref="dataSource" />
<property name="lockFactory" ref="lockFactory"></property>
</bean>qilu-state中支持的锁,根据实际需要载入
<bean name="lockFactory" class="com.istock.base.state.lock.LockFactory">
<property name="lockMap">
<map>
<!-- 记录状态乐观锁,修改记录状态 -->
<entry key="STATUS_LOCK" >
<bean class="com.istock.base.state.lock.StatusLockImpl">
<property name="template" ref="transactionTemplate" />
<property name="dataSource" ref="dataSource" />
</bean>
</entry>
<!-- 使用Lock_Info表的唯一索引冲突,构建锁 -->
<entry key="TABLE_LOCK" >
<bean class="com.istock.base.state.lock.TableLockImpl">
<property name="dataSource" ref="dataSource" />
</bean>
</entry>
<!--使用第三方缓存锁-->
<entry key="CACHE_LOCK" >
<bean class="com.istock.base.state.lock.CacheLockImpl">
<property name="cacheManager" ref="baseCacheManager" />
</bean>
</entry>
</map>
</property>
</bean>qilu-state的配置
<?xml version="1.0" encoding="UTF-8"?>
<root>
<!-- 配置一个订单类,qilu-state通过class类型来识别订单 -->
<!-- idQuery 存储的是属性字段,主要用于上下文中订单状态的更新,会重新使用数据库的信息更新上下文中的订单 status是订单中的状态字段,用于状态识别 -->
<typeDef name="orderInfo" class="com.istock.state.model.OrderInfo" tableName="TB_ORDER_INFO" idQuery="id" status="status"/>
<typeDef name="orderApply" class="com.istock.state.model.OrderApply" tableName="TB_ORDER_APPLY" idQuery="id" status="status"/>
<!-- 订单的执行逻辑 -->
<!-- orderInfo 的处理逻辑,插入记录,orderInfo状态为1,未处理 -->
<!-- 变更为状态2,处理中,同时需要插入一条orderApply -->
<order name="orderInfo" >
<!-- 执行器的执行条件 -->
<state name="test1" expression="order != null and order.status == 1" ref="spring:orderInfoHandler.execute">
<lock type="TABLE_LOCK">
<property name="EXPIRE_TIME" value="5"/>
</lock>
<!-- 执行器返回后的事务更新 -->
<!-- 一个result类型,可以配置多个save,多个save在同一个事务中,任何一个异常都会引发当前的result回滚 -->
<result type="S">
<save invoke="@orderApplyManager.createOrderApply(order.id)"></save>
<!-- 出现订单外的更新,直接调用manager或者service,使用spring的name和方法,直接调用,支持传值 -->
</result>
<!-- rowCheck是执行save之后的检查,如果更新的记录不是rowCheck的配置,将会回滚事务 -->
<result type="F">
<save name="orderInfo" rowCheck="1">
<set field="status" value="1" />
<condition field="status" value="2" />
<condition field="id" expression="#order.id#" />
</save>
</result>
<!-- process=true 设置当前的路由返回是一个临时状态,需要让主流程退出等待的标识 -->
<result type="P" process="true">
<!-- notify 当前result的本地事务提交以后,对对象的状态进行推进 -->
<!-- notify一定会触发一次内存对象的更新,使用condition或者idQuery进行变更内存对象 -->
<!-- 内存对象变更完成以后,会重新跑对应对象的state状态判断 -->
<notify type="orderPay">
<!-- notify存在condition节点,表明推进的不是当前的对象 -->
<!-- 如果推进的是当前对象,condition的状态可以省略 -->
<!-- 主订单推从订单,不保证唯一,orderId和status需要同时满足 -->
<condition field="orderId" expression="#order.id#"/>
<condition field="status" value="2" />
</notify>
</result>
</state>
</order>
</root>执行流程
基于伪代码的执行流程
数据库对象 = 根据order类型-->javaType,从javaType配置的id,获取数据库对象
执行上下文 = 构建上下文(数据库对象)
while(匹配是否存在适用的state(执行上下文)){
@NOT_SUPPORT{
执行结果 = 执行handler(上下文)
}
保存信息 = 检查state的result结果,进行匹配(执行结果)
@REQUIRES_NEW{
执行保存(state配置配置/调用spring方法)
}
//查找下一个当前的对象,是否有配置是否可以匹配继续执行
数据库对象 = 根据order类型-->javaType,从javaType配置的id,获取数据库对象
执行上下文 = 构建上下文(数据库对象)
}配置说明
typeDef节点
| 属性 | 说明 |
|---|---|
| name | 订单类的别名,用于order和state中的引用 |
| class | 订单的java类,context中的order是一个object,使用object的class来识别订单 |
| tableName | 当前的订单再数据库表中的表名 |
| idQuery | 记录的id字段,用于唯一标识一笔订单,在loopCondition被触发以后,需要从数据库更新上下文中的订单信息,会触发一个id的查询 |
| status | 订单记录的状态字段,用于state执行前的状态检查 |
order节点
| 属性 | 说明 |
|---|---|
| name | 订单的别名,绑定typeDef的name,用于通过class来识别订单 |
state节点
| 属性 | 说明 |
|---|---|
| name | 当前state的别名,用于被其他配置的绑定比如stateMap.state |
| status | 当前的匹配的简单配置,比如status=”1”,那么只有订单状态为1的,才能进入,status判断优先 |
| expression | 当前状态匹配的复杂配置,可能有2个字段需要判断或者在判断条件中存在or和and,使用springEL的方式配置表达式,返回为true,才能进入当前配置,优先级在status之后 |
| ref | 当前状态的handler配置,在执行完order.state.lock以后,就会执行ref配置的handler,这边配置的是handler需要满足spring:BeanId.methodName格式,比如:spring:EmptyHandler.execute |
lock节点
| 属性 | 说明 |
|---|---|
| type | 锁的类型,支持的锁类型,在spring配置的lockFactory中锁类型如下,默认为Status_Lock. Status_Lock,状态锁,是直接更新order的status字段,必须配置save节点 Table_Lock,通用的表锁,将锁保存在ST_TABLE_LOCK_INFO中,不对业务订单表进行操作 Cache_Lock,缓存锁,将锁信息使用缓存进行保存,需要配置cacheManager,使用cacheManager映射到不同的缓存 |
| 子节点Property | 锁的属性节点,用于描述锁的属性信息 |
| EXPIRE_TIME | 锁的超时时间,单位秒,对于StatusLock无效 |
result节点
Result节点用于配置handler执行后,对结果的路由 在handler执行后,返回的字符串会路由决定执行哪个路径. Result可以包含多个save节点,result在执行前,会开启executor中注入的datasource的一个事务,所有的save节点都在这个事务内,如果有某一个save节点执行失败,这个事务将会回滚,且execute方法退出,当前调用返回FAIL,节点状态为SAVE_EXCEPTION.
| 属性 | 说明 |
|---|---|
| type | 路由标识,由handler执行返回值进行路由选择. |
save节点
| 属性 | 说明 |
|---|---|
| name | 用于指定对应的typeDef配置的订单别名,指定需要更新的订单 |
| rowCheck | 更新的结果检查,需要配置数字 使用update的返回值(更新记录条数)来判断当前的更新是否合法,如果不匹配,当前的事务将会回滚 |
| condition | 进入当前save的条件,配置一个springEL表达式,可以使用上下文,如果表达式执行为false,将不会触发当前的save |
| invoke | 不使用qilu-state生成的update语句操作,使用spring的beanName,注意参数的类型 |
notify节点
| 属性 | 说明 |
|---|---|
| type | 通知的对象类型,需要再typeDef中进行配置 |
| condition | 子节点,配置查询条件 如果condition不为空,则代表需要推进的是一个非当前state的对象,需要通过当前对象的部分属性进行查询 |
condition节点
| 属性 | 说明 |
|---|---|
| field | 操作的字段名,这边一般填写驼峰的属性名,在转化为数据库操作的时,会自动解成字段名 |
| value | 代表当前的这个条件配置,是一个静态值 |
| operator | 当前条件的操作符,默认为"=" |
| expression | 当前条件的取值,需要通过springEL在当前的context中进行获取 |
示例
@orderApplyManager.createOrderApply(order.id)
将会查询spring的beanFactory中的orderApplyManager的beanName,并调用createOrderApply方法,参数为上下文中的order.id属性
Condition节点 生成update语句的where部分
Set节点 生成update语句的set部分
Field 生成条件/set部分的字段,需要配置order对象的属性,默认会按照驼峰原则,反向生成表的字段名
Value 生成条件/set部分的值,不需要关注数据类型,会默认使用数据库的数据类型进行插入
Operator 生成操作符,针对condition节点,可以配置大于,小于,针对xml配置需要转义
Expression 生成条件/set部分的值,区别于value,expression可以允许从上下文中获取值,填写springEL表达式java调用
构建qilu-state的上下文,将需要操作的订单传入上下文,调用StateExecutor.execute方法.
//调用示例
ExecuteContext context = new ExecuteContext();
OrderInfo orderInfo = orderInfoManager.queryById(orderId);
context.setOrder(orderInfo);
ExecuteResult result = executor.execute(context);被qilu-state配置的订单对象,如果需要操作到状态变更,都只要写上述代码就能驱动qilu-state执行.
这边的代码可以在controller中触发,也可以在MQ的listener中触发,也可以在定时任务中触发.
当然,如果是MQ的listener中触发,一般都需要传入MQ的消息数据,一般通过order.extention操作.
返回值说明
如果在state内部配置了notify,那么qilu-state会执行完一个state就通知一个state,直到无法处理或者处理失败.
注意
依据这个特性,我们可以不再把接口定义为"同步接口"或者"异步接口",如果qilu-state能直接处理到完结状态,那么调用就是同步的,如果qilu-state无法直接处理到完结状态,那才是异步的.
ExecuteResultEnum
| 属性 | 说明 |
|---|---|
| resultEnum | 当前调用的结果 这是一个枚举值,有如下三种结果 SUCCESS,代表当前调用成功,没有错误 PARTLY_SUCCESS,代表当前调用有多个步骤,最后一个步骤失败,前面的步骤是成功的 PROCESSING,指代当前的状态进入了一个不确定的情况,需要外部线程进行退出 FAIL,代表当前的处理是失败的. |
| lastStateEnum | 最后一个调用state的结果 这是一个枚举值,有如下几种结果 SUCCESS,handler调用成功,之后的result调用成功,事务提交,没有错误 NO_MATCH_STATE,没有匹配到对应的state执行器 NO_HANDLE,没有找到配置的ref的handler,也有可能是handler的canExecute返回了false LOCK_FAIL,调用lock的过程中,由lockFactory返回了false EXECUTE_EXECPTION,调用stateHandler的过程抛出了异常 SAVE_EXCEPTION,调用result的过程中某一个save出现了异常,回滚事务,抛出异常 CYCLE_EXCEPTION,在调用state的过程中,出现了state的重复 NO_SAVE,没有找到handler的返回值配置的result |
ExecuteContext
| 属性 | 说明 |
|---|---|
| Order | 代表订单对象的实例 qilu-state会使用order.class去匹配对应的state |
| Extention | 上下文的扩展字段,当次调用有效 会在不同的state中上下传递 |
| handlerResultMap | 所有步骤执行handler的结果,一个map,key是当前state的name 获取某一个步骤的result,context.getHandlerResultMap().get(“state-name”) |
| executeResultMap | 所有步骤的返回码 正常应该都是SUCCESS |
注意事项
qilu-state可以在状态设计准确的情况下,保证应用程序顺利执行,而且执行可以是同步的.所以,在设计一个接口的时候,可以不再预先设定一个接口是同步接口还是异步接口.而是可以根据业务需要,在后续接口提供同步请求的情况下,当前接口也可以提供同步接口,当当前的业务已经无法进行下去(可能是因为网络原因),提供异步处理.
