目录
需求
在开发过程中,有很多场景都需要用到延迟队列来解决。它是一种特殊类型的消息队列,它允许把消息发送到队列中,但不立即投递给消费者,而是在一定时间后再将消息投递给消费者。延迟队列的常见使用场景有以下几种:
- 在各种购物平台上下单,订单超过30分钟未支付,自动关闭。
- 订单完成后, 如果用户一直未评价, 5天后自动好评。
- 会员到期前15天, 到期前3天分别发送短信提醒。
- 当订单一直处于未支付状态时,如何及时的关闭订单,并退还库存?
- 如何定期检查处于退款状态的订单是否已经退款成功?
实现方案
目前支持延迟队列的中间件也不少,特别是基于JMS模式下的消息中间件基本上都支持延迟队列。但是有时我们项目规模可能比较小,用不上JMS这些中间件。那么利用Redis也可以实现延迟队列的功能。
实现途径
基于分布式消息组件eventbus-spring-boot-starter 实现redis延时消息和异步及时消息的发送与订阅。
版本要求
1.SpringBoot 2.5.0.RELEASE+
2.Redis 6.2+
快速开始
引入依赖
json序列化支持Fast2json
、Fastjson
、Jackson
、Gson
等任意一种,如果存在多个json序列化工具依赖,序列化时的优先级如上。
<dependency>
<groupId>com.github.likavn</groupId>
<artifactId>eventbus-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
<!-- 各JSON序列化工具 任选一个-->
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- jackson 如果项目已引入spring-boot-starter-web,项目自带jackson依赖,不需要单独引入-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
设置消息中间件
在application.yml文件中配置消息引擎类型,如下:
eventbus:
type: redis
使用Redis5.0 新功能Stream,Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
需要在pom.xml单独引入,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
发送与订阅延时消息
发送延时消息
@Resource
private MsgSender msgSender;
// 发送异步消息
// 第一个参数 【DemoMsgDelayListener.class】为当前延时消息的处理实现类
// 第二个参数为延时消息体Object对象
// 第三个参数为延时时间,单位:秒
msgSender.sendDelayMessage(DemoMsgDelayListener.class,"922321333",5);
延时消息监听器实现类
/**
* 订阅延时消息
* 实现接口【MsgDelayListener】并设置回调消息body实体
*/
@Slf4j
@Component
public class DemoMsgDelayListener implements MsgDelayListener<String> {
@Override
public void onMessage(Message<String> message) {
String body = message.getBody();
}
}
发送与订阅异步消息
@Resource
private MsgSender msgSender;
// 发送异步消息
// 第一个参数是业务消息code
// 第二个参数是业务消息Object实体对象数据
msgSender.send("testMsgSubscribe", "charging");
订阅异步业务消息监听器实现类
/**
* 订阅异步消息
* 继承超类【MsgListener】并设置监听的消息实体对象
*/
@Slf4j
@Component
public class DemoMsgListener extends MsgListener<String> {
protected DemoMsgListener() {
// 订阅消息code
super("testMsgSubscribe");
}
// 接收业务消息体对象数据
@Override
public void onMessage(Message<String> message) {
String body = message.getBody();
}
}
也可让消息实体实现接口MsgBody
,并在实体中定义消息编码,使得同一类型的消息在定义监听器或发送消息时不需要单独设置消息编码。
// 定义消息实体
@Data
public class TestBody implements MsgBody {
private String content;
@Override
public String code() {
return MsgConstant.TEST_MSG_SUBSCRIBE_LISTENER;
}
}
发送TestBody实体消息,如下:
// 发送消息
@Resource
private MsgSender msgSender;
// 发送异步消息
TestBody testBody = new TestBody();
testBody.setContent("这是一个测试消息!!!");
// 第一个参数是业务消息Object实体对象数据
msgSender.send(testBody);
定义消息监听器(不需要指定消息编码),如下:
@Slf4j
@Component
public class DemoMsgListener3 extends MsgListener<TestBody> {
@Override
public void onMessage(Message<TestBody> message) {
TestBody body = message.getBody();
}
}
异常捕获
当消息或延时消息投递失败时,可以自定义消息重复投递次数和下次消息投递时间间隔(系统默认重复投递3次,每次间隔10秒),即便这样,消息还是有可能会存在投递不成功的问题,当消息进行最后一次投递还是失败时,可以使用注解@Fail
标识在消息处理类的接收方法或处理类上,到达最大重复投递次数且还是投递失败时调用 callMethod
此方法,即可捕获投递错误异常及数据。如下:
/**
* 订阅异步消息
* 继承超类【MsgSubscribeListener】并设置监听的消息实体对象
*/
@Slf4j
@Component
public class DemoMsgSubscribeListener extends MsgListener<String> {
protected DemoMsgSubscribeListener() {
// 订阅消息code
super("testMsgSubscribe");
}
// 接收业务消息体对象数据
// @Fail消息投递失败时重试,callMethod=投递失败时异常处理方法名,这里设置重试2次,下次重试间隔5秒(引擎为rocketMq时,此处延时时间为rocketMq的18个延时级别)后触发
@Override
@Fail(callMethod = "exceptionHandler", retry = 2, nextTime = 5)
public void onMessage(Message<String> message) {
String body = message.getBody();
log.info("接收数据: {}", message.getRequestId());
// throw new RuntimeException("DemoMsgSubscribeListener test");
}
/**
* 消息投递失败处理,参数顺序无要求
*/
public void exceptionHandler(Message<String> message, Throwable throwable) {
log.error("消息投递失败!: {},{}", message.getRequestId(), throwable.getMessage());
}
}
更多教程详情访问: