整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

干货:网页跳转的写法大全及其用途区别

干货:网页跳转的写法大全及其用途区别

时候,我们会希望网页自动跳转,应用场景包括:

  • 提交表单后自动转到另外一个页面,
  • 页面地址变了,希望访问老地址时能自动转到新地址,等等。

下面总结下如何在前端页面中控制跳转的方法:

利用html的refresh

<meta http-equiv="refresh" content="0;url=index.html"> 

其中0表示0秒以后跳转,可以自行设定时间。

利用js的href属性

window.location.href='index.html';

如果要设定延迟时间,则加上setTimeout

setTimeout("javascript:location.href='index.html'", 5000);

利用js的navigate方式

window.navigate("index.html");

自动刷新页面

在上述方式中,如果跳转的页面就是本页面,那么就是自动刷新页面的功能。

或者使用reload

location.reload()

跳转到上一页,下一页的方式

window.history.go(-1);

其中 -1 表示上一页,如果没有负号的就是表示下一页

如果不是1而是 2,3,4......n 则表示前进或者后退 n 页

后退还可以用

window.history.back();

两者的区别是:

go(-1):返回上一页,原页面表单中的内容会丢失;

back():返回上一页,原页表表单中的内容会保留。

前进则对应的是:

history.forward():

此外,还有一个参数 history.length 记录了页面前进的序号,如果等于0表示第一页

怎么选择

至此,自动跳转页面、刷新页面、前后切换的方法都齐了!方法多了就有了选择恐惧症?

基本原则:

单纯的页面跳转建议就用html的refresh方法,无需js代码,很简洁。

如果比较复杂,涉及js代码的业务功能,再加上跳转功能的,就用js的各种方法。

此外还要考虑页面是否刷新的问题,希望刷新就用go,否则用back/forward

延时消息(定时消息)指的在 分布式异步消息场景 下,生产端发送一条消息,希望在指定延时或者指定时间点被消费端消费到,而不是立刻被消费。

延时消息适用的业务场景非常的广泛,在分布式系统环境下,延时消息的功能一般会在下沉到中间件层,通常是 MQ 中内置这个功能或者内聚成一个公共基础服务。

本文旨在探讨常见延时消息的实现方案以及方案设计的优缺点。

实现方案

1. 基于外部存储实现的方案

这里讨论的外部存储指的是在 MQ 本身自带的存储以外又引入的其他的存储系统。

基于外部存储的方案本质上都是一个套路,将 MQ 和 延时模块 区分开来,延时消息模块是一个独立的服务/进程。延时消息先保留到其他存储介质中,然后在消息到期时再投递到 MQ。当然还有一些细节性的设计,比如消息进入的延时消息模块时已经到期则直接投递这类的逻辑,这里不展开讨论。

下述方案不同的是,采用了不同的存储系统。

基于 数据库(如MySQL)

基于关系型数据库(如MySQL)延时消息表的方式来实现。

CREATE TABLE `delay_msg` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `delivery_time` DATETIME NOT NULL COMMENT '投递时间',
  `payloads` blob COMMENT '消息内容',
  PRIMARY KEY (`id`),
  KEY `time_index` (`delivery_time`)
)

通过定时线程定时扫描到期的消息,然后进行投递。定时线程的扫描间隔理论上就是你延时消息的最小时间精度。

优点:

  • 实现简单;

缺点:

  • B+Tree索引不适合消息场景的大量写入;

基于 RocksDB

RocksDB 的方案其实就是在上述方案上选择了比较合适的存储介质。

RocksDB 在笔者之前的文章中有聊过,LSM 树根更适合大量写入的场景。滴滴开源的DDMQ中的延时消息模块 Chronos 就是采用了这个方案。

DDMQ 这个项目简单来说就是在 RocketMQ 外面加了一层统一的代理层,在这个代理层就可以做一些功能维度的扩展。延时消息的逻辑就是代理层实现了对延时消息的转发,如果是延时消息,会先投递到 RocketMQ 中 Chronos 专用的 topic 中。延时消息模块 Chronos 消费得到延时消息转出到 RocksDB,后面就是类似的逻辑了,定时扫描到期的消息,然后往 RocketMQ 中投递。

这个方案老实说是一个比较重要的方案。因为基于 RocksDB 来实现的话,从数据可用性的角度考虑,你还需要自己去处理多副本的数据同步等逻辑。

优点:

  • RocksDB LSM 树很适合消息场景的大量写入;

缺点:

  • 实现方案较重,如果你采用这个方案,需要自己实现 RocksDB 的数据容灾逻辑;

