交易、订单轮询策略(能用数据库轮询解决的不用Redis,能用Redis解决的不用消息队列)

1、传统定时轮询数据库、然后查询渠道

优点

  1. 实现简单:代码直观,易于理解

  2. 无外部依赖:只依赖数据库

  3. 数据一致性强:直接操作数据库,状态一致

  4. 排查问题容易:SQL可查询所有待处理数据

缺点

  1. 时间精度低:最快30秒延迟(轮询间隔)

  2. 数据库压力大:频繁全表/索引扫描

  3. 实时性差:可能延迟30秒才发现超时

  4. 资源浪费:无任务时也在空转

  5. 难以扩展:多实例可能导致重复处理

适用场景

  • 对实时性要求不高(分钟级)

  • 数据量小(< 10万)

  • 查询条件简单

  • 低成本项目

2、消息队列

优点

  1. 实时性好:精确到毫秒级触发

  2. 解耦性强:订单服务和超时处理服务分离

  3. 可削峰填谷:突发流量可缓冲处理

  4. 支持重试:消费失败可重新投递

  5. 分布式友好:天然支持多消费者

缺点

  1. 复杂度高:引入消息中间件,运维复杂

  2. 消息可能丢失:需要额外保证机制

  3. 顺序问题:需要特殊处理消息顺序

  4. 延迟精度有限:RabbitMQ延迟消息有精度限制

  5. 数据一致性:需要处理"本地事务+消息"一致性问题

适用场景

  • 分布式系统,服务解耦

  • 需要精确延迟触发

  • 流量波动大的场景

  • 需要异步处理的业务

3、redis zset

优点

  1. 性能极高:Redis内存操作,O(log N)

  2. 实时性好:可达到秒级甚至毫秒级精度

  3. 支持大量数据:内存存储,可处理百万级任务

  4. 灵活性高:支持动态调整延迟时间

  5. 分布式协调:可作为分布式调度中心

缺点

  1. 数据易失:Redis故障可能丢失数据(需持久化)

  2. 内存占用:大数据量时占用较多内存

  3. 可靠性依赖:依赖Redis的可用性

  4. 需要轮询:仍需要定时扫描(间隔可很短)

  5. 复杂状态管理:状态需在数据库中维护

适用场景

  • 高性能要求的延迟任务

  • 短周期、高频次的定时任务

  • 需要动态调整时间的场景

  • 已使用Redis的项目

三方案对比表格

维度 定时轮询数据库 消息队列 Redis ZSet
实时性 差(分钟级) 好(毫秒级) 好(秒级)
性能 差(数据库压力大) 中(网络开销) 优(内存操作)
可靠性 高(数据不丢) 中(可能丢消息) 中(可能丢数据)
复杂度 低 高 中
扩展性 差 优 良
成本 低 高 中
数据量 小(<10万) 大 大
精确度 低(依赖轮询间隔) 高 高
适用场景 简单低频任务 分布式解耦 高性能调度

现在重点介绍 Redis Zset

java 复制代码
package cn.finopen.boot.autoconfigure.resource.txn.service;

import cn.finopen.boot.autoconfigure.redis.RedisService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

/**
 * MPOS 订单查询队列管理服务(统一服务)
 * <p>
 * 功能:
 * 1. 管理待查询订单的 Redis ZSet 队列
 * 2. 提供订单入队、出队、超时管理等方法
 * 3. 支持不同场景的指数退避查询策略
 * <p>
 * 支持场景:
 * - MICRO: 反扫订单(高频查询,秒级响应)
 * - GENERAL: 通用订单(低频查询,分钟级响应)
 * <p>
 * Redis 数据结构:
 * - Key: mpos:{scene}:query:pending (ZSet)
 * - Member: serverOrderId
 * - Score: nextQueryTimestamp (下次查询时间戳)
 * - Key: mpos:{scene}:query:createtime:{orderId} (String)
 * - Value: createTimestamp (订单创建时间戳)
 * - TTL: 24小时
 *
 * @author xt
 */
