Redisson实现订单超时自动关闭(高可用)

前言

艺术来源于生活,技术来源于业务,由于原业务上的需求,需要做一个用户下单后超过一定时间内没有支付自动取消订单,说白了就是一个超时自动取消的功能,这种场景恐怕是非常非常常见的一个业务需求了,在此记录下使用redis来实现此功能。

业务分析

当产品经理说到超时取消时,第一时间想到的就是延迟队列,市面上也有很多常见的消息中间件,例如:RabbitMQ,RocketMQ,ActiveMQ,Kafka,Redis,ZeroMQ等等,都可以实现此功能,虽然轮子不断造,但合适自己的才是最好的。

技术选型

1、RabbitMQ:一开始选中的就是它,它有一个实现延迟队列的插件,正好符合要求,但后来和技术经理沟通下,由于自身服务架构来说,此方法最终被石沉大海了,此方案被pass掉。

2、Redis:第二个方案是基于Redis,使用Redis的key过期通知,在服务端捕获这个失效通知来处理业务,一开始是可行的,但后来考虑到高可用,而且当接收通知的服务宕机时,就会出现掉单,丢失消息的情况,于是此方法也被Pass掉。

3、Redis+Redisson:经过几次考虑与测试后,最终使用了此技术来实现超时订单取消的功能开发。

技术介绍

Redisson:Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

中文文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

代码示例

pom.xml依赖

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.1</version>
</dependency>

Redisson的配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
public class RedisQueueConfig {

// 连接redis的地址
@Value("${spring.redis.host}")
private String host;

//redis的端口号
@Value("${spring.redis.port}")
private String port;

//redis的密码
@Value("${spring.redis.password}")
private String password;

@Bean
public RedissonClient redissonClient(){
//此处为单机配置,高可用配置请往下看
Config config = new Config();
if (password.equals("")){
config.setCodec(new org.redisson.client.codec.StringCodec());
config.useSingleServer().setAddress("redis://" + host + ":" + port).setDatabase(2).setTimeout(5000);
}else {
config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password).setDatabase(2).setTimeout(5000);
}

return Redisson.create(config);
}
}

伪下单代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RestController
public class TestController {

private final RedissonClient redissonClient;
public TestController(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}

@RequestMapping("/hello")
public String hello(){

/**
* 目标队列
*/
RBlockingQueue<String> blockingRedPacketQueue
= redissonClient.getBlockingQueue("userOrderKey");
/**
* 定时任务将到期的元素转移到目标队列
*/
RDelayedQueue<String> delayedRedPacketQueue
= redissonClient.getDelayedQueue(blockingRedPacketQueue);
/**
* 123456代表订单号,放入队列中
* 设置10秒后到期
*/
delayedRedPacketQueue.offer("123456", 10, TimeUnit.SECONDS);
return "OK";
}
}

处理超时订单代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

@Component
public class AutoStart implements CommandLineRunner {

private static final Logger LOGGER = LoggerFactory.getLogger(AutoStart.class);

@Autowired
private RedissonClient redissonClient;


@Override
public void run(String... args) throws Exception {
RBlockingQueue<String> blockingRedPacketQueue
= redissonClient.getBlockingQueue("userOrderKey");

RDelayedQueue<String> delayedRedPacketQueue
= redissonClient.getDelayedQueue(blockingRedPacketQueue);
while (true){
/**
* 如果当前没有失效的订单,则此循环会暂时阻塞
* 取出超时订单信息
*/
String redPacket = blockingRedPacketQueue.take();
LOGGER.info("订单号:{}过期失效",redPacket);
/**
* 处理相关业务逻辑
*/
}
}
}

高可用配置

1
2
3
4
5
6
7
8
9
10
11
12

/**
* 主从部署方式
*/
Config config = new Config();
config.useMasterSlaveServers()
//设置redis主节点
.setMasterAddress("redis://127.0.0.1:6379")
//设置redis从节点
.addSlaveAddress("redis://127.0.0.2:6379", "redis://127.0.0.3:6379");
RedissonClient redisson = Redisson.create(config);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/**
* 集群部署方式
* cluster方式至少6个节点
* 3主3从,3主做sharding,3从用来保证主宕机后可以高可用
*/
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000)//集群状态扫描间隔时间,单位是毫秒
.addNodeAddress("redis://127.0.0.1:6379")
.addNodeAddress("redis://127.0.0.2:6379")
.addNodeAddress("redis://127.0.0.3:6379")
.addNodeAddress("redis://127.0.0.4:6379")
.addNodeAddress("redis://127.0.0.5:6379")
.addNodeAddress("redis://127.0.0.6:6379");
RedissonClient redissonClient = Redisson.create(config);

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 哨兵部署方式
* sentinel是采用 Paxos拜占庭协议,一般sentinel至少3个节点
*/
Config config = new Config();
config.useSentinelServers()
.setMasterName("my-sentinel-name")
.addSentinelAddress("redis://127.0.0.1:6379")
.addSentinelAddress("redis://127.0.0.2:6379")
.addSentinelAddress("redis://127.0.0.3:6379");
RedissonClient redisson = Redisson.create(config);

运行截图

访问hello接口后,等待10秒,控制台返回如下

结言

还是那句话,技术类型越来越多,轮子不停造,选对技术合适自己业务的才是最好的,无论是JDK内置的延迟队列还是基于时间轮算法的队列,都无法保证生产系统的高可用性,而Redisson很好的解决了这个问题。