基于 Redis

再来聊聊 Redis 的方案。下面放一个比较完善的方案。

本方案来源于: https://www.cnblogs.com/lylife/p/7881950.html

  • Messages Pool 所有的延时消息存放,结构为KV结构,key为消息ID,value为一个具体的message(这里选择Redis Hash结构主要是因为hash结构能存储较大的数据量,数据较多时候会进行渐进式rehash扩容,并且对于HSET和HGET命令来说时间复杂度都是O(1))
  • Delayed Queue是16个有序队列(队列支持水平扩展),结构为ZSET,value 为 messages pool中消息ID,score为过期时间**(分为多个队列是为了提高扫描的速度)**
  • Worker 代表处理线程,通过定时任务扫描 Delayed Queue 中到期的消息

这个方案选用 Redis 存储在我看来有以下几点考虑,

  • Redis ZSET 很适合实现延时队列
  • 性能问题,虽然 ZSET 插入是一个 O(logn) 的操作,但是Redis 基于内存操作,并且内部做了很多性能方面的优化。

但是这个方案其实也有需要斟酌的地方,上述方案通过创建多个 Delayed Queue 来满足对于并发性能的要求,但这也带来了多个 Delayed Queue 如何在多个节点情况下均匀分配,并且很可能出现到期消息并发重复处理的情况,是否要引入分布式锁之类的并发控制设计?

在量不大的场景下,上述方案的架构其实可以蜕化成主从架构,只允许主节点来处理任务,从节点只做容灾备份。实现难度更低更可控。

定时线程检查的缺陷与改进

上述几个方案中,都通过线程定时扫描的方案来获取到期的消息。

定时线程的方案在消息量较少的时候,会浪费资源,在消息量非常多的时候,又会出现因为扫描间隔设置不合理导致延时时间不准确的问题。可以借助 JDK Timer 类中的思想,通过 wait-notify 来节省 CPU 资源。

获取中最近的延时消息,然后wait(执行时间-当前时间),这样就不需要浪费资源到达时间时会自动响应,如果有新的消息进入,并且比我们等待的消息还要小,那么直接notify唤醒,重新获取这个更小的消息,然后又wait,如此循环。

2. 开源 MQ 中的实现方案

再来讲讲目前自带延时消息功能的开源MQ,它们是如何实现的

RocketMQ

RocketMQ 开源版本支持延时消息,但是只支持 18 个 Level 的延时,并不支持任意时间。只不过这个 Level 在 RocketMQ 中可以自定义的,所幸来说对普通业务算是够用的。默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。

通俗地讲,设定了延时 Level 的消息会被暂存在名为 SCHEDULE_TOPIC_XXXX 的topic中,并根据 level 存入特定的queue,queueId=delayTimeLevel – 1,**即一个queue只存相同延时的消息,保证具有相同发送延时的消息能够顺序消费。**broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。

下面是整个实现方案的示意图,红色代表投递延时消息,紫色代表定时调度到期的延时消息:

优点:

  • Level 数固定,每个 Level 有自己的定时器,开销不大
  • 将 Level 相同的消息放入到同一个 Queue 中,保证了同一 Level 消息的顺序性;不同 Level 放到不同的 Queue 中,保证了投递的时间准确性;
  • 通过只支持固定的Level,将不同延时消息的排序变成了固定Level Topic 的追加写操作

缺点:

  • Level 配置的修改代价太大,固定 Level 不灵活
  • CommitLog 会因为延时消息的存在变得很大

Pulsar

Pulsar 支持“任意时间”的延时消息,但实现方式和 RocketMQ 不同。

通俗的讲,Pulsar 的延时消息会直接进入到客户端发送指定的 Topic 中,然后在堆外内存中创建一个基于时间的优先级队列,来维护延时消息的索引信息。延时时间最短的会放在头上,时间越长越靠后。在进行消费逻辑时候,再判断是否有到期需要投递的消息,如果有就从队列里面拿出,根据延时消息的索引查询到对应的消息进行消费。

如果节点崩溃,在这个 broker 节点上的 Topics 会转移到其他可用的 broker 上,上面提到的这个优先级队列也会被重建。

下面是 Pulsar 公众号中对于 Pulsar 延时消息的示意图。

