You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot是否有Redis阻塞回调替代方案?如何实现签名等待超时?

问题解答

1. SpringBoot本地实现 vs Redis哪个更优?

SpringBoot本地等价实现方案

可以用异步任务+重试+超时控制完成需求:

  • 用CompletableFuture配合超时参数,或者WebClient/RestTemplate设置请求超时,当签名API返回404时,启动异步线程定期重试调用签名API,直到超时时间到了就发送无签名回执。
  • 也可以用Spring Retry框架封装重试逻辑,配合@Async实现异步等待。

但本地实现有明显局限:

  • 单实例部署下,应用重启会导致等待中的任务直接丢失;
  • 集群部署时,多个实例可能重复处理同一交易,需要额外做分布式锁控制;
  • 本地异步任务的可靠性依赖应用进程,故障恢复能力弱。

Redis的优势

如果是集群部署、需要任务可靠性(重启不丢失)、高并发的场景,Redis是更优选择:

  • 用Redis的延迟队列(ZSet)或键过期通知机制,能实现可靠的超时等待,即使应用重启,任务依然保存在Redis中;
  • 天然支持分布式,多个应用实例可以监听同一个Redis事件,避免重复处理;
  • Redis性能高,适合处理这类短延迟任务,资源消耗低。

2. 针对此场景的Redis实现步骤

前置配置

首先需要开启Redis的键空间通知,让Redis在键过期时发送事件通知:

  • 如果是Docker容器启动Redis,加启动参数:docker run redis --notify-keyspace-events Ex
  • 如果用配置文件,在redis.conf中添加:notify-keyspace-events "Ex"

SpringBoot代码实现

1. Redis监听配置

配置Redis消息监听器,监听键过期事件:

@Configuration
public class RedisConfig {

    @Bean
    RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,
                                                               ReceiptKeyExpirationListener expirationListener) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 监听db0的键过期事件,若使用其他数据库,修改@后的数字
        container.addMessageListener(expirationListener, new PatternTopic("__keyevent@0__:expired"));
        return container;
    }
}

2. 过期事件处理器

编写监听器处理过期的交易等待键,发送无签名回执:

@Component
public class ReceiptKeyExpirationListener extends KeyExpirationEventMessageListener {

    private final RedisTemplate<String, String> redisTemplate;
    private final MerchantReceiptService receiptService;

    public ReceiptKeyExpirationListener(RedisMessageListenerContainer listenerContainer,
                                        RedisTemplate<String, String> redisTemplate,
                                        MerchantReceiptService receiptService) {
        super(listenerContainer);
        this.redisTemplate = redisTemplate;
        this.receiptService = receiptService;
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        // 只处理交易等待签名的键(前缀自定义)
        if (expiredKey.startsWith("receipt_wait_sign:")) {
            String transactionId = expiredKey.replace("receipt_wait_sign:", "");
            // 从Redis取出交易信息(建议用JSON序列化存储对象,这里简化为字符串)
            String receiptInfo = redisTemplate.opsForValue().get(expiredKey);
            if (receiptInfo != null) {
                receiptService.sendReceiptWithoutSignature(transactionId, receiptInfo);
            }
        }
    }
}

3. 核心业务逻辑

处理交易回执流程,调用签名API,失败则存入Redis并启动重试:

@Service
public class TransactionReceiptService {

    private final RestTemplate restTemplate;
    private final RedisTemplate<String, String> redisTemplate;
    private final MerchantReceiptService receiptService;

    // 等待签名的超时时间(单位:秒,可配置在application.yml)
    @Value("${signature.wait.timeout:300}")
    private int waitTimeout;
    // 重试签名API的间隔(单位:秒)
    @Value("${signature.retry.interval:30}")
    private int retryInterval;

    public TransactionReceiptService(RestTemplate restTemplate,
                                     RedisTemplate<String, String> redisTemplate,
                                     MerchantReceiptService receiptService) {
        this.restTemplate = restTemplate;
        this.redisTemplate = redisTemplate;
        this.receiptService = receiptService;
    }