@Slf4j
@Service
@AllArgsConstructor
public class MposQueryService {
    private final RedisService redisService;

    /**
     * 查询场景枚举
     */
    public enum QueryScene {
        /**
         * 反扫订单(高频查询)
         */
        MICRO("micro", 5000L, 15 * 60 * 1000L),
        /**
         * 通用订单(低频查询)
         */
        GENERAL("general", 60000L, 12 * 60 * 60 * 1000L);
        private final String id;
        private final long firstQueryDelay; // 首次查询延迟
        private final long timeoutMillis;   // 超时时间

        QueryScene(String id, long firstQueryDelay, long timeoutMillis) {
            this.id = id;
            this.firstQueryDelay = firstQueryDelay;
            this.timeoutMillis = timeoutMillis;
        }

        public String getId() {
            return id;
        }

        public long getFirstQueryDelay() {
            return firstQueryDelay;
        }

        public long getTimeoutMillis() {
            return timeoutMillis;
        }
    }

    /**
     * 获取 Redis ZSet Key
     */
    private String getQueryPendingKey(QueryScene scene) {
        return "mpos:" + scene.getId() + ":query:pending";
    }

    /**
     * 获取 Redis 创建时间 Key 前缀
     */
    private String getCreateTimeKeyPrefix(QueryScene scene) {
        return "mpos:" + scene.getId() + ":query:createtime:";
    }


