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
相关产品推荐
相关产品推荐

