设计思路-通用的补偿

说明

放入的redis的好处是防止定时任务扫库,以下未封装 可以利用spring 生命周期进行更好的封装

场景

针对MQ发送消息

消费端为了防止队列阻塞,失败的不重新丢回队列的补偿 如需要保证原子性的获取锁的消费失败,或者timeout异常等

针对MQ发送端

发送消息失败的统一重新发送补偿

非MQ场景

针对非MQ也能保证最终一致性

 

表设计

DROP TABLE IF EXISTS common_process_log;
    CREATE TABLE `common_process_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `business_id` varchar(32) NOT NULL COMMENT '业务id',
  `business_ext_id` varchar(32) DEFAULT NULL COMMENT '扩展业务id',
  `batch_id` varchar(32) DEFAULT NULL COMMENT '针对批量处理记录批次号',
  `business_type` varchar(32) NOT NULL COMMENT '业务类型',
  `idempotent_id` varchar(32) NOT NULL COMMENT '幂等参数 唯一索引保证幂等',
  `content` text NOT NULL COMMENT '内容',
  `process_count` int(11) NOT NULL DEFAULT '1' COMMENT '处理次数',
  `delay_second` int(11) NOT NULL DEFAULT '0' COMMENT '延迟补偿时间,定义任务扫描处理时间',
  `max_process_count` int(11) NOT NULL DEFAULT '0' COMMENT '最大处理次数',
  `state` int(2) DEFAULT NULL COMMENT '推送状态 -2推送失败不参与后续补偿 -1_处理失败 0_待处理 1_处理成功 ',
  `user_id` bigint(20) DEFAULT NULL COMMENT '操作人id',
  `user_name` varchar(32) DEFAULT NULL COMMENT '操作人',
  `created_at` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime DEFAULT NULL COMMENT '最后一次处理时间',
  `trace_id` varchar(32) COMMENT '日志的traceId 通过它可以去日志系统获取相应的关联日志',
  PRIMARY KEY (`id`),
  UNIQUE KEY `common_process_log_idx_idempotent_id` (`idempotent_id`),
  KEY `common_process_log_idx_batch_id` (`batch_id`),
  KEY `common_process_log_idx_business_id` (`business_id`),
  KEY `common_process_log_idx_business_ext_id` (`business_ext_id`),
  KEY `common_process_log_idx_created_at` (`created_at`),
  KEY `common_process_log_idx_updated_at` (`updated_at`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='公共的处理日志';

 

 

代码实现

持久化事件

   /**
     * 内部很多判断标签是否存在 用户是否存在,如果上游是并发调用调用 可能会重复创建通过队列消费
     */
    @Override
    @Transactional(rollbackOn = Exception.class)
    public boolean sendSyncUserMessage(SyncUserMessageReqVo syncUserMessageReqVo) {
        List<CommonProcessLog> commonProcessLogs = new ArrayList<>();
        String batchId = UUID.randomUUID().toString().replace("-", "");
        for (SyncUserReqVo syncUserReqVo :
                syncUserMessageReqVo.getSyncUserReqVoList()) {
            CommonProcessLog commonProcessLog = new CommonProcessLog();
            commonProcessLog.setBatchId(batchId);
            commonProcessLog.setBusinessId(syncUserMessageReqVo.getProviderId().toString());
            commonProcessLog.setBusinessType(CommonProcessLog.BUSINESS_TYPE_SYNCUSER);
            commonProcessLog.setContent(JSON.toJSONString(syncUserReqVo));
            //状态待处理
            commonProcessLog.setState(0);
            //处理次数
            commonProcessLog.setProcessCount(0);
            commonProcessLog.setTraceId(EweiTLogUtils.getCurrentTraceId());
            //补偿机制延迟10秒,理论上如果被及时消费则不会被补偿
            commonProcessLog.setDelaySecond(10);
            //最大处理次数,如果多次补偿不能成功则需要人工干预
            commonProcessLog.setMaxProcessCount(10);
            //幂等参数 mysql唯一索引
            commonProcessLog.setIdempotentId(UUID.randomUUID().toString().replace("-", ""));
            commonProcessLog.setUserId(syncUserMessageReqVo.getOperationUserId());
            commonProcessLogs.add(commonProcessLog);
        }
        //持久化消息日志并存储id到redis,用于补偿扫描 后续发送mns消息 消费成功会从redis移除
        commonProcessLogService.batchSaveAndSetRedis(commonProcessLogs);
        // 如果阿里云消息服务开启,优先使用。 此开关是为了兼容独立版
        return BooleanUtil.isTrue(eweiAliyunMnsConfig.getAliyunMnsOn()) ?
                this.doAsyncSendMessageWithAliyun(syncUserMessageReqVo, batchId) : this.doAsyncSendMessageWithRedis(syncUserMessageReqVo);

    }

 

 

 @Override
    public void batchSaveAndSetRedis(List<CommonProcessLog> commonProcessLogs) {
        save(commonProcessLogs);
        //写入redis增加事物后置 事物提交后持久化到redis  用于补偿扫描com.ewei.account.task.SyncUserTask
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                for (
                        CommonProcessLog commonProcessLog :
                        commonProcessLogs) {
                    //redis score为当前时间+延迟时间,扫描通过zrangeByscore score为当前时间来完成延迟判断逻辑
                    double delayTimeMillis = System.currentTimeMillis() + commonProcessLog.getDelaySecond() * 1000;
                    redisOperationService.zaddWithPrefix(WAIT_PROCESS_LOG_KEY, delayTimeMillis, commonProcessLog.getId().toString(), WAIT_PROCESS_LOG_KEY_SAVE_SECONDS);
                }
            }
        });
    }

 

统一的补偿

 /**
     * 定时任务补偿失败补偿 com.ewei.account.task.SyncUserTask
     */
    @Override
    public void processScan() throws BusinessException {
        int offset = 0;
        int size = 20;
        //队列待消费数量越多 则每次最多偏移200
        int maxOffset = 200;
        Date currentDate = new Date();
        Long waitCount = redisOperationService.zCount(WAIT_PROCESS_LOG_KEY, 0, Long.valueOf(currentDate.getTime()).doubleValue());
        while (true && offset < maxOffset) {
            //score到当前时间的数据信息 实现延迟效果
            Set<String> ids = redisOperationService.zrangeByScoreWithPrefix(WAIT_PROCESS_LOG_KEY, 0, Long.valueOf(currentDate.getTime()).doubleValue(), offset, size);
            if (CollectionUtils.isEmpty(ids)) {
                log.info("[CommonProcessLogDao通用补偿]没有数据忽略,offset:{},count:{},Ids:{}", offset, waitCount, JSON.toJSONString(ids));
                break;
            }
            log.info("[CommonProcessLogDao通用补偿]执行补偿消费,offset:{},count:{},Ids:{}", offset, waitCount, JSON.toJSONString(ids));
            offset += size;
            //使用AopUtil 一个批次为一个事物
            List<Integer> alreadyProcessIds = AopUtil.proxy(this).processByIds(ids.stream().map(Integer::valueOf).collect(Collectors.toSet()));
            //内部已经将这些id从redis移除了,所以需要指针前移动
            if (!CollectionUtils.isEmpty(alreadyProcessIds)) {
                //指针前移
                offset -= alreadyProcessIds.size();
            }
        }
    }

 

  @Transactional(rollbackFor = Exception.class)
    @Override
    public List<Integer> processByIds(Set<Integer> ids) {
        List<Integer> alreadyProcessIds = new ArrayList<>();
        List<CommonProcessLog> commonProcessLogs = commonProcessLogDao.list(ids.toArray(new Integer[0]));
        //-----------未持久化的未从db查到的删除 表示通过id在数据库未找到-------------------
        if (CollectionUtils.isEmpty(commonProcessLogs)) {
            alreadyProcessIds.addAll(ids);
            //redis移除,防止下次再次被扫描到
            removeRedis(alreadyProcessIds);
            return alreadyProcessIds;
        }
        Map<Integer, Integer> idMaps = commonProcessLogs.stream().collect(Collectors.toMap(CommonProcessLog::getId, CommonProcessLog::getId, (c1, c2) -> c1));
        if (ids.size() > commonProcessLogs.size()) {
            List<Integer> notExists = ids.stream().filter(c -> !idMaps.containsKey(c)).collect(Collectors.toList());
            alreadyProcessIds.addAll(notExists);
        }

        //--------------------------------数据库已经是成功状态的,不需要重复处理移除--------------------------
        List<Integer> successIds = commonProcessLogs.stream().filter(c -> c.getState() == 1).map(CommonProcessLog::getId).collect(Collectors.toList());
        if (!CollectionUtils.isEmpty(successIds)) {
            commonProcessLogs.removeIf(c -> successIds.contains(c.getId()));
            alreadyProcessIds.addAll(successIds);
        }

        //排除后不需要处理,没有待处理数据直接返回 并从redis移除
        if (CollectionUtils.isEmpty(commonProcessLogs)) {
            removeRedis(alreadyProcessIds);
            return alreadyProcessIds;
        }

        for (CommonProcessLog commonProcessLog :
                commonProcessLogs) {
            //超过最大处理次数的忽略 并加入从redis移除集合 后续删除
            if (commonProcessLog.getProcessCount() >= commonProcessLog.getMaxProcessCount()) {
                alreadyProcessIds.add(commonProcessLog.getId());
                continue;
            }
            boolean successful = false;
            //暂时未抽象 后期使用可以抽象出handle
            if (CommonProcessLog.BUSINESS_TYPE_SYNCUSER.equals(commonProcessLog.getBusinessType())) {
                try {
                    //处理方法需要单独开事物,因为try catch 会影响外部事物的状态提交阶段会报错 内部如果加锁失败会抛出异常
                    successful = userService.syncUserByCommonProcessLog(commonProcessLog);
                } catch (BusinessException e) {
                    log.error("[CommonProcessLog异常]" + commonProcessLog.getId(), e);
                }
            } else {
                log.error("不支持的处理类型:{}", commonProcessLog.getBusinessType());
            }
            int state = -1;
            //处理成功的加入待移除队列
            if (successful) {
                state = 1;
                alreadyProcessIds.add(commonProcessLog.getId());
            }
            //更新处理状态和次数
            commonProcessLogDao.updateColumns(commonProcessLog.getId(), "state", state, "process_count", commonProcessLog.getProcessCount() + 1, "updated_at", new Date(), "trace_id", EweiTLogUtils.getCurrentTraceId());
        }
        //从redis移除
        removeRedis(alreadyProcessIds);
        return alreadyProcessIds;
    }

 

public void removeRedis(List<Integer> alreadyProcessIds) {
        if (CollectionUtils.isEmpty(alreadyProcessIds)) {
            return;
        }
        //已经处理的从队列移除
        for (Integer id :
                alreadyProcessIds) {
            redisOperationService.zremWithPrefix(WAIT_PROCESS_LOG_KEY, id.toString());
        }
    }

 

posted @ 2022-06-06 15:27  意犹未尽  阅读(106)  评论(0编辑  收藏  举报