乍一看会觉得这个方案其实非常简单,还能支持任意时间的消息。但是这个方案有几个比较大的问题

  • **内存开销:**维护延时消息索引的队列是放在堆外内存中的,并且这个队列是以订阅组(Kafka中的消费组)为维度的,比如你这个 Topic 有 N 个订阅组,那么如果你这个 Topic 使用了延时消息,就会创建 N 个 队列;并且随着延时消息的增多,时间跨度的增加,每个队列的内存占用也会上升。(是的,在这个方案下,支持任意的延时消息反而有可能让这个缺陷更严重)
  • **故障转移之后延时消息索引队列的重建时间开销:**对于跨度时间长的大规模延时消息,重建时间可能会到小时级别。(摘自 Pulsar 官方公众号文章)
  • 存储开销 :延时消息的时间跨度会影响到 Pulsar 中已经消费的消息数据的空间回收。打个比方,你的 Topic 如果业务上要求支持一个月跨度的延时消息,然后你发了一个延时一个月的消息,那么你这个 Topic 中底层的存储就会保留整整一个月的消息数据,即使这一个月中99%的正常消息都已经消费了。

对于前面第一点和第二点的问题,社区也设计了解决方案,在队列中加入时间分区,Broker 只加载当前较近的时间片的队列到内存,其余时间片分区持久化磁盘,示例图如下图所示:

但是目前,这个方案并没有对应的版本。可以在实际使用时,规定只能使用较小时间跨度的延时消息,来减少前两点缺陷的影响。

至于第三个方案,估计是比较难解决的,需要在数据存储层将延时消息和正常消息区分开来,单独存储延时消息。

QMQ

QMQ提供任意时间的延时/定时消息,你可以指定消息在未来两年内(可配置)任意时间内投递。

把 QMQ 放到最后,是因为我觉得 QMQ 是目前开源 MQ 中延时消息设计最合理的。里面设计的核心简单来说就是 多级时间轮 + 延时加载 + 延时消息单独磁盘存储

如果对时间轮不熟悉的可以阅读笔者的这篇文章 从 Kafka 看时间轮算法设计

QMQ的延时/定时消息使用的是两层 hash wheel 来实现的。第一层位于磁盘上,每个小时为一个刻度(默认为一个小时一个刻度,可以根据实际情况在配置里进行调整),每个刻度会生成一个日志文件(schedule log),因为QMQ支持两年内的延时消息(默认支持两年内,可以进行配置修改),则最多会生成 2 * 366 * 24=17568 个文件(如果需要支持的最大延时时间更短,则生成的文件更少)。 第二层在内存中,当消息的投递时间即将到来的时候,会将这个小时的消息索引(索引包括消息在schedule log中的offset和size)从磁盘文件加载到内存中的hash wheel上,内存中的hash wheel则是以500ms为一个刻度 。

总结一下设计上的亮点:

  • 时间轮算法适合延时/定时消息的场景,省去延时消息的排序,插入删除操作都是 O(1) 的时间复杂度;
  • 通过多级时间轮设计,支持了超大时间跨度的延时消息;
  • 通过延时加载,内存中只会有最近要消费的消息,更久的延时消息会被存储在磁盘中,对内存友好;
  • 延时消息单独存储(schedule log),不会影响到正常消息的空间回收;

总结

本文汇总了目前业界常见的延时消息方案,并且讨论了各个方案的优缺点。希望对读者有所启发。

原文 https://ricstudio.top/archives/delay-msg-designs

  • 应用场景
  • 消息延迟推送的实现
  • 测试结果

应用场景

目前常见的应用软件都有消息的延迟推送的影子,应用也极为广泛,例如:

  • 淘宝七天自动确认收货。在我们签收商品后,物流系统会在七天后延时发送一个消息给支付系统,通知支付系统将款打给商家,这个过程持续七天,就是使用了消息中间件的延迟推送功能。
  • 12306 购票支付确认页面。我们在选好票点击确定跳转的页面中往往都会有倒计时,代表着 30 分钟内订单不确认的话将会自动取消订单。其实在下订单那一刻开始购票业务系统就会发送一个延时消息给订单系统,延时30分钟,告诉订单系统订单未完成,如果我们在30分钟内完成了订单,则可以通过逻辑代码判断来忽略掉收到的消息。

在上面两种场景中,如果我们使用下面两种传统解决方案无疑大大降低了系统的整体性能和吞吐量:

  • 使用 redis 给订单设置过期时间,最后通过判断 redis 中是否还有该订单来决定订单是否已经完成。这种解决方案相较于消息的延迟推送性能较低,因为我们知道 redis 都是存储于内存中,我们遇到恶意下单或者刷单的将会给内存带来巨大压力。
  • 使用传统的数据库轮询来判断数据库表中订单的状态,这无疑增加了IO次数,性能极低。
  • 使用 jvm 原生的 DelayQueue ,也是大量占用内存,而且没有持久化策略,系统宕机或者重启都会丢失订单信息。

