运维补偿接口的设计与实现:模式、形态与通用示例

运维补偿接口的设计与实现:模式、形态与通用示例

本文聚焦"运维补偿接口"(用于修复线上异常数据的后台重试/补救入口)这一工程实践,不依赖任何具体业务,所有示例均为通用代码。


一、什么是运维补偿接口

运维补偿接口(也称运维工具接口 / 补偿重试接口 / 修复接口 )是一类只面向内部运维人员、不面向普通用户的后台入口,用于在以下场景修复数据:

  • 外部系统调用超时/失败,导致本地"待通知/待推送"记录没发出去。
  • MQ 消费失败,业务单据处于中间状态。
  • 第三方回传丢失,需要人工触发重新处理。
  • 历史脏数据需要按条件批量订正。

它的本质特征:读历史记录 → 重新执行某段业务逻辑 → 幂等、可重跑、有日志


二、常见实现形态

形态 1:按 ID 列表重推(最常用)

入参是业务主键列表,逐条查出原记录/原参数,重新调用核心逻辑。

复制代码
POST /ops/retry-push
body: { "ids": [101, 102, 103] }

适用:单条失败、可逐条定位的重推。

形态 2:按条件批量扫描重跑

入参是状态/时间范围等筛选条件,扫描失败记录批量重试。

复制代码
POST /ops/retry-by-status
body: { "status": "FAILED", "startTime": "...", "endTime": "..." }

适用:某段时间大面积失败。

形态 3:按原参数重放(Replay)

从日志/历史表取出上次请求的完整入参,原样重放给下游。

适用:下游需要完全一致的参数,不能重新反解析。

形态 4:强制订正(Direct Fix)

不调核心逻辑,直接写 SQL/Repository 把状态、字段改对。

适用:简单字段错误,但风险高,需审计

形态 5:dry-run 预览

补偿接口先返回"将要处理哪些、影响多少行",不真正执行,确认后再带 confirm=true 执行。

适用:批量高危操作。


三、核心设计原则

原则 说明
幂等 同一记录重跑 N 次结果一致,不重复发、不重复建单
可观测 每条处理结果记录日志 / 写补偿日志表(成功/失败/跳过原因)
不破坏主流程 补偿逻辑复用核心方法,但隔离事务,失败不影响其他条
权限隔离 仅运维角色可访问,生产需审批/白名单
有总额统计 返回处理总数、成功数、失败数、跳过数
可重入 中断后可再次调用,已成功的跳过

四、通用完整示例(形态 1:按 ID 重推)

1. 入参 DTO

java 复制代码
public class RetryPushRequest {
    /** 业务主键列表 */
    private List<Long> ids;

    public List<Long> getIds() { return ids; }
    public void setIds(List<Long> ids) { this.ids = ids; }
}

2. 返回结果(带逐条明细)

java 复制代码
public class RetryResult {
    private int total;
    private int success;
    private int failed;
    private int skipped;
    private List<String> errors = new ArrayList<>();

    public void incSuccess() { success++; }
    public void incFailed(String msg) { failed++; errors.add(msg); }
    public void incSkipped() { skipped++; }
    // getter / setter
}

3. 日志实体(补偿记录表)

java 复制代码
@Entity
@Table(name = "ops_retry_log")
public class OpsRetryLog {
    @Id @GeneratedValue
    private Long id;
    private String bizType;        // 业务类型
    private Long bizId;            // 业务主键
    private String status;         // SUCCESS / FAILED / SKIPPED
    private String errorMsg;
    private Date createTime;
    // getter / setter
}

4. 核心补偿 Service

java 复制代码
@Service
@Slf4j
public class PushRetryService {

    @Autowired
    private BizRecordRepository bizRepo;
    @Autowired
    private NotifyLogRepository notifyRepo;
    @Autowired
    private OpsRetryLogRepository opsLogRepo;
    @Autowired
    private NotifyService notifyService;   // 真正发通知的核心方法

