引言
最近参与了一个业务迁移的项目,需要把站点A迁移到站点B。不同的站点拥有各自独立的服务和数据库,可以说是毫无关联。为了兼容迁移过程中的存在的一部分特殊交易数据(正向[支付]交易在站点A,但逆向[退款]操作在站点B操作),因此需要做站点A和站点B数据的关联,形如下图,在目前迁移站点落的单据拼接站点A的交易单号。
原本站点B表的字段设计长度为32,这样对站点A的单据做个拼接后就出现了字段超长的情况【针对特殊业务做的处理方式,非平台通用能力】,于是乎在落幂等表(一般是xx_unique表)时出现insert异常,但是这个异常并未直接阻断后续的业务流程,而如果以相同的内容(交易单据)再此发起接口请求,就可能会(本文中就是如果没将transId作为primary key的话,就会重复插入数据)产生了重复落(业务表)单的缺陷。
非字段超长的场景:第一笔请求进来,则会落幂等表一条数据、一笔业务表数据;第二笔重复请求,则会查询幂等表是否已经存在交易单,存在则阻断业务操作,反之继续走业务操作落幂等表&业务表数据。
大家应该了解到了,这其实也是一个幂等相关的缺陷,我先对幂等做个科普,再对缺陷进行复现&探讨解决方案。下面是百度百科对于幂等的解释:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号实现。
幂等的实现方式
先介绍一下几个概念:
-
幂等(去重)表,利用数据库表单的特性来实现幂等。以订单请求支付场景为例:将订单号orderId设为去重表的唯一索引,每次请求支付都根据订单号向去重表中插入一条数据,只有插入成功才继续执行支付操作,相当于在事务的开始阶段加锁。
-
(联合)主键。PRIMARY KEY 约束唯一标识数据库表中的每条记录。主键必须包含唯一的值。主键列不能包含 NULL 值。每个表都应该有一个主键,并且每个表只能有一个主键。
-
幂等返回的错误码。一般是REPEAT_REQUEST,返回上游用于判断是否重复请求。
基于 mysql 实现
这种实现方式是利用 mysql 唯一索引的特性。示意图如下:
具体流程步骤:
-
建立一张幂等表,其中某个字段需要建立唯一索引
-
客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中
-
因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑
-
如果插入失败,则代表已经执行过当前请求,直接返回
基于 redis 实现
这种实现方式是基于 SETNX 命令实现的。SETNX key value:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
该命令在设置成功时返回 1,设置失败时返回 0。示意图如下:
具体流程步骤:
-
客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段
-
将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间
-
如果设置成功,证明这是第一次请求,则执行后续的业务逻辑
-
如果设置失败,则代表已经执行过当前请求,直接返回
缺陷复盘
一句话缺陷描述:字段超长导致幂等表数据插入异常,但是这个异常【被捕获】导致未阻断业务流程。
测试用例:幂等测试,对接口A进行幂等测试,重复请求预期返回REPEAT_REQUEST,实际UN_EXCEPTION。
代码设计
原表结构,trans_id长度是32。联调阶段,发现字段超长会导致业务表insert异常,所以对aqc_trans表字段trans_id进行了扩容到64,但是此时没有对aqc_unique.trans_id字段进行扩容。
表名非真实业务表名,只做测试用
代码实现
/**
*/
// 接口请求实现逻辑
public class TransServiceImpl {
private TransResult trans(String transId){
TransResult result = new TransResult();
// 幂等检查
UniqueCheckResult uniqueCheckResult = UniqueCheckServiceImpl.uniqueCheckAndInsert(transId);
if (uniqueCheckResult.isExist){
result.setSuccess(true);
result.setErrorDescription("REPEAT_REQUEST");
return result;
}
// 执行业务流程
TransDo transDo = new TransDo();
transDo.setTransId(transId);
Date now = new Date();
transDo.setCreatedTime(now);
insert(transDo);
result.setSuccess(true);
return result;
}
}
// 幂等判断逻辑
public class UniqueCheckServiceImpl{
static UniqueCheckResult uniqueCheck(String transId){
UniqueCheckResult result = new UniqueCheckResult();
UniqueDo uniqueDo = uniqueQuery(transId);
if (uniqueDo!=null){
result.setExist(true);
return result;
}
return result;
}
static UniqueCheckResult uniqueCheckAndInsert(String transId){
UniqueCheckResult result = uniqueCheck(transId);
// 存在,直接return
if (result.isExist){
return result;
}
// 不存在, 需要创建unique数据
try{
UniqueDo uniqueDo = new UniqueDo();
uniqueDo.setTransId(transId);
insert(uniqueDo);
return result;
} catch (DataIntegrityViolationException e){
LogUtils.error("insert db exception");
return uniqueCheck(transId);
}
}
}
@Data
public class UniqueCheckResult {
boolean isExist = false;
void setExist(boolean isExist){
isExist = this.isExist;
}
}
缺陷分析
问题出在DataIntegrityViolationException,DataIntegrityViolationException是Spring框架中的一个异常类,它表示在尝试将数据插入、更新或删除到数据库时,由于数据完整性约束(例如唯一性约束、外键约束等)而导致的异常。通常情况下,这种异常是由于应用程序中的错误或者非法操作导致的,例如试图插入重复(异常)的数据或者试图删除具有外键约束的数据。
缺陷修复
缺陷的原因是,insert超长字段导致DataIntegrityViolationException被捕获,返回uniqueCheck()结果为false,进而继续执行业务流程。
可以看到上面的流程图红色箭头部分,目前缺陷就是走到了这条链路产生的【根因就是异常被捕获】;预期链路是绿色箭头表示的链路,即应该异常跳出,不执行业务逻辑。
因此目前的解法是更换被捕获的异常类型,替换成DuplicateKeyException,这样出现超长字段插入时就不会被捕获了,会直接抛出异常。
对缺陷的思考
缺陷介绍完了,但是对这个缺陷的争论(即是否将此问题归类为系统缺陷)仍没有定论。一方认为站点B当初设计表结构时候,字段长度的取值是经过业务发展评估的,请求过来的字段长度就应该不能超出定义的长度,即业务要run在平台设计能力范畴内,这样理论上永远不会存在字段超长的情况,因此这个问题不应该是缺陷,是预期外的业务接入导致的系统无法正常处理。
另一方(包括我)认为这就是一个缺陷,1. 系统自开发完成后并不是一成不变的,它需要随着业务不断演进【演进性】,即平台是服务于业务的。2. 对于测试来说,边界值测试是非常重要的,特别是异常边界值场景。3. 这个缺陷也暴露出系统设计的一大缺陷,即接口的参数校验是不严谨的。
本系统对于接口的请求内容校验只有必传值层面,而字段类型和长度并未做校验,这就导致问题延伸到了DAO层,而这是非常不合理的(理论上check request阶段就应该拦截超长字段)。
针对这个缺陷,可以举一反三。1. 此问题可能同样存在于站点B其他系统。2. 对系统的所有接口请求参数做(异常)边界值测试,把其他潜在问题暴露出来。
- END -
下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!
往期推荐
聊聊工作中的自我管理和向上管理
经验分享|测试工程师转型测试开发历程
聊聊UI自动化的PageObject设计模式
细读《阿里测试之道》
我在阿里做测开