消息延迟推送的实现

在 RabbitMQ 3.6.x 之前我们一般采用死信队列+TTL过期时间来实现延迟队列,我们这里不做过多介绍,可以参考之前文章来了解:TTL、死信队列

在 RabbitMQ 3.6.x 开始,RabbitMQ 官方提供了延迟队列的插件,可以下载放置到 RabbitMQ 根目录下的 plugins 下。延迟队列插件下载

首先我们创建交换机和消息队列,application.properties 中配置与上一篇文章相同。

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class MQConfig {
 public static final String LAZY_EXCHANGE="Ex.LazyExchange";
 public static final String LAZY_QUEUE="MQ.LazyQueue";
 public static final String LAZY_KEY="lazy.#";
 @Bean
 public TopicExchange lazyExchange(){
 //Map<String, Object> pros=new HashMap<>();
 //设置交换机支持延迟消息推送
 //pros.put("x-delayed-message", "topic");
 TopicExchange exchange=new TopicExchange(LAZY_EXCHANGE, true, false, pros);
 exchange.setDelayed(true);
 return exchange;
 }
 @Bean
 public Queue lazyQueue(){
 return new Queue(LAZY_QUEUE, true);
 }
 @Bean
 public Binding lazyBinding(){
 return BindingBuilder.bind(lazyQueue()).to(lazyExchange()).with(LAZY_KEY);
 }
}

我们在 Exchange 的声明中可以设置exchange.setDelayed(true)来开启延迟队列,也可以设置为以下内容传入交换机声明的方法中,因为第一种方式的底层就是通过这种方式来实现的。

 //Map<String, Object> pros=new HashMap<>();
 //设置交换机支持延迟消息推送
 //pros.put("x-delayed-message", "topic");
 TopicExchange exchange=new TopicExchange(LAZY_EXCHANGE, true, false, pros);

发送消息时我们需要指定延迟推送的时间,我们这里在发送消息的方法中传入参数 new MessagePostProcessor() 是为了获得 Message对象,因为需要借助 Message对象的api 来设置延迟时间。

import com.anqi.mq.config.MQConfig;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MQSender {
 @Autowired
 private RabbitTemplate rabbitTemplate;
 //confirmCallback returnCallback 代码省略,请参照上一篇
 
 public void sendLazy(Object message){
 rabbitTemplate.setMandatory(true);
 rabbitTemplate.setConfirmCallback(confirmCallback);
 rabbitTemplate.setReturnCallback(returnCallback);
 //id + 时间戳 全局唯一
 CorrelationData correlationData=new CorrelationData("12345678909"+new Date());
 //发送消息时指定 header 延迟时间
 rabbitTemplate.convertAndSend(MQConfig.LAZY_EXCHANGE, "lazy.boot", message,
 new MessagePostProcessor() {
 @Override
 public Message postProcessMessage(Message message) throws AmqpException {
 //设置消息持久化
 message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
 //message.getMessageProperties().setHeader("x-delay", "6000");
 message.getMessageProperties().setDelay(6000);
 return message;
 }
 }, correlationData);
 }
}

我们可以观察 setDelay(Integer i)底层代码,也是在 header 中设置 s-delay。等同于我们手动设置 header

message.getMessageProperties().setHeader("x-delay", "6000");
/**
 * Set the x-delay header.
 * @param delay the delay.
 * @since 1.6
 */
public void setDelay(Integer delay) {
 if (delay==null || delay < 0) {
 this.headers.remove(X_DELAY);
 }
 else {
 this.headers.put(X_DELAY, delay);
 }
}
消费端进行消费
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
@Component
public class MQReceiver {
 @RabbitListener(queues="MQ.LazyQueue")
 @RabbitHandler
 public void onLazyMessage(Message msg, Channel channel) throws IOException{
 long deliveryTag=msg.getMessageProperties().getDeliveryTag();
 channel.basicAck(deliveryTag, true);
 System.out.println("lazy receive " + new String(msg.getBody()));
 }

测试结果

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MQSenderTest {
 @Autowired
 private MQSender mqSender;
 @Test
 public void sendLazy() throws Exception {
 String msg="hello spring boot";
 mqSender.sendLazy(msg + ":");
 }
}

果然在 6 秒后收到了消息 lazy receive hello spring boot:

转载请注明出处,谢谢。https://www.cnblogs.com/haixiang/p/10966985.html