    /**
     * 按 ID 重推,每条独立处理、独立事务,互不影响。
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public RetryResult retryByIds(List<Long> ids) {
        RetryResult result = new RetryResult();
        result.total = ids.size();

        for (Long id : ids) {
            try {
                Optional<BizRecord> opt = bizRepo.findById(id);
                if (!opt.isPresent()) {
                    log.warn("补偿跳过-记录不存在, id={}", id);
                    result.incSkipped();
                    saveOpsLog("PUSH", id, "SKIPPED", "记录不存在");
                    continue;
                }
                BizRecord record = opt.get();

                // 幂等:查上次通知日志,已成功则跳过
                List<NotifyLog> logs = notifyRepo.findByBizId(id);
                if (logs.stream().anyMatch(l -> "SUCCESS".equals(l.getStatus()))) {
                    log.info("补偿跳过-已成功, id={}", id);
                    result.incSkipped();
                    saveOpsLog("PUSH", id, "SKIPPED", "已成功");
                    continue;
                }

                // 取最近一次失败日志的参数重放;无日志则从主记录反解析
                String param = logs.isEmpty()
                        ? resolveParamFromRecord(record)
                        : logs.get(logs.size() - 1).getBizParam();

                notifyService.push(record, param);   // 复用核心逻辑
                result.incSuccess();
                saveOpsLog("PUSH", id, "SUCCESS", null);
            } catch (Exception e) {
                log.error("补偿失败, id={}, err={}", id, e.getMessage(), e);
                result.incFailed("id=" + id + ":" + e.getMessage());
                saveOpsLog("PUSH", id, "FAILED", e.getMessage());
            }
        }
        return result;
    }

    private String resolveParamFromRecord(BizRecord record) {
        // 从主记录反解析出下游所需参数(通用示例)
        return "{\"bizId\":" + record.getId() + "}";
    }

    private void saveOpsLog(String type, Long bizId, String status, String err) {
        OpsRetryLog log = new OpsRetryLog();
        log.setBizType(type);
        log.setBizId(bizId);
        log.setStatus(status);
        log.setErrorMsg(err);
        log.setCreateTime(new Date());
        opsLogRepo.saveAndFlush(log);
    }
}

5. Controller(运维入口)

java 复制代码
@RestController
@RequestMapping("/ops")
public class OpsController {

    @Autowired
    private PushRetryService retryService;

    @PostMapping("/retry-push")
    public RetryResult retryPush(@RequestBody RetryPushRequest req) {
        if (req.getIds() == null || req.getIds().isEmpty()) {
            throw new IllegalArgumentException("ids 不能为空");
        }
        return retryService.retryByIds(req.getIds());
    }
}

五、形态 2 示例:按状态批量扫描

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public RetryResult retryByStatus(String status, Date startTime, Date endTime) {
    List<NotifyLog> failed = notifyRepo
            .findByStatusAndCreateTimeBetween(status, startTime, endTime);
    List<Long> ids = failed.stream()
            .map(NotifyLog::getBizId)
            .distinct()
            .collect(Collectors.toList());
    return retryByIds(ids);   // 复用形态 1 的逻辑
}

六、形态 5 示例:dry-run 预览

java 复制代码
public RetryResult previewRetry(List<Long> ids) {
    RetryResult result = new RetryResult();
    result.total = ids.size();
    for (Long id : ids) {
        boolean exists = bizRepo.findById(id).isPresent();
        if (!exists) result.incSkipped();
        else result.incSuccess();   // 仅预览,不真正执行
    }
    return result;   // 调方确认后再带 confirm=true 调真正的 retryByIds
}

七、权限与安全防护(必做)

java 复制代码
@PostMapping("/retry-push")
@PreAuthorize("hasRole('OPS')")   // 仅运维角色
public RetryResult retryPush(@RequestBody RetryPushRequest req) {
    // ...
}

补充建议:

  • 生产环境加IP 白名单 / 审批流
  • 接口路径带 /ops//internal/ 前缀,网关层禁止对外暴露。
  • 大批量限制单次 ID 数量(如 ≤ 1000),防止雪崩。
  • 所有补偿操作留痕到 ops_retry_log,可追溯。

八、与事务边界的关联

  • 补偿接口本身通常逐条 REQUIRES_NEW,保证一条失败不影响其他条。
  • 补偿里写"补偿日志表"也处于新事务,需注意:若在事务提交后(afterCommit)写补偿日志,同样要手动填审计字段(见审计篇),或直接传参。
  • 补偿调的核心方法若涉及外部 HTTP,要吞掉异常、记日志、落失败状态,绝不能让异常冒泡导致整批中断。

九、踩坑清单

现象 正确做法
补偿逻辑直接复用主流程但没隔离事务 一条失败整批回滚 逐条 REQUIRES_NEW
不幂等 重跑重复发通知/建单 先查已成功则跳过
无日志 跑完不知道谁成功谁失败 写补偿日志表 + 返回明细
暴露给公网 被滥用 @PreAuthorize + 网关隔离
不限制批量大小 大批量拖垮 DB 限流 / 分页
异常冒泡 接口 500,调用方不知部分成功 逐条 try-catch,汇总返回
没 dry-run 误操作无法预览 高危操作先 preview

十、一句话总结

运维补偿接口 = "查历史 → 复用核心逻辑重跑 → 逐条独立事务 + 幂等跳过 + 全量留痕" 的后台修复工具。它和"事务提交后写库"共享同一套事务边界认知(REQUIRES_NEW、审计字段手动填、异常隔离),是生产系统不可缺少的"后悔药",但必须用权限、留痕、幂等三道闸门锁住风险。

相关推荐
智恒百亿22 分钟前
5090八卡服务器整机技术解析与部署常见问题(FAQ)
运维·服务器
萌动的小火苗26 分钟前
Linux进程线程面试题【无答案】
linux·运维·服务器·c语言·开发语言
cuijiecheng201832 分钟前
Linux下ss命令用法
linux·运维·服务器
云上工程笔记35 分钟前
4090 GPU 服务器适合哪些 AI 场景?2026 年显存、推理性能、租用价格和风险对比
运维·服务器·人工智能
mennekes1 小时前
光伏场站检修电源:户外工业连接器防腐与防雷选型方案
运维·科技·安全·制造
名字还没想好☜1 小时前
kubectl 排障实战:jsonpath 精准取值、custom-columns、events 排序与 top 速查
运维·前端·chrome·docker·kubernetes
Doraemomo1 小时前
Linux编程-epoll多路IO复用
linux·运维·服务器
wtblszn1 小时前
城市二供泵站远程监控运维系统物联网方案
运维·物联网
白猫不黑1 小时前
运维如何转安全(个人经验篇)
运维·学习·安全·web安全·网络安全·信息安全