    public void processReceipt(String transactionId, String merchantId, String receiptContent) {
        String signature = null;
        try {
            // 调用签名API
            signature = restTemplate.getForObject("http://signature-api/sign?transId={id}", String.class, transactionId);
        } catch (HttpClientErrorException.NotFound e) {
            // 签名API返回404,进入等待重试流程
            handleSignatureUnavailable(transactionId, merchantId, receiptContent);
            return;
        } catch (Exception e) {
            // 其他异常(如网络超时),也进入等待流程(可根据业务调整)
            handleSignatureUnavailable(transactionId, merchantId, receiptContent);
            return;
        }

        // 成功获取签名,发送带签名的回执
        receiptService.sendReceiptWithSignature(transactionId, merchantId, receiptContent, signature);
    }

    private void handleSignatureUnavailable(String transactionId, String merchantId, String receiptContent) {
        String redisKey = "receipt_wait_sign:" + transactionId;
        // 存储交易信息(建议用Jackson将对象序列化为JSON字符串,这里简化拼接)
        String receiptInfo = String.format("merchantId:%s,content:%s", merchantId, receiptContent);
        // 设置Redis键的过期时间为等待时长
        redisTemplate.opsForValue().set(redisKey, receiptInfo, waitTimeout, TimeUnit.SECONDS);

        // 启动异步任务定期重试获取签名
        retryGetSignature(transactionId, redisKey);
    }

    @Async
    public void retryGetSignature(String transactionId, String redisKey) {
        long endTime = System.currentTimeMillis() + waitTimeout * 1000;

        while (System.currentTimeMillis() < endTime) {
            try {
                String signature = restTemplate.getForObject("http://signature-api/sign?transId={id}", String.class, transactionId);
                if (signature != null) {
                    // 成功获取签名,删除Redis键(避免监听器触发过期事件)
                    redisTemplate.delete(redisKey);
                    // 取出交易信息并发送带签名的回执
                    String receiptInfo = redisTemplate.opsForValue().get(redisKey);
                    if (receiptInfo != null) {
                        String[] parts = receiptInfo.split(",");
                        String merchantId = parts[0].split(":")[1];
                        String content = parts[1].split(":")[1];
                        receiptService.sendReceiptWithSignature(transactionId, merchantId, content, signature);
                    }
                    return;
                }
            } catch (Exception e) {
                // 重试失败,忽略异常继续等待下一次重试
            }

            try {
                Thread.sleep(retryInterval * 1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
        // 超时后,Redis键会自动过期,由监听器处理发送无签名回执
    }
}

4. 回执发送服务

封装发送回执的具体逻辑(根据实际业务对接商户):

@Service
public class MerchantReceiptService {

    public void sendReceiptWithSignature(String transactionId, String merchantId, String content, String signature) {
        // 实现发送带签名回执的逻辑:如调用商户API、发送MQ消息等
        System.out.printf("【带签名回执】交易ID:%s,商户ID:%s,签名:%s%n", transactionId, merchantId, signature);
    }

    public void sendReceiptWithoutSignature(String transactionId, String receiptInfo) {
        String[] parts = receiptInfo.split(",");
        String merchantId = parts[0].split(":")[1];
        String content = parts[1].split(":")[1];
        // 实现发送无签名回执的逻辑
        System.out.printf("【无签名回执】交易ID:%s,商户ID:%s%n", transactionId, merchantId);
    }
}

注意事项

  • 存储交易信息时,建议用JSON序列化对象(如Jackson),比字符串拼接更易维护;
  • 集群部署时,确保所有应用实例都能监听Redis的键过期事件;
  • 可以用Spring Retry替代手动的while循环重试,代码更优雅;
  • 若担心Redis键过期事件丢失,也可以用ZSet实现延迟队列,定时扫描ZSet中的到期任务。

内容的提问来源于stack exchange,提问作者Sandy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 20:14:53