SpringBoot基于Redis的订单回调流程
平时在做订单相关的业务时,一定会遇到对接第三方支付、锁定库存等情况,因为各种不确定的因素,我们无法确认该订单一定会被支付,对于这些订单,支付状态和库存的处理则需要一套相对完善的机制。常用的有基于定时器的方式、基于MQ的机制、基于redis的机制,因为项目中未使用MQ,只使用了redis,所以采用了redis的方案。
处理逻辑如下
具体来说就是利用订阅redis事件过期来达到MQ延时队列的效果,实现发起订单订单后,自动查询第三方支付结果,类似于微信支付的回调逻辑。
基于类似的方式还可以处理库存锁定等情况。
redis是一个高性能NoSql数据库,目前广泛应用于缓存服务,除了常规的缓存服务外,还提供了简单的事件通知服务,redis的订单支付回调就是基于这种机制设计的。
redis的常用功能在不再赘述,这里只关注redis的事件通知服务。
首先看一下redis配置文件中notify-keyspace-events的解释。
# It is possible to select the events that Redis will notify among a set
# of classes. Every class is identified by a single character:
#
# K Keyspace events, published with __keyspace@<db>__ prefix. //键空间通知
# E Keyevent events, published with __keyevent@<db>__ prefix. //键事件通知
# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ... //通用命令,如 del、expire、rename
# $ String commands //字符串命令的通知
# l List commands //列表命令的通知
# s Set commands //集合命令的通知
# h Hash commands //哈希命令的通知
# z Sorted set commands //有序集合命令的通知
# x Expired events (events generated every time a key expires) //过期事件的通知
# e Evicted events (events generated when a key is evicted for maxmemory) //驱逐事件通知(当内存满时)
# A Alias for g$lshzxe, so that the "AKE" string means all the events. //所有事件
#
# The "notify-keyspace-events" takes as argument a string that is composed
# of zero or multiple characters. The empty string means that notifications
# are disabled.
# By default all notifications are disabled because most users don't need
# this feature and the feature has some overhead. Note that if you don't
# specify at least one of K or E, no events will be delivered.
notify-keyspace-events ""
#notify-keyspace-events”的参数是一个组成的字符串0或多个字符的。空字符串意味着通知被禁用。
#默认情况下,所有通知都是禁用的,因为大多数用户不需要。这个特性有一些开销。如果没有,请注意指定至少一个K或E,没有事件将被交付。
实现redis的订阅服务,主要就是依靠notify-keyspace-events的配置来实现的。
注意:虽然配置文件上面文档说是默认禁用,因为默认 notify-keyspace-events “”,但是,根据笔者实验结果来看,就算没有进行配置,事件通知也是可以实现的,通过命令查看发现实际上默认是AE模式,即所有事件都会通知。因为我们不需要那么多通知,所以设置未EX即可,只需要订阅key失效的事件。
127.0.0.1:6379> CONFIG GET notify-keyspace-events
1) "notify-keyspace-events"
2) "AE"
OK,基本知识差不多了,接下来开始。
#修改redis notify-keyspace-events
vim redis.conf
notify-keyspace-events "EX" #即过期事件通知模式
保存并重启,或者使用命令进行热修改。
#连接redis服务
redis-cli -h host -p port -a password
#查看当前redis事件通知配置
127.0.0.1:6379> CONFIG GET notify-keyspace-events
1) "notify-keyspace-events"
2) "AE"
#执行命令修改
127.0.0.1:6379> config set notify-keyspace-events Ex
OK
#再次查看
127.0.0.1:6379> CONFIG get notify-keyspace-events
1) "notify-keyspace-events"
2) "xE"
需要注意的是,热修改仅对本次启动有效,如果重启redis,该配置将失效,建议使用配置文件修改。
redis端配置完成,接下来是java代码。
导入jar包,可以使用redis或者redission选其一即可,因为项目使用的redission,笔者使用的是redission包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
创建redis配置类
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.redisson.config.TransportMode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.IOException;
/**
* redis配置
*
* @author 777666
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport
{
@Value("${redisson.singleServerConfig.idleConnectionTimeout}")
private Integer idleConnectionTimeout;
@Value("${redisson.singleServerConfig.connectTimeout}")
private Integer connectTimeout;
@Value("${redisson.singleServerConfig.timeout}")
private Integer timeout;
@Value("${redisson.singleServerConfig.retryAttempts}")
private Integer retryAttempts;
@Value("${redisson.singleServerConfig.retryInterval}")
private Integer retryInterval;
@Value("${redisson.singleServerConfig.password}")
private String password;
@Value("${redisson.singleServerConfig.subscriptionsPerConnection}")
private Integer subscriptionsPerConnection;
@Value("${redisson.singleServerConfig.clientName}")
private String clientName;
@Value("${redisson.singleServerConfig.address}")
private String address;
@Value("${redisson.singleServerConfig.subscriptionConnectionMinimumIdleSize}")
private Integer subscriptionConnectionMinimumIdleSize;
@Value("${redisson.singleServerConfig.subscriptionConnectionPoolSize}")
private Integer subscriptionConnectionPoolSize;
@Value("${redisson.singleServerConfig.connectionMinimumIdleSize}")
private Integer connectionMinimumIdleSize;
@Value("${redisson.singleServerConfig.connectionPoolSize}")
private Integer connectionPoolSize;
@Value("${redisson.singleServerConfig.database}")
private Integer database;
@Value("${redisson.singleServerConfig.dnsMonitoringInterval}")
private Integer dnsMonitoringInterval;
@Value("${redisson.threads}")
private Integer threads;
@Value("${redisson.nettyThreads}")
private Integer nettyThreads;
/**
* 载入配置文件配置的连接工厂
**/
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes", "deprecation" })
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
{
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//全局开关,支持jackson在反序列是使用多态
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisTemplate redisTemplate = new StringRedisTemplate();
//设置key
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
//设置value
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
/**
*redis事件监听器配置
**/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
/**
*redission的配置
**/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.setCodec(JsonJacksonCodec.INSTANCE)
.setThreads(threads)
.setNettyThreads(nettyThreads)
.setTransportMode(TransportMode.NIO)
.useSingleServer()
.setIdleConnectionTimeout(idleConnectionTimeout)
.setConnectTimeout(connectTimeout)
.setTimeout(timeout)
.setRetryAttempts(retryAttempts)
.setRetryInterval(retryInterval)
.setPassword(password)
.setSubscriptionsPerConnection(subscriptionsPerConnection)
.setAddress(address)
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
.setConnectionPoolSize(connectionPoolSize)
.setDatabase(database)
.setDnsMonitoringInterval(dnsMonitoringInterval);
return Redisson.create(config);
}
}
yml中的redission配置
redisson:
singleServerConfig:
#连接空闲超时(毫秒),默认10000
idleConnectionTimeout: 10000
#连接空闲超时(毫秒),默认10000
connectTimeout: 10000
#命令等待超时(毫秒),默认3000
timeout: 3000
#命令失败重试次数
retryAttempts: 3
#命令重试发送时间间隔(毫秒),默认1500
retryInterval: 1500
password: 123456
#单个连接最大订阅数量,默认5
subscriptionsPerConnection: 5
#客户端名称
clientName: null
address: "redis://127.0.0.1:6379"
#发布和订阅连接的最小空闲连接数,默认1
subscriptionConnectionMinimumIdleSize: 1
#发布和订阅连接池大小,默认50
subscriptionConnectionPoolSize: 50
#最小空闲连接数,默认32
connectionMinimumIdleSize: 24
#连接池大小,默认64
connectionPoolSize: 64
database: 0
#DNS监测时间间隔(毫秒),默认5000
dnsMonitoringInterval: 5000
threads: 16
nettyThreads: 32
创建事件监听类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
/**
* @ClassName RedisListener
* @Description redis过期监听
* @Author 777666
* @Date 2021/6/29 15:35
*/
@Slf4j
@Component
public class RedisListener extends KeyExpirationEventMessageListener {
public RedisListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Autowired
private OrderService orderService;
@Override
public void onMessage(Message message, byte[] pattern) {
//message.toString()可以获取失效的key
String expiredKey = message.toString();
if(expiredKey.startsWith(StaticVariable.ORDER_PAY_TASK_KEY)){
// 获取orderNo
//处理发起第三方支付后的订单支付状态
String orderNo = expiredKey.substring(expiredKey.lastIndexOf(":")+1);
orderService.handleThirdPayOrderStatus(orderNo);
log.info("自动处理工单第三方支付状态,订单编号:{}",orderNo);
}else if(expiredKey.startsWith(StaticVariable.ORDER_CONFIRM_TASK_KEY)){
// 获取orderNo
//处理订单库存状态
String orderNo = expiredKey.substring(expiredKey.lastIndexOf(":")+1);
orderService.handleOrderConfirm(orderNo);
log.info("自动处理订单确认状态,订单编号:{}",orderNo);
}
}
}
到这里,基本的代码已经完成了,关于订单接受第三方的回调代码可根据自己的业务进行编写,监听key过期只需要在创建第三方支付时存入订单过期key即可,当key过期时,系统会自动监听到key进行相应的处理,配合系统定时任务(比如设置10分钟扫描一次支付中的订单)可解决大部分问题,实在不行就走人工处理。