    /**
     * 添加订单到查询队列(指定场景)
     *
     * @param serverOrderId 订单ID
     * @param scene         查询场景
     */
    public void addToQueryQueue(String serverOrderId, QueryScene scene) {
        try {
            long nextQueryTime = System.currentTimeMillis() + scene.getFirstQueryDelay();
            String queueKey = getQueryPendingKey(scene);
            redisService.zAdd(queueKey, serverOrderId, nextQueryTime);
            // 记录订单创建时间,用于超时判断
            String createTimeKey = getCreateTimeKeyPrefix(scene) + serverOrderId;
            redisService.set(createTimeKey, String.valueOf(System.currentTimeMillis()), 24 * 3600);
            log.info("订单加入查询队列: scene={}, serverOrderId={}, nextQueryTime={}", scene.getId(), serverOrderId, nextQueryTime);
        } catch (Exception e) {
            log.error("订单加入查询队列失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
        }
    }


    /**
     * 获取所有到期应该查询的订单(指定场景)
     *
     * @param scene 查询场景
     * @return 订单ID集合
     */
    public Set<String> getExpiredOrders(QueryScene scene) {
        try {
            long currentTime = System.currentTimeMillis();
            String queueKey = getQueryPendingKey(scene);
            Set<String> orders = redisService.zRangeByScore(queueKey, 0, currentTime);
            if (orders != null && !orders.isEmpty()) {
                log.debug("获取到期订单: scene={}, count={}", scene.getId(), orders.size());
            }
            return orders != null ? orders : new HashSet<>();
        } catch (Exception e) {
            log.error("获取到期订单失败: scene={}", scene.getId(), e);
            return new HashSet<>();
        }
    }


    /**
     * 更新订单的下次查询时间(指定场景)
     *
     * @param serverOrderId      订单ID
     * @param nextQueryTimestamp 下次查询时间戳
     * @param scene              查询场景
     */
    public void updateNextQueryTime(String serverOrderId, long nextQueryTimestamp, QueryScene scene) {
        try {
            String queueKey = getQueryPendingKey(scene);
            redisService.zAdd(queueKey, serverOrderId, nextQueryTimestamp);
            log.debug("更新订单下次查询时间: scene={}, serverOrderId={}, nextQueryTime={}",
                    scene.getId(), serverOrderId, nextQueryTimestamp);
        } catch (Exception e) {
            log.error("更新订单查询时间失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
        }
    }


    /**
     * 从查询队列移除订单(指定场景)
     *
     * @param serverOrderId 订单ID
     * @param scene         查询场景
     */
    public void removeFromQueue(String serverOrderId, QueryScene scene) {
        try {
            String queueKey = getQueryPendingKey(scene);
            String createTimeKey = getCreateTimeKeyPrefix(scene) + serverOrderId;
            redisService.zRemove(queueKey, serverOrderId);
            redisService.deleteByKey(createTimeKey);
            log.info("订单从查询队列移除: scene={}, serverOrderId={}", scene.getId(), serverOrderId);
        } catch (Exception e) {
            log.error("订单移除失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
        }
    }


    /**
     * 获取订单创建时间(指定场景)
     *
     * @param serverOrderId 订单ID
     * @param scene         查询场景
     * @return 创建时间戳(毫秒), 不存在返回当前时间
     */
    public long getOrderCreateTime(String serverOrderId, QueryScene scene) {
        try {
            String createTimeKey = getCreateTimeKeyPrefix(scene) + serverOrderId;
            String createTimeStr = redisService.get(createTimeKey);
            if (createTimeStr != null && !createTimeStr.isEmpty()) {
                return Long.parseLong(createTimeStr);
            }
        } catch (Exception e) {
            log.error("获取订单创建时间失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
        }
        return System.currentTimeMillis();
    }


    /**
     * 计算下次查询时间(指数退避策略)
     * <p>
     * MICRO 策略(快速):
     * - 前30秒: 每5秒查询一次
     * - 30秒-2分钟: 每10秒查询一次
     * - 2分钟-5分钟: 每30秒查询一次
     * - 5分钟-15分钟: 每60秒查询一次
     * - 超过15分钟: 返回-1表示超时
     * <p>
     * GENERAL 策略(宽松):
     * - 前5分钟: 每1分钟查询一次
     * - 5-30分钟: 每5分钟查询一次
     * - 30分钟-2小时: 每15分钟查询一次
     * - 2-12小时: 每1小时查询一次
     * - 超过12小时: 返回-1表示超时
     *
     * @param createTime 订单创建时间(毫秒)
     * @param scene      查询场景
     * @return 下次查询时间戳, 返回-1表示已超时
     */
    public long calculateNextQueryTime(long createTime, QueryScene scene) {
        long currentTime = System.currentTimeMillis();
        long elapsed = currentTime - createTime;

        if (scene == QueryScene.MICRO) {
            // 反扫订单:快速退避策略(秒级)
            long elapsedSeconds = elapsed / 1000;
            long intervalSeconds;

            if (elapsedSeconds < 30) {
                intervalSeconds = 5;
            } else if (elapsedSeconds < 120) {
                intervalSeconds = 10;
            } else if (elapsedSeconds < 300) {
                intervalSeconds = 30;
            } else if (elapsedSeconds < 900) {
                intervalSeconds = 60;
            } else {
                log.warn("订单查询超时: scene={}, elapsed={}秒", scene.getId(), elapsedSeconds);
                return -1;
            }
            return currentTime + (intervalSeconds * 1000);

        } else {
            // 通用订单:宽松退避策略(分钟级)
            long elapsedMinutes = elapsed / 60000;
            long intervalMinutes;

            if (elapsedMinutes < 5) {
                intervalMinutes = 1;
            } else if (elapsedMinutes < 30) {
                intervalMinutes = 5;
            } else if (elapsedMinutes < 120) {
                intervalMinutes = 15;
            } else if (elapsedMinutes < 720) {
                intervalMinutes = 60;
            } else {
                log.warn("订单查询超时: scene={}, elapsed={}分钟", scene.getId(), elapsedMinutes);
                return -1;
            }
            return currentTime + (intervalMinutes * 60000);
        }
    }


    /**
     * 获取待查询订单总数(指定场景)
     *
     * @param scene 查询场景
     * @return 订单数量
     */
    public Long getPendingOrderCount(QueryScene scene) {
        try {
            String queueKey = getQueryPendingKey(scene);
            return redisService.zCard(queueKey);
        } catch (Exception e) {
            log.error("获取待查询订单数量失败: scene={}", scene.getId(), e);
            return 0L;
        }
    }


    /**
     * 获取未来指定时间内需要查询的订单数量(指定场景)
     *
     * @param futureSeconds 未来秒数
     * @param scene         查询场景
     * @return 订单数量
     */
    public Long getUpcomingOrderCount(int futureSeconds, QueryScene scene) {
        try {
            long currentTime = System.currentTimeMillis();
            long futureTime = currentTime + (futureSeconds * 1000);
            String queueKey = getQueryPendingKey(scene);
            return redisService.zCount(queueKey, currentTime, futureTime);
        } catch (Exception e) {
            log.error("获取未来订单数量失败: scene={}", scene.getId(), e);
            return 0L;
        }
    }

    /**
     * 获取超时未处理的订单(反扫订单)
     *
     * @param timeoutMinutes 超时分钟数
     * @return 超时订单ID集合
     */
    public Set<String> getTimeoutOrders(int timeoutMinutes) {
        return getTimeoutOrders(timeoutMinutes, QueryScene.MICRO);
    }

    /**
     * 获取超时未处理的订单(指定场景)
     *
     * @param timeoutMinutes 超时分钟数
     * @param scene          查询场景
     * @return 超时订单ID集合
     */
    public Set<String> getTimeoutOrders(int timeoutMinutes, QueryScene scene) {
        long currentTime = System.currentTimeMillis();
        long timeoutThreshold = timeoutMinutes * 60 * 1000;
        Set<String> timeoutOrders = new HashSet<>();

        try {
            String queueKey = getQueryPendingKey(scene);
            Set<String> allOrderIds = redisService.zRange(queueKey, 0, -1);
            if (allOrderIds == null || allOrderIds.isEmpty()) {
                return timeoutOrders;
            }

            // 检查每个订单的创建时间
            for (String serverOrderId : allOrderIds) {
                long createTime = getOrderCreateTime(serverOrderId, scene);
                if (currentTime - createTime > timeoutThreshold) {
                    timeoutOrders.add(serverOrderId);
                }
            }

            if (!timeoutOrders.isEmpty()) {
                log.warn("发现超时订单: scene={}, count={}, timeoutMinutes={}",
                        scene.getId(), timeoutOrders.size(), timeoutMinutes);
            }
            return timeoutOrders;

        } catch (Exception e) {
            log.error("获取超时订单失败: scene={}", scene.getId(), e);
            return timeoutOrders;
        }
    }

    /**
     * 清理所有查询队列数据(反扫订单,仅用于测试或维护)
     */
    public void clearAll() {
        clearAll(QueryScene.MICRO);
    }

    /**
     * 清理所有查询队列数据(指定场景,仅用于测试或维护)
     *
     * @param scene 查询场景
     */
    public void clearAll(QueryScene scene) {
        try {
            String queueKey = getQueryPendingKey(scene);
            String createTimePrefix = getCreateTimeKeyPrefix(scene);

            Set<String> allOrderIds = redisService.zRange(queueKey, 0, -1);
            if (allOrderIds != null) {
                for (String serverOrderId : allOrderIds) {
                    redisService.deleteByKey(createTimePrefix + serverOrderId);
                }
            }
            redisService.deleteByKey(queueKey);
            log.warn("清理所有查询队列数据: scene={}", scene.getId());
        } catch (Exception e) {
            log.error("清理数据失败: scene={}", scene.getId(), e);
        }
    }
}

定时任务:

java 复制代码
package cn.finopen.boot.autoconfigure.resource.txn.scheduling;

import cn.finopen.boot.autoconfigure.redis.scheduling.AbstractDistributedSchedule;
import cn.finopen.boot.autoconfigure.resource.txn.service.MposNotifyService;
import cn.finopen.boot.autoconfigure.resource.txn.service.MposQueryService;
import cn.finopen.faas.api.channel.ChannelService;
import cn.finopen.faas.api.channel.MposExecuter;
import cn.finopen.faas.api.channel.dto.*;
import cn.finopen.faas.api.merchant.dto.MerchantChannelProduct;
import cn.finopen.faas.api.txn.TxnMposService;
import cn.finopen.faas.api.txn.dto.TxnMposReq;
import cn.finopen.faas.api.txn.dto.TxnMposResp;
import cn.finopen.faas.api.txn.dto.TxnMposSimpleResp;
import cn.finopen.faas.api.txn.dto.TxnStatus;
import cn.finopen.faas.merchant.api.MchChannelProductService;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import fib.core.exception.FibException;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.time.ZoneId;
import java.util.List;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 聚合支付反扫订单状态查询任务 - Redis ZSet 优化版
 * <p>
 * 优化要点:
 * 1. 使用 Redis ZSet 管理待查询订单,以时间戳为 score 实现自动排序
 * 2. 实现指数退避策略,动态调整查询间隔
 * 3. 减少数据库压力 95%+,提升查询效率
 * 4. 整合队列管理逻辑,代码更简洁
 * <p>
 * Redis ZSet 设计:
 * - Key: mpos:micro:query:pending
 * - Member: serverOrderId (订单ID)
 * - Score: nextQueryTimestamp (下次查询时间戳,毫秒)
 * <p>
 * 辅助数据结构:
 * - Key: mpos:micro:query:createtime:{serverOrderId} -> 订单创建时间
 * - 过期时间: 24小时自动清理
 *
 * @author xt
 */
@Service
@AllArgsConstructor
@Lazy(false)
public class MposMicroQueryJobV2 extends AbstractDistributedSchedule {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final TxnMposService txnService;
    private final List<MposExecuter> executers;
    private final ChannelService channelService;
    private final MchChannelProductService mchChannelProductService;
    private final MposNotifyService notifyService;
    private final MposQueryService queryService;

    private final static ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            50, 50, 60000, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(),
            new ThreadFactoryBuilder().setNameFormat("MposMicroQueryJob pool-%d").build(),
            new ThreadPoolExecutor.AbortPolicy()
    );

    /**
     * 主任务: 从 Redis ZSet 获取到期订单进行查询
     * 每5秒执行一次
     */
    @Override
    @Scheduled(cron = "0/5 * * * * ?")
    public void execute() {
        try {
            logger.info("聚合支付反扫订单状态查询任务(Redis ZSet优化版)");
            // 获取分布式锁
            boolean lock = lock(0, 1000);
            if (!lock) {
                logger.debug("获取分布式锁失败,跳过本次执行");
                return;
            }
            // 从 Redis ZSet 获取所有到期订单 (score <= 当前时间)
            Set<String> expiredOrderIds = queryService.getExpiredOrders(MposQueryService.QueryScene.MICRO);
            if (CollectionUtils.isEmpty(expiredOrderIds)) {
                logger.debug("当前没有到期订单需要查询");
                return;
            }
            logger.info("获取到{}笔到期订单,开始查询", expiredOrderIds.size());
            // 提交到线程池异步查询
            for (String serverOrderId : expiredOrderIds) {
                THREAD_POOL_EXECUTOR.execute(() -> queryOrder(serverOrderId));
            }

        } catch (Exception e) {
            logger.error("聚合支付反扫订单状态查询任务执行失败", e);
        }
    }

    /**
     * 查询单个订单状态
     *
     * @param serverOrderId 订单ID
     */
    private void queryOrder(String serverOrderId) {
        try {
            // 1. 从数据库加载订单详情
            TxnMposResp history = txnService.getByServerOrderId(serverOrderId);
            if (history == null) {
                logger.warn("订单不存在,从队列移除: serverOrderId={}", serverOrderId);
                queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
                return;
            }
            // 检查订单状态,如果不是处理中则移除
            if (history.getTxnStatus() != TxnStatus.PROCESS.ordinal()) {
                logger.info("订单状态已变更,从队列移除: serverOrderId={}, status={}", serverOrderId, history.getTxnStatus());
                queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
                return;
            }
            // 2. 调用渠道查询
            MposResp resp = queryFromChannel(history);
            // 3. 根据查询结果处理
            handleQueryResult(history, resp);
        } catch (Exception e) {
            logger.error("订单查询失败: serverOrderId={}", serverOrderId, e);
            // 查询失败,延长查询间隔(30秒后重试)
            long retryTime = System.currentTimeMillis() + 30000;
            queryService.updateNextQueryTime(serverOrderId, retryTime, MposQueryService.QueryScene.MICRO);
        }
    }

    /**
     * 调用渠道接口查询订单
     *
     * @param history 订单信息
     * @return 查询结果
     */
    private MposResp queryFromChannel(TxnMposResp history) {
        String txnType = history.getTxnType();
        String sceneType = history.getSceneType();
        String channelCode = history.getChannelCode();
        String merchantCode = history.getMerchantCode();
        // 获取执行器
        MposExecuter executer = getServiceExecute(txnType, sceneType, channelCode);
        // 获取渠道配置
        ChannelGetReq channelReq = ChannelGetReq.newBuilder()
                .setChannelCode(channelCode)
                .setTxnType(txnType)
                .setTxnSceneType(sceneType)
                .build();
        ChannelGetResp channel = channelService.getSceneByChannel(channelReq);
        // 获取商户渠道配置
        MerchantChannelProduct mchChannel = mchChannelProductService.getByTxnType(
                txnType, sceneType, merchantCode, channelCode);
        MchChannelGetResp mchChannelDetails = MchChannelGetResp.newBuilder()
                .setChannelCode(channelCode)
                .setMerchantCode(merchantCode)
                .setTxnType(txnType)
                .setTxnSceneType(sceneType)
                .setThirdMerchantCode(history.getThirdMerchantCode())
                .setThirdMerchantSecretKey(mchChannel.getThirdMerchantSecretKey())
                .setThirdMerchantAppId(mchChannel.getThirdMerchantAppId())
                .setThirdMerchantAccount(mchChannel.getThirdMerchantAccount())
                .setChannelDetails(channel)
                .build();

        // 构建查询请求
        String serverOrderId = history.getServerOrderId();
        MposReq mposReq = MposReq.newBuilder()
                .setServerOrderId(serverOrderId)
                .setThirdOrderId(history.getThirdOrderId())
                .setPreAuth(history.getPreAuth())
                .setCreateDate(history.getCreateDate())
                .build();

        // 调用渠道查询
        return executer.query(mchChannelDetails, mposReq);
    }

    /**
     * 处理查询结果
     *
     * @param history 订单信息
     * @param resp    查询结果
     */
    private void handleQueryResult(TxnMposResp history, MposResp resp) {
        String serverOrderId = history.getServerOrderId();
        Integer tradeStatus = resp.getTradeStatus();

        // 情况1: 仍在处理中,更新下次查询时间
        if (tradeStatus == TradeStatus.IN_PROCESS.ordinal()) {
            long createTime = queryService.getOrderCreateTime(serverOrderId, MposQueryService.QueryScene.MICRO);
            if (history.getCreateTime() != null) {
                createTime = history.getCreateTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            }
            // 计算下次查询时间(指数退避)
            long nextQueryTime = queryService.calculateNextQueryTime(createTime, MposQueryService.QueryScene.MICRO);
            if (nextQueryTime == -1) {
                // 已超时,停止查询,不更新订单状态
                logger.warn("订单查询超时,停止查询: serverOrderId={}", serverOrderId);
                queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
            } else {
                // 更新下次查询时间
                queryService.updateNextQueryTime(serverOrderId, nextQueryTime, MposQueryService.QueryScene.MICRO);
                logger.debug("订单仍在处理中,下次查询时间: serverOrderId={}, nextTime={}", serverOrderId, nextQueryTime);
            }
            return;
        }
        // 情况2: 交易成功或失败,更新订单状态并移除队列
        TxnStatus txnStatus = tradeStatus == TradeStatus.SUCCESS.ordinal()
                ? TxnStatus.SUCCESS
                : TxnStatus.FAIL;
        String respCode = resp.getErrCode();
        String respMessage = resp.getErrMessage();
        logger.info("订单查询完成: serverOrderId={}, status={}, code={}, message={}",
                serverOrderId, txnStatus, respCode, respMessage);
        // 更新数据库订单状态
        updateOrderStatus(history, txnStatus, respCode, respMessage, resp);
        // 从 Redis 队列移除
        queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
        // 如果交易成功,发送异步通知
        if (txnStatus == TxnStatus.SUCCESS) {
            sendNotification(history, resp);
        }
    }

    /**
     * 更新订单状态到数据库
     *
     * @param history     原订单信息
     * @param txnStatus   交易状态
     * @param respCode    响应码
     * @param respMessage 响应消息
     * @param resp        查询响应
     */
    private void updateOrderStatus(TxnMposResp history, TxnStatus txnStatus,
                                   String respCode, String respMessage, MposResp resp) {
        Long groupId = history.getGroupId();
        String serverOrderId = history.getServerOrderId();
        txnService.update(TxnMposReq.newBuilder()
                .setGroupId(groupId)
                .setTxnStatus(txnStatus.ordinal())
                .setServerOrderId(serverOrderId)
                .setThirdOrderId(resp.getThirdOrderId())
                .setChannelOrderId(resp.getChannelOrderId())
                .setPreAuthOrderId(resp.getPreAuthOrderId())
                .setErrCode(respCode)
                .setErrMessage(respMessage)
                .setCardType(resp.getCardType())
                .setSceneType(resp.getSceneType())
                .setOpenId(resp.getOpenId())
                .setWxOpenId(resp.getWxOpenId())
                .setRate(resp.getRate())
                .setServiceFee(resp.getServiceFee())
                .setBillStatus(resp.getBillStatus())
                .setPayTime(resp.getPayTime())
                .build());
    }

    /**
     * 发送异步通知
     *
     * @param history 订单信息
     * @param resp    查询响应
     */
    private void sendNotification(TxnMposResp history, MposResp resp) {
        try {
            TxnMposSimpleResp result = TxnMposSimpleResp.newBuilder()
                    .setServerOrderId(history.getServerOrderId())
                    .setRefundOrderId(history.getRefundOrderId())
                    .setOrigServerOrderId(history.getOrigServerOrderId())
                    .setClientOrderId(history.getClientOrderId())
                    .setChannelOrderId(resp.getChannelOrderId())
                    .setPayAmount(history.getPayAmount())
                    .setTransAmount(resp.getTransAmount())
                    .setRefundAmount(history.getRefundAmount())
                    .setServiceFee(resp.getServiceFee())
                    .setTxnStatus(TxnStatus.SUCCESS.ordinal())
                    .setTxnType(resp.getTxnType())
                    .setSceneType(resp.getSceneType())
                    .setMerchantCode(history.getMerchantCode())
                    .setErrCode(resp.getErrCode())
                    .setErrMessage(resp.getErrMessage())
                    .setAttach(history.getAttach())
                    .setOpenId(history.getOpenId())
                    .setDelay(history.getDelay())
                    .setLsy(history.getIsLsy())
                    .setPayTime(resp.getPayTime())
                    .build();

            notifyService.execute(result, history.getClientNotifyUrl());

        } catch (Exception e) {
            logger.error("发送异步通知失败: serverOrderId={}", history.getServerOrderId(), e);
        }
    }

    /**
     * 获取服务执行器
     *
     * @param txnType     交易类型
     * @param sceneType   场景类型
     * @param channelCode 渠道代码
     * @return 执行器
     */
    public MposExecuter getServiceExecute(String txnType, String sceneType, String channelCode) {
        if (CollectionUtils.isEmpty(executers)) {
            throw FibException.ofNotFound(String.format("不支持的渠道[txnType=%s,sceneType=%s,channelCode=%s]", txnType, sceneType, channelCode));
        }
        for (MposExecuter api : executers) {
            if (api.support(txnType, sceneType, channelCode)) {
                return api;
            }
        }
        throw FibException.ofNotFound(String.format("不支持的渠道[txnType=%s,sceneType=%s,channelCode=%s]", txnType, sceneType, channelCode));
    }

    /**
     * 清理超时订单任务
     * 每10分钟执行一次,将超过15分钟仍在队列中的订单从队列移除(不更新订单状态)
     */
    @Scheduled(cron = "0 */10 * * * ?")
    public void cleanTimeoutOrders() {
        try {
            logger.info("开始清理超时订单");
            boolean lock = lockByClazz(0, 60);
            if (!lock) {
                logger.debug("获取清理任务锁失败");
                return;
            }
            // 获取超时订单(15分钟)
            Set<String> timeoutOrders = queryService.getTimeoutOrders(15);
            if (CollectionUtils.isEmpty(timeoutOrders)) {
                logger.info("没有超时订单需要清理");
                return;
            }
            logger.warn("发现{}笔超时订单,从队列移除", timeoutOrders.size());
            for (String serverOrderId : timeoutOrders) {
                try {
                    // 直接从队列移除,不更新订单状态
                    queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
                    logger.info("超时订单已从队列移除: serverOrderId={}", serverOrderId);
                } catch (Exception e) {
                    logger.error("清理超时订单失败: serverOrderId={}", serverOrderId, e);
                }
            }
            logger.info("超时订单清理完成: count={}", timeoutOrders.size());
        } catch (Exception e) {
            logger.error("清理超时订单任务失败", e);
        }
    }

    /**
     * 监控任务: 输出队列统计信息
     * 每分钟执行一次
     */
    @Scheduled(cron = "0 * * * * ?")
    public void monitorQueueStatus() {
        try {
            Long pendingCount = queryService.getPendingOrderCount(MposQueryService.QueryScene.MICRO);
            Long upcomingCount = queryService.getUpcomingOrderCount(60, MposQueryService.QueryScene.MICRO);
            logger.info("查询队列状态: 总待查询订单={}, 未来1分钟待查询={}", pendingCount, upcomingCount);
            // 可以在这里添加告警逻辑
            if (pendingCount != null && pendingCount > 10000) {
                logger.warn("查询队列堆积严重: count={}", pendingCount);
            }
        } catch (Exception e) {
            logger.error("监控任务失败", e);
        }
    }
}
相关推荐
SelectDB20 分钟前
Apache Doris 与 SelectDB 是什么关系?从开源技术到面向 Agent 的极速分析数据库
大数据·数据库·数据分析
小南家的青蛙21 分钟前
华为云DCS全形态原生Redis命令兼容性测试方案
redis·华为云·兼容性·兼容性测试·dcs
xiaoqiMikko36 分钟前
Redis 实战(三):持久化出错 —— fork 失败、磁盘写满、AOF 膨胀与损坏,逐个复现(6.2.4)
redis·后端
VimorphTech37 分钟前
文本已经改了,旧的音频生成结果还能回写吗?
java·数据库·mybatis
SelectDB38 分钟前
存算分离冷查询提速 7.5 倍!Apache Doris 4.2 Peer Read 实现跨计算组缓存复用
大数据·数据库·数据分析
盛世宏博智慧档案44 分钟前
一站式智慧档案解决方案,如何实现防火防盗漏水多维度安全监测实践
服务器·网络·数据库
IT规划师1 小时前
面试中被嘲笑Token放在redis里,该如何应对?
redis·面试
数据库小学妹1 小时前
MySQL磁盘IO瓶颈:iostat与刷脏fsync
数据库·mysql·性能优化
三兔测序学社1 小时前
【零代码生信分析】RNA-seq中级分析11-差异基因进行222种不同类型数据库基因集之间的富集分析
数据库