事件驱动实现延时队列
事件驱动的延时队列是一种常见的设计模式,用于在系统中处理延迟触发的任务或事件。以下是一个实现事件发布实现消息推送可支持延时队列的实践,包括主要组件和步骤。
本地事件交给ApplicationContext处理,远程事件交给RabbitMq处理.
组件
- 事件接口(IDomainEvent):定义需要发布的事件, 可以是本地事件也可以是远程事件(消息事件).
- 事件发布者(IEventPublisher):发布事件的抽象接口, 拥有发布事件的能力
- 消息发布者(IMessageSender):RabbitMQ的消息发送接口抽象, 可推送RabbitMQ消息, 被IEventPublisher所调用.
组件: 定义事件(IDomainEvent)
事件包含的属性
- **事件发送延迟时间**:事件接口包含一个 `delay` 字段,表示事件的延迟时间。默认情况下,延迟时间为 0,即立即发送事件。
- **事件唯一标识符**:通过 `getEventId()` 方法获取每个事件的唯一标识符。
- **事件类型**:使用事件类名作为事件类型。可以通过 `getEventType()` 方法获取事件的类型。
- **事件发生时间**:通过 `getOccurredOn()` 方法获取事件的发生时间。
- **是否是本地事件**:通过 `isLocalEvent()` 方法判断事件是否为本地事件。
- **交换机**:通过 `getExchange()` 方法获取事件在 RabbitMQ 中使用的交换机名称。
- **路由键**:通过 `getRoutingKey()` 方法获取事件在 RabbitMQ 中使用的路由键。在某些情况下,该字段是非必填的。
- **是否忽略事务**:默认情况下,事件发送需要在事务提交后执行。可以通过 `ignoreTransaction()` 方法设置是否忽略事务。
事件IDomainEvent接口
import java.util.Date;
public interface IDomainEvent {
/**
* 事件发送延迟时间,目前支持远程事件
*/
int delay = 0;
/**
* 获取每个event唯一的id
* @return
*/
String getEventId();
/**
* 使用事件类名作为事件类型
* @return
*/
default String getEventType() {
return this.getClass().getName();
}
/**
* 事件发生时间
* @return
*/
Date getOccurredOn();
default int getDelay() {
return delay;
}
/**
* 是否是本地事件
* @return
*/
boolean isLocalEvent();
/**
* 交换机,rabbitmq需要
* @return
*/
String getExchange();
/**
* 路由key,rabbitmq需要,非必填
* @return
*/
String getRoutingKey();
/**
* 默认发送事件需要在事务提交后
*/
default boolean ignoreTransaction() {
return false;
}
}
组件: 事件发布者(IEventPublisher)
public interface IEventPublisher {
/**
* 发布事件
* @param domainEvent
*/
void publish(final IDomainEvent domainEvent);
}
@Slf4j
@Component
public final class RabbitEventPublisher implements IEventPublisher {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private IMessageSender messageSender;
@Override
public void publish(final IDomainEvent domainEvent) {
if (!domainEvent.ignoreTransaction() && TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
doPublish(domainEvent);
}
});
} else {
doPublish(domainEvent);
}
}
private void doPublish(IDomainEvent domainEvent) {
//本地事件
if (domainEvent.isLocalEvent()) {
applicationContext.publishEvent(domainEvent);
log.info("【异步事件】发布事件: {}", JSON.toJSONString(domainEvent));
} else {
//消息事件
messageSender.send(new PublishedEventContent(domainEvent));
}
}
}
public PublishedEventContent(IDomainEvent domainEvent) {
this.publishedTimes = 1;
this.publishedTime = System.currentTimeMillis();
this.domainEvent = domainEvent;
}
组件: 消息发送方(IMessageSender)
public interface IMessageSender {
/**
* 发送mq消息
* @param publishedEventContent
*/
void send(PublishedEventContent publishedEventContent);
}
IMessageSender的实现体现MQ 消息机制
-
确认机制(Confirm Callback):
- 当消息成功发送到交换机时,通过设置
rabbitTemplate.setConfirmCallback()
来监听确认回调。 - 如果确认回调中的
ack
参数为true
,表示消息成功到达交换机。 - 如果
ack
参数为false
,表示消息发送失败,可能由于交换机不存在或路由键错误等原因。 - 当消息发送失败时,会执行重试逻辑或进行相应的处理。
- 当消息成功发送到交换机时,通过设置
-
返回机制(Return Callback):
- 当消息无法路由到队列时,通过设置
rabbitTemplate.setReturnCallback()
来监听返回回调。 - 如果消息无法路由到队列,会触发返回回调,可以根据回调中的信息进行相应的处理。
- 在返回回调中,可以获取到消息的相关信息,如回复码(
replyCode
)、回复文本(replyText
)、交换机(exchange
)和路由键(routingKey
)等。
- 当消息无法路由到队列时,通过设置
-
重试机制(Retry):
- 初始化: 在 RabbitMQ 消息发送方的初始化阶段,设置确认机制和返回机制的回调,确保在消息发送成功或失败时执行相应的逻辑。
- 确认机制回调: 通过
setConfirmCallback
监听器,当消息成功发送到交换机时,判断ack
参数是否为true
。如果为true
,表示消息成功到达交换机,从缓存中删除相应的缓存项。如果为false
,表示消息发送失败,执行重试逻辑。 - 返回机制回调: 通过
setReturnCallback
监听器,当消息无法路由到队列时触发,根据回调中的信息执行相应的处理。这里主要用于处理消息无法到达队列的情况,例如交换机到队列的绑定错误。在返回回调中,可以获取消息的相关信息,如回复码、回复文本、交换机和路由键等。 - 重试逻辑: 在确认机制回调中,如果
ack
为false
,表示消息发送失败。通过doRetry(String id, int times)
方法执行重试逻辑。根据缓存中的重试次数,如果未达到上限,增加重试次数并重新发送消息,否则放弃重试并进行相应的处理。重试次数上限可根据实际情况设置,这里示例设置为 2 次。
@Slf4j
@Component
public class RabbitMessageSender implements IMessageSender {
//mq template
@Autowired
private RabbitTemplate rabbitTemplate;
//重试机制
@Autowired
private IRetryCache retryCache;
/**
* Publisher Confirms and Returns配置,依赖开启配置
* connectionFactory.setPublisherConfirms(true);
* connectionFactory.setPublisherReturns(true);
* spring.rabbitmq.template.mandatory=true
*
*/
@PostConstruct
public void init() {
// ack确认机制-是否到达exchange
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
// 判断correlationData是否为空,兼容部分老代码实现
if (correlationData != null) {
if (ack) {
log.info("【异步事件】事件确认成功eventId: {}", correlationData.getId());
retryCache.del(correlationData.getId());
} else {
log.info("【异步事件】事件确认失败eventId: {}, cause: {}", correlationData.getId(), cause);
doRetry(correlationData.getId(), 2);
}
}
});
// exchange是否到达queue
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
Integer delay = message.getMessageProperties().getReceivedDelay();
if (delay == null || delay == 0) {
log.error("【异步事件】exchange到queue失败回调, replyCode {}, replyText {}, 需人工介入, eventId: {}", replyCode, replyText, message.getMessageProperties().getMessageId());
} else {
log.info("【异步事件】exchange到queue可能失败回调, replyCode {}, replyText {}, eventId: {}", replyCode, replyText, message.getMessageProperties().getMessageId());
}
});
}
@Override
public void send(PublishedEventContent publishedEventContent) {
IDomainEvent domainEvent = publishedEventContent.getDomainEvent();
MessageProperties properties = new MessageProperties();
properties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
properties.setDelay(domainEvent.getDelay());
properties.setMessageId(domainEvent.getEventId());
String eventJsonStr = JSON.toJSONString(domainEvent);
Message message = rabbitTemplate.getMessageConverter().toMessage(eventJsonStr, properties);
try {
// 发送前先存一下消息,用于ack机制重发
retryCache.add(domainEvent.getEventId(), publishedEventContent);
rabbitTemplate.send(domainEvent.getExchange(), domainEvent.getRoutingKey(), message, new CorrelationData(domainEvent.getEventId()));
log.info("【异步事件】发布事件: {}", eventJsonStr);
} catch (Exception e) {
log.error("【异步事件】发布事件失败需人工介入: {}", eventJsonStr, e);
}
}
private void doRetry(String id, int times) {
PublishedEventContent publishedEventContent = retryCache.get(id);
if (publishedEventContent == null) {
return;
}
if (publishedEventContent.getPublishedTimes() > times) {
log.error("【异步事件】ACK重试达到上限,需人工介入: eventId: {}", publishedEventContent.getDomainEvent().getEventId());
retryCache.del(id);
return;
}
publishedEventContent.incrementPublishedTimes();
this.send(publishedEventContent);
}
}
存储缓存机制: 重试缓存接口
-
添加缓存项:通过
add(String id, PublishedEventContent publishedEventContent)
方法将事件的唯一标识符id
与包含事件内容的PublishedEventContent
存储到缓存中。 -
删除缓存项:通过
del(String id)
方法可以从缓存中删除特定标识符的缓存项。 -
获取缓存项:通过
get(String id)
方法可以根据唯一标识符获取相应的PublishedEventContent
缓存项。 -
获取过期缓存项:通过
getExpiredObjects()
方法获取所有过期的缓存项列表,过期时间定义为超过指定时间(CACHE_EXPIRE_TIME
)未被处理。
public interface IRetryCache {
void add(String id, PublishedEventContent publishedEventContent);
void del(String id);
PublishedEventContent get(String id);
List<PublishedEventContent> getExpiredObjects();
}
@Slf4j
@Component
public class RedisRetryCache implements IRetryCache {
private static final String CACHE_KEY = "EVENT_PUBLISHED";
private static final long CACHE_EXPIRE_TIME = 300000;
private StringRedisTemplate redisTemplate;
private String eventPublishedCacheKey;
public RedisRetryCache(StringRedisTemplate redisTemplate, @Value("${spring.application.name:}") String appName) {
this.redisTemplate = redisTemplate;
this.eventPublishedCacheKey = new StringJoiner(":").add(CACHE_KEY).add(appName).toString();
}
@Override
public void add(String id, PublishedEventContent publishedEventContent) {
redisTemplate.opsForHash().put(eventPublishedCacheKey, id, JSON.toJSONString(publishedEventContent));
}
@Override
public void del(String id) {
redisTemplate.opsForHash().delete(eventPublishedCacheKey, id);
}
@Override
public PublishedEventContent get(String id) {
Object value = redisTemplate.opsForHash().get(eventPublishedCacheKey, id);
if (value != null) {
return PublishedEventContent.convertTo(value.toString());
}
return null;
}
@Override
public List<PublishedEventContent> getExpiredObjects() {
List<PublishedEventContent> list = new ArrayList<>();
List<Object> objs = redisTemplate.opsForHash().values(eventPublishedCacheKey);
if (objs != null) {
long now = System.currentTimeMillis();
for (Object obj : objs) {
PublishedEventContent publishedEventContent = PublishedEventContent.convertTo(obj.toString());
if (now - publishedEventContent.getPublishedTime() > CACHE_EXPIRE_TIME) {
list.add(publishedEventContent);
}
}
}
return list;
}
}
总结:
- 解耦:发布者和消费者之间解耦,它们不需要直接交互,通过消息队列进行通信。
- 可靠性:保证事件或任务能够按照预定的延时时间触发,即使消费者不可用或发生故障。
- 可扩展性:可以根据需求添加新的发布者、消费者或消息队列来处理不同类型的事件或任务。
- 灵活性:可以根据业务需求设定不同的延时时间,以满足各种场景下的需求。