说明
放入的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()); } }