前言
在之前的文章手把手教你Spring Cloud集成Seata TCC模式中,实现了TCC方式完成购物车下单的分布式事务;在该案例中,我无意间发现了一个小BUG,下面我带大家通过源码分析来看一下为啥会出现这个BUG;
BUG复现
在TCC Action
中,我们在预扣款
接口的请求参数中有一个Long
类型的参数amount
,示例如下:
/** * 预扣款 * * @param businessActionContext * @param userId * @param amount * @return */ @TwoPhaseBusinessAction(name = "prepareDeductMoney", commitMethod = "commitDeductMoney", rollbackMethod = "rollbackDeductMoney") boolean prepareDeductMoney(BusinessActionContext businessActionContext, @BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "amount") Long amount); 复制代码
然后我们在提交或回滚逻辑中,需要拿到amount
这个参数值,并通过sql语句实现真实扣款
或者预扣款解除
,这个时候我们就需要从BusinessActionContext
中取出amount
参数值:
Map<String, Object> actionContext = businessActionContext.getActionContext(); Long amount = (Long) actionContext.get("amount"); 复制代码
但是如果我们按照上面的代码这样取值,就会立马报ClassCastException
异常:
Cannot cast 'java.lang.Integer' to 'java.lang.Long' 复制代码
根据以上异常,也就是说,我们直接从BusinessActionContext
中取出来的amount
参数值是java.lang.Integer
类型,是无法直接强转为java.lang.Long
的;可是我们明明在prepareDeductMoney()
方法中的amount
参数类型就是Long
类型,为啥取出来的时候变成了java.lang.Integer
类型呢?
BUG出现的原因
为了解答上面的疑问,我们来看一下Seata源码;我们的思路主要看两个点:
- Seata序列化
BusinessActionContext
对象
在ActionInterceptorHandler
中,需要注册分支事务,这个时候会同时携带BusinessActionContext
对象数据一起提交;我们知道java对象是无法直接在网络中传输的,所以在提交前做了一步序列化的操作:
Map<String, Object> applicationContext = Collections.singletonMap(Constants.TCC_ACTION_CONTEXT, context); // 执行JSON序列化操作 String applicationContextStr = JSON.toJSONString(applicationContext); 复制代码
在上述代码中,假如amount=1000
,那么amount
参数会被序列化成如下json字符串{"amount":1000}
;
- Seata反序列化
BusinessActionContext
对象
当我们的预扣款完成后,TM
会决议出提交或回滚,此时TC服务
会下发指令给到对应的RM
执行提交或回滚逻辑,并携带注册分支事务时提交的json字符串给到RM
,此时RM
将该json字符串进行反序列化并创建新的BusinessActionContext
对象;请查看TCCResourceManager
:
protected BusinessActionContext getBusinessActionContext(String xid, long branchId, String resourceId, String applicationData) { Map actionContextMap = null; if (StringUtils.isNotBlank(applicationData)) { // 反序列化成Map对象 Map tccContext = JSON.parseObject(applicationData, Map.class); actionContextMap = (Map)tccContext.get(Constants.TCC_ACTION_CONTEXT); } if (actionContextMap == null) { actionContextMap = new HashMap<>(2); } //instance the action context BusinessActionContext businessActionContext = new BusinessActionContext( xid, String.valueOf(branchId), actionContextMap); businessActionContext.setActionName(resourceId); return businessActionContext; } 复制代码
也就是说,Seata之前把amount
参数序列化成{"amount":1000}
,然后又通过反序列化把{"amount":1000}
转成Map对象,在反序列化过程中,fastjson
并不知道1000这个数值到底是java.lang.Integer
类型还是java.lang.Long
类型,所以默认转换成了java.lang.Integer
类型,也就造成了我们从BusinessActionContext
取出来的amount
是java.lang.Integer
类型。
如何应对
按照上面的源码分析,不仅仅是Long
类型会在反序列化的时候默认为java.lang.Integer
类型,其他需要注意的类型还包括char
、byte
、float
、double
、BigDecimal
等序列化后不能明确类型的数据;那么针对这种情况,我们在日常开发中应该如何应对呢?
根据我自己的使用体验,建议大家在commitMethod
或rollbackMethod
中通过BusinessActionContext
取值之前,先通过Debug方式把需要的数据类型打印出来,这样我们编码的时候就可以根据打印出来的数据类型灵活地进行处理了,千万不能先入为主地认为Seata会自动帮我们把数据类型转成了和参数列表中的类型一致。
最后,为了让Seata变得更好,昨天我已经顺便把这个Issue给提交上去了,静候Seata社区早日解决这个问题,让大家能够使用更加方便。
Issue如下:TCC模式中,BusinessActionContext中反序列化出来的数据类型有问题。