• 设计思路通用的补偿


    说明

    放入的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());
            }
        }
  • 相关阅读:
    案例19-页面使用ajax显示类别菜单
    案例18-首页最新商品和热门商品显示
    案例17-validate自定义校验规则校验验证码是否输入正确
    案例16-validate自定义校验规则校验用户名是否存在
    案例15-基本的表单校验使用validate
    测开之路六十九:监控平台之视图层
    测开之路六十八:监控平台之监控逻辑和处理逻辑
    测开之路六十七:监控平台之附加功能准备
    测开之路六十六:UI测试平台之处理逻辑和蓝图添加到程序入口
    测开之路六十五:UI测试平台之js
  • 原文地址:https://www.cnblogs.com/LQBlog/p/16348358.html
Copyright © 2020-2023  润新知