最近做区块链项目时,由于区块链做存证的时候会有延迟(区块链默认会收集2秒内所有的transanction,然后做统一计算,打包上链),所以后端请求该接口的时候,不能立刻得到返回,只能返回状态为200的成功响应。
考虑到在不增加复杂度的情况下,不能要求客户端做二次请求,因为客户端做二次请求的时候是不知道服务端是否已经处理完成该上链请求的。所以,这部分的处理只能服务端完成后做 延时回调。
“延时回调”准确的说是实现 阶梯性异步回调通知,即如果收不到客户端的答复,会以定期的重试策略来做retry。
看起来似乎使用定时任务,一直轮询数据,每2秒查一次,取出需要被处理的数据,然后处理不就完事了吗?
如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。
但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
采取jdk自带的延迟队列能很好的优化传统的处理方案,但是该方案的弊、端也是非常致命的,所有的消息数据都是存于内存之中,一旦宕机或重启服务队列中数据就全无了,而且也无法进行扩展。
@Component
public class Broker {
DelayQueue<Item> delayQueue;
...
}
@Data
public class Item<T> implements Delayed {
private T object;
private long time;
...
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
Item item = (Item) o;
long diff = this.time - item.time;
if (diff <= 0) {// 改成>=会造成问题
return -1;
} else {
return 1;
}
}
}
设计主要包含以下几点:
一台普通的rabbitmq服务器单队列容纳千万级别的消息还是没什么压力的,而且rabbitmq集群扩展支持的也是非常好的,并且队列中的消息是可以进行持久化,即使我们重启或者宕机也能保证数据不丢失。
引入RabbitMQ的延时队列,在消息存入区块链的时候,向RabbitMQ发送延迟消息:2s后查询上链结果,并将上链结果持久化,然后发送至客户端。
这时,客户端可能有两种结果:
(1)处理成功,向服务端返回200响应。
(2)处理失败或者网络波动,服务端收到非200的其他响应或者收不到响应。对于第二种结果,需要将该消息以阶梯性延时的方式发送至延迟队列来做处理。
众所周知,引入消息队列会增加系统的复杂度:
首先要理解RabbitMQ上的几个概念:
所以,如果要使用RabbitMQ的延时队列:
通过上述方式,当一个设置了TTL的消息发送到ExchangeB上的时候,会被路由到queueB。由于queueB没有设置Consumer,所以queueB上的消息会因为没有消费而过期,过期后的消息会经由ExchangeA路由到queueA上。
后端实现最核心的一个步骤是:声明交换机、队列以及他们的绑定关系。
rabbitMQ包会采用懒加载的方式,在第一次向服务端请求的时候,会分别在服务端生成Exchange、Queue以及他们的绑定关系。
@Configuration
@Slf4j
public class RabbitMQConfig {
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
/**
* 调用正常队列过期时间 单位为微秒.
*/
private long makeCallExpire = 2000L;
//交换机
public static final String DEFALT_EXCHANGE = "blockcert.default";
public static final String DEAD_EXCHANGE = "blockcert.dead";
//路由键 用于把生产者的数据绑定到交换机上的
public static final String CERTIFICATION_ROUTINGKEY = "cert";
//队列
public static final String DEFALT_QUEUE = "blockcert.default.queue";
public static final String DEAD_QUEUE = "blockcert.dead.queue";
@Bean
public ConnectionFactory connectionFactory() {
log.info("rabbitmq的配置信息为host:[" + host + "]port:[" + port + "]username:[" + username + "]password:[" + password + "]");
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true); //自动确认机制 false为手动确认
return connectionFactory;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //必须是prototype类型
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
template.setMessageConverter(jsonMessageConverter());
return template;
}
/**
* 创建交换机
*/
@Bean("businessExchange")
public DirectExchange defaultExchange() {
return new DirectExchange(DEFALT_EXCHANGE);
}
/**
* 创建死信交换机
*/
@Bean("deadLetterExchange")
public DirectExchange lindExchangeDl() {
return (DirectExchange) ExchangeBuilder.directExchange(DEAD_EXCHANGE).durable(true)
.build();
}
/**
* 创建业务队列
*/
@Bean("businessQueue")
public Queue lindQueue() {
return QueueBuilder.durable(DEFALT_QUEUE)
.withArgument("x-dead-letter-exchange", DEAD_EXCHANGE)//设置死信交换机,并非绑定规则
.withArgument("x-message-ttl", makeCallExpire)
.withArgument("x-dead-letter-routing-key", CERTIFICATION_ROUTINGKEY)//设置死信routingKey
.build();
}
/**
* 创建死信队列
*/
@Bean("deadLetterQueue")
public Queue lindDelayQueue() {
return QueueBuilder.durable(DEAD_QUEUE).build();
}
/**
* 绑定普通队列.
*
* @return
*/
@Bean
public Binding bindBuilders(@Qualifier("businessQueue") Queue queue,
@Qualifier("businessExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(CERTIFICATION_ROUTINGKEY);
}
/**
* 绑定死信队列规则:即绑定死信交换机和死信队列
*/
@Bean
public Binding bindDeadBuilders(@Qualifier("deadLetterQueue") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(CERTIFICATION_ROUTINGKEY);
}
//json消息转换器保证接收和发送的格式一致
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jsonMessageConverter());
return factory;
}
}
当然,在做业务队列声明的时候,使用x-dead-letter-exchange
表示该正常队列上消息过期后连接的死信交换机,使用x-message-ttl
表示队列上消息的过期时间。显然,如果要对过期时间做自定义,需要一种更通用的方案才能满足需求,那么就只能将TTL设置在消息属性里了。
如果要将TTL设置在消息属性里,那么就不需要声明x-message-ttl
属性了,直接在消息里附带TTL即可。但是,如果使用在消息属性上设置TTL的方式,消息可能并不会按时“死亡“,因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,索引如果第一个消息的延时时长很长,而第二个消息的延时时长很短,则第二个消息并不会优先得到执行。
解决这个问题的方案是加入插件,下载rabbitmq_delayed_message_exchange插件,然后解压放置到RabbitMQ的插件目录。接下来,进入RabbitMQ的安装目录下的sbin目录,执行下面命令让该插件生效,然后重启RabbitMQ。
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
注意:下面这段代码。RabbitTemplate使用了Prototype而不是Singleton,是因为如果使用单例RabbitTemplate性能会比较慢。
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //必须是prototype类型
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
template.setMessageConverter(jsonMessageConverter());
return template;
}
一个很好的解决办法是使用池化的RabbitMQ,每次使用RabbitTemplate的时候去连接池里拿取。
参考:Java架构直通车——RabbitMQ池化方案
延时队列在需要延时处理的场景下非常有用,使用RabbitMQ来实现延时队列可以很好的利用RabbitMQ的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。