项目迭代中新老逻辑切流和对比-数据结果对比

  1. 项目迭代中新老逻辑切流-切换入口
  2. 项目迭代中新老逻辑切流 - 异步查询新接口
  3. 项目迭代中新老逻辑切流 - 新老数据对比

前言

前面说了在项目迭代过程中如何对接口进行迁移,迁移步骤中的新老接口逻辑的切流、新老逻辑多线程获取结果,那么这一节说一说这个迁移过程中的最后一步:流量数据对比。

只有所有新逻辑返回的数据和老逻辑返回的数据保持一致,才说明没有问题,对业务无损或者无感知,才能放心的所有流量给新逻辑开放。

有兴趣的可以翻一下上面的链接看完成的改造过程。

正文

我们那上一节改造代码继续进行改造。先放改造前的代码。

这里我们实现了进入方法时的流量切换开关,通过配置可以实时进行切换新老逻辑。

在切流之前执行老逻辑的时候还能异步执行新逻辑,得到新逻辑的结果。

@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 可以完全替换掉逻辑的类,你甚至可以将他删除,重新引用新逻辑的类。如果你处理完成,你甚至只需要进行删除就可以清除垃圾。

尾言

经过了三次更新,已经将迭代过程中逻辑改造的步骤呈现出来了,迁移改造一般是一个比较漫长的过程,在这个过程中不断发现问题并解决,才能完成迁移任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值