前言
前面说了在项目迭代过程中如何对接口进行迁移,迁移步骤中的新老接口逻辑的切流、新老逻辑多线程获取结果,那么这一节说一说这个迁移过程中的最后一步:流量数据对比。
只有所有新逻辑返回的数据和老逻辑返回的数据保持一致,才说明没有问题,对业务无损或者无感知,才能放心的所有流量给新逻辑开放。
有兴趣的可以翻一下上面的链接看完成的改造过程。
正文
我们那上一节改造代码继续进行改造。先放改造前的代码。
这里我们实现了进入方法时的流量切换开关,通过配置可以实时进行切换新老逻辑。
在切流之前执行老逻辑的时候还能异步执行新逻辑,得到新逻辑的结果。
@Service
public class ARepository {
@Resource
private AMapper aMapper;
//注入开关
@Resource
private MapperSwitchHandler mapperSwitchHandler;
public AEntity query(long id) {
Supplier<AEntity> supplier = () -> mapperSwitchHandler.getNewARepository().query(id);
MapperSwitchHandler.Result<AEntity> result = mapperSwitchHandler.switchHandler(
"ARepository",
"query",
supplier
);
// flag 判断是否走新 sql ,true 标识走新 sql,直接返回;false 标识走老逻辑
if (result.isFlag()) {
return result.getData();
}
//原有的接口逻辑处理
AEntity entity = aMapper.query(id);
//下面是异步操作,不影响老逻辑接口返回
mapperSwitchHandler.getThreadPoolTaskExecutor().submit(() -> {
AEntity newAEntityResult = supplier.get();
//我们在这里对比老数据 entity 和 newAEntityResult 的数据是否一致,下面的对比逻辑肯定是不对的,下节介绍
if (!newAEntityResult.equals(entity)) {
System.out.println(
"老数据:" + JSONObject.toJSONString(entity) + "\r\n" +
"新数据:" + JSONObject.toJSONString(newAEntityResult) + "\r\n"
);
throw new RuntimeException("数据不一致,请检查");
}
});
return entity;
}
}
我们这一节就是将新老逻辑的数据进行处理和对比,看它的结果是否一致。
我们创建一个数据对比工具类 DataDiffUtil
和 实体 DataDiffDTO
。
@Slf4j
@Service
public class DataDiffUtil {
@Resource
private Gson gson = new Gson();
/**
* 存已经对比的字段
*/
private static final ThreadLocal<Set<Object>> keySetThreadLocal = new ThreadLocal<>();
public Boolean check(DataDiffDTO dto) {
MDC.put("method", dto.getMethod());
Object oldRes = JSONObject.parseObject(dto.getReginJsonStr());
Object newRes = JSONObject.parseObject(dto.getNewJsonStr());
keySetThreadLocal.set(new HashSet<>());
try {
if (oldRes == null && newRes == null) {
return true;
} else if (oldRes == null || newRes == null) {
log.error("MarketingChecker, check {} not equal", JSONObject.toJSONString(dto));
return false;
}
//正反 check
boolean result = checkItem(oldRes, newRes);
boolean result2 = checkItem(newRes, oldRes);
return result & result2;
} catch (Exception e) {
log.error("MarketingChecker, check {} error,exception :", JSONObject.toJSONString(dto), e);
return true;
} finally {
log.info("MarketingChecker, check fields {}", JSONObject.toJSONString(keySetThreadLocal.get()));
MDC.remove("method");
keySetThreadLocal.remove();
}
}
/**
* @Author: zhou
* @Description: val 有三种情况,最终都会变成单字段比较
* 单字段: 直接比较
* 单实体: 遍历比较
* 数组: 遍历比较
* @Date: 2024/5/14 19:49
* @Param: oldMap
* @Param: newMap
* @return: boolean
*/
private boolean checkItem(Object oldObj, Object newObj) {
boolean result;
if (oldObj instanceof List && newObj instanceof List) {
//List 遍历
List<?> oldList = Lists.newArrayList();
oldList = gson.fromJson(gson.toJson(oldObj), oldList.getClass());
List<?> newList = Lists.newArrayList();
newList = gson.fromJson(gson.toJson(newObj), newList.getClass());
if (oldList.size() != newList.size()) {
log.error("MarketingChecker, method:[{}], list size not equal:{},{}", MDC.get("method"), oldList.size(), newList.size());
return false;
} else {
for (int i = 0; i < oldList.size(); i++) {
result = checkItem(oldList.get(i), newList.get(i));
if (!result) return false;
}
}
} else if (oldObj instanceof Map && newObj instanceof Map) {
//Map 遍历
Map<?, ?> oldMap = (Map<?, ?>) oldObj;
Map<?, ?> newMap = (Map<?, ?>) newObj;
Set<?> keyset = oldMap.keySet();
for (Object key : keyset) {
Set<Object> keySet = keySetThreadLocal.get();
keySet.add(key);
result = checkItem(oldMap.get(key), newMap.get(key));
if (!result) return false;
}
} else {
//单字段比较
result = oldObj.equals(newObj);
if (!result) {
log.error("MarketingChecker, method:[{}], check not equal,value={},{}",
MDC.get("method"),
JSONObject.toJSONString(oldObj),
JSONObject.toJSONString(newObj));
return false;
}
}
return true;
}
}
@Data
public class DataDiffDTO {
//方法名
private String method;
//老结果
private String reginJsonStr;
//新结果
private String newJsonStr;
}
然后改造一下 MapperSwitchHandler
,需要在里面注入 DataDiffUtil
。
@Resource
private DataDiffUtil dataDiffUtil;
接下来改造 ARepository
的数据对比部分。
@Service
public class ARepository {
@Resource
private AMapper aMapper;
//注入开关
@Resource
private MapperSwitchHandler mapperSwitchHandler;
public AEntity query(long id) {
Supplier<AEntity> supplier = () -> mapperSwitchHandler.getNewARepository().query(id);
MapperSwitchHandler.Result<AEntity> result = mapperSwitchHandler.switchHandler(
"ARepository",
"query",
supplier
);
// flag 判断是否走新 sql ,true 标识走新 sql,直接返回;false 标识走老逻辑
if (result.isFlag()) {
return result.getData();
}
//原有的接口逻辑处理
AEntity entity = aMapper.query(id);
//下面是异步操作,不影响老逻辑接口返回
mapperSwitchHandler.getThreadPoolTaskExecutor().submit(() -> {
AEntity newAEntityResult = supplier.get();
//我们在这里对比老数据 entity 和 newAEntityResult 的数据是否一致,下面的对比逻辑肯定是不对的,下节介绍
DataDiffDTO dataDiffDTO = new DataDiffDTO("ARepository.query", JSONObject.toJSONString(entity), JSONObject.toJSONString(newAEntityResult));
mapperSwitchHandler.getDataDiffUtil().check(dataDiffDTO);
});
return entity;
}
}
这样处理完成后,就可以在不影响老逻辑结果输出的情况下,执行了新逻辑得到结果并且和老逻辑结果进行对比。
在对比过程中如果对比不正确抛异常也不会影响原先的逻辑处理。
我们只要关注抛异常的这类情况。可以采取下面几种方法:
- 对异常的消息进行日志写入(需要定时查看)
- 对异常的消息存入数据库(需要定时查看)
- 对异常的消息进行监控并且及时报警(这个响应处理的相对及时一些)
完成这些工作以后,等到所有的结果都对齐不抛错,就可以将流量切入到新逻辑接口里面,待运行平稳后,jiu 可以完全替换掉逻辑的类,你甚至可以将他删除,重新引用新逻辑的类。如果你处理完成,你甚至只需要进行删除就可以清除垃圾。
尾言
经过了三次更新,已经将迭代过程中逻辑改造的步骤呈现出来了,迁移改造一般是一个比较漫长的过程,在这个过程中不断发现问题并解决,才能完成迁移任务。