SpringBoot基于Redis的订单回调流程

平时在做订单相关的业务时,一定会遇到对接第三方支付、锁定库存等情况,因为各种不确定的因素,我们无法确认该订单一定会被支付,对于这些订单,支付状态和库存的处理则需要一套相对完善的机制。常用的有基于定时器的方式、基于MQ的机制、基于redis的机制,因为项目中未使用MQ,只使用了redis,所以采用了redis的方案。

处理逻辑如下

java redis 外卖抢单 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分钟扫描一次支付中的订单)可解决大部分问题,实在不行就走人工处理。