夜店小新新

夜店小新新 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

fight 加油

个人动态

夜店小新新 赞了回答 · 9月6日

解决面试经典问题:Cookie禁用了,Session还能用吗?

这个问题下面的所有答案都被人恶意的踩,麻烦管理员出来主持公道

默认SESSION配置

默认的JSP、PHP配置中,SessionID是需要存储在Cookie中的,默认Cookie名为:

  • PHPSESSIONID

  • JSESSIONID

以下以PHP为例:

  1. 你第一次访问网站时,

  2. 服务端脚本中开启了Sessionsession_start();

  3. 服务器会生成一个不重复的 SESSIONID 的文件session_id();,比如在/var/lib/php/session目录

  4. 并将返回(Response)如下的HTTP头 Set-Cookie:PHPSESSIONID=xxxxxxx

  5. 客户端接收到Set-Cookie的头,将PHPSESSIONID写入cookie

  6. 当你第二次访问页面时,所有Cookie会附带的请求头(Request)发送给服务器端

  7. 服务器识别PHPSESSIONID这个cookie,然后去session目录查找对应session文件,

  8. 找到这个session文件后,检查是否过期,如果没有过期,去读取Session文件中的配置;如果已经过期,清空其中的配置

如果客户端禁用了Cookie,那PHPSESSIONID都无法写入客户端,Session还能用?

答案显而易见:不能

并且服务端因为没有得到PHPSESSIONID的cookie,会不停的生成session_id文件

取巧传递session_id

但是这难不倒服务端程序,聪明的程序员想到,如果一个Cookie都没接收到,基本上可以预判客户端禁用了Cookie,那将session_id附带在每个网址后面(包括POST),
比如:

GET http://www.xx.com/index.php?session_id=xxxxx
POST http://www.xx.com/post.php?session_id=xxxxx

然后在每个页面的开头使用session_id($_GET['session_id']),来强制指定当前session_id

这样,答案就变成了:

聪明的你肯定想到,那将这个网站发送给别人,那么他将会以你的身份登录并做所有的事情
(目前很多订阅公众号就将openid附带在网址后面,这是同样的漏洞)。

其实不仅仅如此,cookie也可以被盗用,比如XSS注入,通过XSS漏洞获取大量的Cookie,也就是控制了大量的用户,腾讯有专门的XSS漏洞扫描机制,因为大量的QQ盗用,发广告就是因为XSS漏洞

所以Laravel等框架中,内部实现了Session的所有逻辑,并将PHPSESSIONID设置为httponly并加密,这样,前端JS就无法读取和修改这些敏感信息,降低了被盗用的风险。

Cookie在现代

禁用Cookie是 IE6 那个年代的事情,现在的网站都非常的依赖Cookie,禁用Cookie会造成大量的麻烦。

在Flash还流行的年代,Flash在提交数据会经常出现用户无法找到的情况,其实是因为Flash在IE下是独立的程序,无法得到IE下的Cookie。
所以在Flash的flash_var中,一般都会指定当前的session_id,让Flash提交数据的时候,将这个session_id附带着提交过去
Chrome中使用 Flash沙箱 已经解决了cookie的问题,但是为了兼容IE,比如swfupload等flash程序都要求开发者附带一个session_id

面试者的用意

面试者出此题也是为了考察你对HTTP协议和服务器会话的理解。

关注 50 回答 13

夜店小新新 赞了文章 · 9月2日

Kubernetes Service详解

为什么需要service

Kubernetes可以方便的为容器应用提供了一个持续运行且方便扩展的环境,但是,应用最终是要被用户或其他应用访问、调用的。要访问应用pod,就会有以下两个问题:

  1. pod是有生命周期的。它会根据集群的期望状态不断的在创建、删除、更新,所以pod的ip也在不断变化,如何访问到不断变化的pod?
  2. 通常一个应用不会单只有一个pod,而是由多个相同功能的pod共同提供服务的。那么对这个应用的访问,如何在多个pod中负载均衡?

service主要就是用来解决这两个问题的。简单来说,它是一个抽象的api对象,用来表示一组提供相同服务的pod及对这组pod的访问方式。

service的实现

service作为一个类似中介的角色,对内,它要能代理访问到不断变换的一组后端Pod;对外,它要能暴露自己给集群内部或外部的其他资源访问。我们分别来看下具体是怎么实现的。

后端代理

之前的文章kubeadm部署最后的测试部分,创建了一组pod及服务来验证业务,继续以这个例子来说明:

集群中已经有如下一组pod:

NAME                     READY   STATUS       IP            NODE     APP
goweb-55c487ccd7-5t2l2   1/1     Running     10.244.1.15   node-1   goweb
goweb-55c487ccd7-cp6l8   1/1     Running     10.244.3.9    node-2   goweb
goweb-55c487ccd7-gcs5x   1/1     Running     10.244.1.17   node-1   goweb
goweb-55c487ccd7-pp6t6   1/1     Running     10.244.3.10   node-2   goweb

pod都带有app:goweb标签,对外暴露8000端口,访问/info路径会返回主机名。

创建service

创建一个servcie有两种方式

  • 命令式
$ kubectl expose deployment goweb --name=gowebsvc --port=80  --target-port=8000  
  • 声明式
# 定义服务配置文件
# svc-goweb.yaml
apiVersion: v1
kind: Service
metadata:
  name: gowebsvc
spec:
  selector:
    app: goweb
  ports:
  - name: default
    protocol: TCP
    port: 80
    targetPort: 8000
  type: ClusterIP
# 创建服务
$ kubectl apply -f svc-goweb.yaml

我们来看下配置文件中几个重点字段:

  • selector指定了app: goweb标签。说明该svc代理所有包含有"app: goweb"的pod
  • port字段指定了该svc暴露80端口
  • targetPort指定改svc代理对应pod的8000端口
  • type定义了svc的类型为ClusterIP,这也是svc的默认类型

通过apply创建服务后,来查看一下服务状态


$ kubectl  get svc gowebsvc  -o wide
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE   SELECTOR
gowebsvc   ClusterIP   10.106.202.0   <none>        80/TCP    3d   app=goweb

可以看到,Kubernetes自动为服务分配了一个CLUSTER-IP。通过这个访问这个IP的80端口,就可以访问到"app: goweb"这组pod的8000端口,并且可以在这组pod中负载均衡。

[root@master-1 ~]# curl http://10.106.202.0/info
Hostname: goweb-55c487ccd7-gcs5x
[root@master-1 ~]# curl http://10.106.202.0/info
Hostname: goweb-55c487ccd7-cp6l8
[root@master-1 ~]# curl http://10.106.202.0/info
Hostname: goweb-55c487ccd7-pp6t6

请求代理转发

cluster-ip是一个虚拟的ip地址,并不是某张网卡的真实地址。那具体的请求代理转发过程是怎么实现的呢? 答案是iptables。我们来看下iptables中与cluster-ip相关的规则

[root@master-1 ~]# iptables-save | grep 10.106.202.0
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-SVC-SEG6BTF25PWEPDFT

可以看到,目的地址为CLUSTER-IP、目的端口为80的数据包,会被转发到KUBE-MARK-MASQ与KUBE-SVC-SEG6BTF25PWEPDFT链上。其中,KUBE-MARK-MASQ链的作用是给数据包打上特定的标记(待验证),重点来看下KUBE-SVC-SEG6BTF25PWEPDFT链:

-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-5ZXTVLEM4DKNW7T2
-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-EBFXI7VOCPDT2QU5
-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-C3PKSXKMO2M43SPF
-A KUBE-SVC-SEG6BTF25PWEPDFT -j KUBE-SEP-2GQCCNJGO65Z5MFS

可以看到,KUBE-SVC-SEG6BTF25PWEPDFT链通过设置--probability,将请求等概率转发到4条链上,查看其中一条转发链:

[root@master-1 ~]# iptables-save | grep  "A KUBE-SEP-5ZXTVLEM4DKNW7T2" 
-A KUBE-SEP-5ZXTVLEM4DKNW7T2 -s 10.244.1.15/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-5ZXTVLEM4DKNW7T2 -p tcp -m tcp -j DNAT --to-destination 10.244.1.15:8000

发现KUBE-SEP-5ZXTVLEM4DKNW7T2这条规则对请求的目的地址作了DNAT到10.244.1.15:8000,这正是goweb组中goweb-55c487ccd7-5t2l2这个pod的ip地址。这样,对svc的CLUSTER-IP的请求,就会通过iptables规则转发到相应的pod。

但是,还有个问题,svc是怎么跟踪pod的ip变化的?
注意到前面的nat规则,第一次转发的链名称是KUBE-SVC-xxx,第二次转发给具体pod的链名称是KUBE-SEP-xxx,这里的SEP实际指的是kubernetes另一个对象endpoint,我们可以通过vkubectl get ep命令来查看:

[root@master-1 ~]# kubectl  get ep gowebsvc
NAME         ENDPOINTS 
gowebsvc     10.244.1.15:8000,10.244.1.17:8000,10.244.3.10:8000 + 1 more...   35d

在svc创建的时候,kube-proxy组件会自动创建同名的endpoint对象,动态地跟踪匹配selector的一组pod当前ip及端口,并生成相应的iptables KUBE-SVC-xxx规则。

请求代理的三种方式

上面说的请求代理转发的方式,是kubernetes目前版本的默认方式,实际上,service的代理方式一共有三种:

Userspace 模式

在这种模式下,kube-proxy为每个服务都打开一个随机的端口,所有访问这个端口的请求都会被转发到服务对应endpoints指定的后端。最后,kube-proxy还会生成一条iptables规则,把访问cluster-ip的请求重定向到上面说的随机端口,最终转发到后端pod。整个过程如下图所示:

clipboard.png

Userspace模式的代理转发主要依靠kube-proxy实现,工作在用户态。所以,转发效率不高。较为不推荐用该种模式。

iptables 模式

iptables模式是目前版本的默认服务代理转发模式,上两小节做过详细说明的就是这种模式,来看下请求转发的示意图:

clipboard.png

与userspace模式最大的不同点在于,kube-proxy只动态地维护iptables,而转发完全靠iptables实现。由于iptables工作在内核态,不用在用户态与内核态切换,所以相比userspace模式更高效也更可靠。但是每个服务都会生成若干条iptables规则,大型集群iptables规则数会非常多,造成性能下降也不易排查问题。

ipvs 模式

在v1.9版本以后,服务新增了ipvs转发方式。kube-proxy同样只动态跟踪后端endpoints的情况,然后调用netlink接口来生成ipvs规则。通过ipvs来转发请求:

clipboard.png

ipvs同样工作在内核态,而且底层转发是依靠hash表实现,所以性能比iptables还要好的多,同步新规则也比iptables快。同时,负载均衡的方式除了简单rr还有多种选择,所以很适合在大型集群使用。而缺点就是带来了额外的配置维护操作。

集群内部服务发现

在集群内部对一个服务的访问,主要有2种方式,环境变量与DNS。

环境变量方式

当一个pod创建时,集群中属于同个namespace下的所有service对象信息都会被作为环境变量添加到pod中。随便找一个pod查看一下:

$ kubectl exec goweb-55c487ccd7-5t2l2 'env' | grep GOWEBSVC
GOWEBSVC_PORT_80_TCP_ADDR=10.106.202.0
GOWEBSVC_SERVICE_PORT=80
GOWEBSVC_SERVICE_PORT_DEFAULT=80
GOWEBSVC_PORT_80_TCP=tcp://10.106.202.0:80
GOWEBSVC_PORT_80_TCP_PROTO=tcp
GOWEBSVC_PORT_80_TCP_PORT=80
GOWEBSVC_PORT=tcp://10.106.202.0:80
GOWEBSVC_SERVICE_HOST=10.106.202.0

可以看到,pod通过{SVCNAME}_SERVICE_HOST/PORT就可以方便的访问到某个服务。这种访问方式简单易用,可以用来快速测试服务。但最大的问题就是,服务必须先于pod创建,后创建的服务是不会添加到现有pod的环境变量中的。

DNS方式

DNS组件是k8s集群的可选组件,它会不停监控k8s API,在有新服务创建时,自动创建相应的DNS记录。。以gowebsvc为例,在服务创建时,会创建一条gowebsvc.default.svc.cluster.local的dns记录指向服务。而且dns记录作用域是整个集群,不局限在namespace。
虽然是可选组件,但DNS生产环境可以说是必备的组件了。这里先简单说明,后面打算专门开篇文章来详细介绍。

集群外部的服务暴露

服务发现解决了集群内部访问pod问题,但很多时候,pod提供的服务也是要对集群外部来暴露访问的,最典型的就是web服务。k8s中的service有多种对外暴露的方式,可以在部署Service时通过ServiceType字段来指定。默认情况下,ServiceType配置是只能内部访问的ClusterIP方式,前面的例子都是这种模式,除此之外,还可以配置成下面三种方式:

NodePort方式:

该方式把服务暴露在每个Node主机IP的特定端口上,同一个服务在所有Node上端口是相同的,并自动生成相应的路由转发到ClusterIP。这样,集群外部通过<NodeIP>:<NodePort>就可以访问到对应的服务。举个例子:

## 创建svc,通过Nodeport方式暴露服务
$ kubectl expose deployment goweb --name=gowebsvc-nodeport --port=80  --target-port=8000  --type=NodePort 
## 查看svc,可以看到NodePort随机分配的端口为32538
$ kubectl get svc gowebsvc-nodeport 
NAME                TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
gowebsvc-nodeport   NodePort   10.101.166.252   <none>        80:32538/TCP   86s
## 随便访问一个nodeip的32538端口,都可以访问到gowebsvc-nodeport服务对应的pod
$ curl 172.16.201.108:32538/info
Hostname: goweb-55c487ccd7-pp6t6
$ curl 172.16.201.109:32538/info
Hostname: goweb-55c487ccd7-5t2l2

LoadBalance:

LoadBalance方式主要是给公有云服务使用的,通过配置LoadBalance,可以触发公有云创建负载均衡器,并把node节点作为负载的后端节点。每个公有云的配置方式不同,具体可以参考各公有云的相关文档。

ExternalName:

当ServiceType被配置为这种方式时,该服务的目的就不是为了外部访问了,而是为了方便集群内部访问外部资源。举个例子,假如目前集群的pod要访问一组DB资源,而DB是部署在集群外部的物理机,还没有容器化,可以配置这么一个服务:

apiVersion: v1
kind: Service
metadata:
  name: dbserver
  namespace: default
spec:
  type: ExternalName
  externalName: database.abc.com

这样,集群内部的pod通过dbserver.default.svc.cluster.local这个域名访问这个服务时,请求会被cname到database.abc.com来。过后,假如db容器化了,不需要修改业务代码,直接修改service,加上相应selector就可以了。

几种特殊的service

除了上面这些通常的service配置,还有几种特殊情况:

Multi-Port Services

service可以配置不止一个端口,比如官方文档的例子:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  - name: https
    protocol: TCP
    port: 443
    targetPort: 9377

这个service保留了80与443端口,分别对应pod的9376与9377端口。这里需要注意的是,pod的每个端口一定指定name字段(默认是default)。

Headless services

Headless services是指一个服务没有配置了clusterIP=None的服务。这种情况下,kube-proxy不会为这个服务做负载均衡的工作,而是交予DNS完成。具体又分为2种情况:

  • 有配置selector: 这时候,endpoint控制器会为服务生成对应pod的endpoint对象。service对应的DNS返回的是endpoint对应后端的集合。
  • 没有配置selector:这时候,endpoint控制器不会自动为服务生成对应pod的endpoint对象。若服务有配置了externalname,则生成一套cnmae记录,指向externalname。如果没有配置,就需要手动创建一个同名的endpoint对象。dns服务会创建一条A记录指向endpoint对应后端。

External IPs

如果有个非node本地的IP地址,可以通过比如外部负载均衡的vip等方式被路由到任意一台node节点,那就可以通过配置service的externalIPs字段,通过这个IP地址访问到服务。集群以这个IP为目的IP的请求时,会把请求转发到对应服务。参考官方文档的例子:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  externalIPs:
  - 80.11.12.10

这里的80.11.12.10就是一个不由kubernetes维护的公网IP地址,通过80.11.12.10:80就可以访问到服务对应的pod。

简单总结下,service对象实际上解决的就是一个分布式系统的服务发现问题,把相同功能的pod做成一个服务集也能很好的对应微服务的架构。在目前的kubernetes版本中,service还只能实现4层的代理转发,并且要搭配好DNS服务才能真正满足生产环境的需求。

查看原文

赞 4 收藏 2 评论 0

夜店小新新 赞了文章 · 6月11日

6种 延时队列的实现方案,小白也能看的懂!

个人博客地址:http://www.chengxy-nds.top,别有洞天

五一期间原计划是写两篇文章,看一本技术类书籍,结果这五天由于自律性过于差,禁不住各种诱惑,我连电脑都没打开过,计划完美宣告失败。所以在这能看出和大佬之间的差距,人家没白没夜的更文,比你优秀的人比你更努力,难以望其项背,真是让我自愧不如。

知耻而后勇,这不逼着自己又学起来了,个人比较喜欢一些实践类的东西,既学习到知识又能让技术落地,能搞出个demo最好,本来不知道该分享什么主题,好在最近项目紧急招人中,而我有幸做了回面试官,就给大家整理分享一道面试题:“如何实现延时队列?”。

下边会介绍多种实现延时队列的思路,文末提供有几种实现方式的 github地址。其实哪种方式都没有绝对的好与坏,只是看把它用在什么业务场景中,技术这东西没有最好的只有最合适的。

一、延时队列的应用

什么是延时队列?顾名思义:首先它要具有队列的特性,再给它附加一个延迟消费队列消息的功能,也就是说可以指定队列中的消息在哪个时间点被消费。

延时队列在项目中的应用还是比较多的,尤其像电商类平台:

1、订单成功后,在30分钟内没有支付,自动取消订单

2、外卖平台发送订餐通知,下单成功后60s给用户推送短信。

3、如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存

4、淘宝新建商户一个月内还没上传商品信息,将冻结商铺等

。。。。

上边的这些场景都可以应用延时队列解决。

二、延时队列的实现

我个人一直秉承的观点:工作上能用JDK自带API实现的功能,就不要轻易自己重复造轮子,或者引入三方中间件。一方面自己封装很容易出问题(大佬除外),再加上调试验证产生许多不必要的工作量;另一方面一旦接入三方的中间件就会让系统复杂度成倍的增加,维护成本也大大的增加。

1、DelayQueue 延时队列

JDK 中提供了一组实现延迟队列的API,位于Java.util.concurrent包下DelayQueue

DelayQueue是一个BlockingQueue(无界阻塞)队列,它本质就是封装了一个PriorityQueue(优先队列),PriorityQueue内部使用完全二叉堆(不知道的自行了解哈)来实现队列元素排序,我们在向DelayQueue队列中添加元素时,会给元素一个Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首。队列中的元素只有到了Delay时间才允许从队列中取出。队列中可以放基本数据类型或自定义实体类,在存放基本数据类型时,优先队列中元素默认升序排列,自定义实体类就需要我们根据类属性值比较计算了。

先简单实现一下看看效果,添加三个order入队DelayQueue,分别设置订单在当前时间的5秒10秒15秒后取消。
在这里插入图片描述

要实现DelayQueue延时队列,队中元素要implementsDelayed 接口,这哥接口里只有一个getDelay方法,用于设置延期时间。Order类中compareTo方法负责对队列中的元素进行排序。

public class Order implements Delayed {
    /**
     * 延迟时间
     */
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private long time;
    String name;
    
    public Order(String name, long time, TimeUnit unit) {
        this.name = name;
        this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
    }
    
    @Override
    public long getDelay(TimeUnit unit) {
        return time - System.currentTimeMillis();
    }
    @Override
    public int compareTo(Delayed o) {
        Order Order = (Order) o;
        long diff = this.time - Order.time;
        if (diff <= 0) {
            return -1;
        } else {
            return 1;
        }
    }
}

DelayQueueput方法是线程安全的,因为put方法内部使用了ReentrantLock锁进行线程同步。DelayQueue还提供了两种出队的方法 poll()take()poll() 为非阻塞获取,没有到期的元素直接返回null;take() 阻塞方式获取,没有到期的元素线程将会等待。

public class DelayQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        Order Order1 = new Order("Order1", 5, TimeUnit.SECONDS);
        Order Order2 = new Order("Order2", 10, TimeUnit.SECONDS);
        Order Order3 = new Order("Order3", 15, TimeUnit.SECONDS);
        DelayQueue<Order> delayQueue = new DelayQueue<>();
        delayQueue.put(Order1);
        delayQueue.put(Order2);
        delayQueue.put(Order3);

        System.out.println("订单延迟队列开始时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        while (delayQueue.size() != 0) {
            /**
             * 取队列头部元素是否过期
             */
            Order task = delayQueue.poll();
            if (task != null) {
                System.out.format("订单:{%s}被取消, 取消时间:{%s}\n", task.name, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            }
            Thread.sleep(1000);
        }
    }
}

上边只是简单的实现入队与出队的操作,实际开发中会有专门的线程,负责消息的入队与消费。

执行后看到结果如下,Order1Order2Order3 分别在 5秒10秒15秒后被执行,至此就用DelayQueue实现了延时队列。

订单延迟队列开始时间:2020-05-06 14:59:09
订单:{Order1}被取消, 取消时间:{2020-05-06 14:59:14}
订单:{Order2}被取消, 取消时间:{2020-05-06 14:59:19}
订单:{Order3}被取消, 取消时间:{2020-05-06 14:59:24}
2、Quartz 定时任务

Quartz一款非常经典任务调度框架,在RedisRabbitMQ还未广泛应用时,超时未支付取消订单功能都是由定时任务实现的。定时任务它有一定的周期性,可能很多单子已经超时,但还没到达触发执行的时间点,那么就会造成订单处理的不够及时。

引入quartz框架依赖包

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

在启动类中使用@EnableScheduling注解开启定时任务功能。

@EnableScheduling
@SpringBootApplication
public class DelayqueueApplication {
    public static void main(String[] args) {
        SpringApplication.run(DelayqueueApplication.class, args);
    }
}

编写一个定时任务,每个5秒执行一次。

@Component
public class QuartzDemo {

    //每隔五秒
    @Scheduled(cron = "0/5 * * * * ? ")
    public void process(){
        System.out.println("我是定时任务!");
    }
}
3、Redis sorted set

Redis的数据结构Zset,同样可以实现延迟队列的效果,主要利用它的score属性,redis通过score来为集合中的成员进行从小到大的排序。
在这里插入图片描述
通过zadd命令向队列delayqueue 中添加元素,并设置score值表示元素过期的时间;向delayqueue 添加三个order1order2order3,分别是10秒20秒30秒后过期。

 zadd delayqueue 3 order3

消费端轮询队列delayqueue, 将元素排序后取最小时间与当前时间比对,如小于当前时间代表已经过期移除key

    /**
     * 消费消息
     */
    public void pollOrderQueue() {

        while (true) {
            Set<Tuple> set = jedis.zrangeWithScores(DELAY_QUEUE, 0, 0);

            String value = ((Tuple) set.toArray()[0]).getElement();
            int score = (int) ((Tuple) set.toArray()[0]).getScore();
            
            Calendar cal = Calendar.getInstance();
            int nowSecond = (int) (cal.getTimeInMillis() / 1000);
            if (nowSecond >= score) {
                jedis.zrem(DELAY_QUEUE, value);
                System.out.println(sdf.format(new Date()) + " removed key:" + value);
            }

            if (jedis.zcard(DELAY_QUEUE) <= 0) {
                System.out.println(sdf.format(new Date()) + " zset empty ");
                return;
            }
            Thread.sleep(1000);
        }
    }

我们看到执行结果符合预期

2020-05-07 13:24:09 add finished.
2020-05-07 13:24:19 removed key:order1
2020-05-07 13:24:29 removed key:order2
2020-05-07 13:24:39 removed key:order3
2020-05-07 13:24:39 zset empty 
4、Redis 过期回调

Rediskey过期回调事件,也能达到延迟队列的效果,简单来说我们开启监听key是否过期的事件,一旦key过期会触发一个callback事件。

修改redis.conf文件开启notify-keyspace-events Ex

notify-keyspace-events Ex

Redis监听配置,注入Bean RedisMessageListenerContainer

@Configuration
public class RedisListenerConfig {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

编写Redis过期回调监听方法,必须继承KeyExpirationEventMessageListener ,有点类似于MQ的消息监听。

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
 
    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        System.out.println("监听到key:" + expiredKey + "已过期");
    }
}

到这代码就编写完成,非常的简单,接下来测试一下效果,在redis-cli客户端添加一个key 并给定3s的过期时间。

 set xiaofu 123 ex 3

在控制台成功监听到了这个过期的key

监听到过期的key为:xiaofu
5、RabbitMQ 延时队列

利用 RabbitMQ 做延时队列是比较常见的一种方式,而实际上RabbitMQ 自身并没有直接支持提供延迟队列功能,而是通过 RabbitMQ 消息队列的 TTLDXL这两个属性间接实现的。

先来认识一下 TTLDXL两个概念:

Time To Live(TTL) :

TTL 顾名思义:指的是消息的存活时间,RabbitMQ可以通过x-message-tt参数来设置指定Queue(队列)和 Message(消息)上消息的存活时间,它的值是一个非负整数,单位为微秒。

RabbitMQ 可以从两种维度设置消息过期时间,分别是队列消息本身

  • 设置队列过期时间,那么队列中所有消息都具有相同的过期时间。
  • 设置消息过期时间,对队列中的某一条消息设置过期时间,每条消息TTL都可以不同。

如果同时设置队列和队列中消息的TTL,则TTL值以两者中较小的值为准。而队列中的消息存在队列中的时间,一旦超过TTL过期时间则成为Dead Letter(死信)。

Dead Letter ExchangesDLX

DLX即死信交换机,绑定在死信交换机上的即死信队列。RabbitMQQueue(队列)可以配置两个参数x-dead-letter-exchangex-dead-letter-routing-key(可选),一旦队列内出现了Dead Letter(死信),则按照这两个参数可以将消息重新路由到另一个Exchange(交换机),让消息重新被消费。

x-dead-letter-exchange:队列中出现Dead Letter后将Dead Letter重新路由转发到指定 exchange(交换机)。

x-dead-letter-routing-key:指定routing-key发送,一般为要指定转发的队列。

队列出现Dead Letter的情况有:

  • 消息或者队列的TTL过期
  • 队列达到最大长度
  • 消息被消费端拒绝(basic.reject or basic.nack)

下边结合一张图看看如何实现超30分钟未支付关单功能,我们将订单消息A0001发送到延迟队列order.delay.queue,并设置x-message-tt消息存活时间为30分钟,当到达30分钟后订单消息A0001成为了Dead Letter(死信),延迟队列检测到有死信,通过配置x-dead-letter-exchange,将死信重新转发到能正常消费的关单队列,直接监听关单队列处理关单逻辑即可。
在这里插入图片描述

发送消息时指定消息延迟的时间

public void send(String delayTimes) {
        amqpTemplate.convertAndSend("order.pay.exchange", "order.pay.queue","大家好我是延迟数据", message -> {
            // 设置延迟毫秒值
            message.getMessageProperties().setExpiration(String.valueOf(delayTimes));
            return message;
        });
    }
}

设置延迟队列出现死信后的转发规则

/**
     * 延时队列
     */
    @Bean(name = "order.delay.queue")
    public Queue getMessageQueue() {
        return QueueBuilder
                .durable(RabbitConstant.DEAD_LETTER_QUEUE)
                // 配置到期后转发的交换
                .withArgument("x-dead-letter-exchange", "order.close.exchange")
                // 配置到期后转发的路由键
                .withArgument("x-dead-letter-routing-key", "order.close.queue")
                .build();
    }
6、时间轮

前边几种延时队列的实现方法相对简单,比较容易理解,时间轮算法就稍微有点抽象了。kafkanetty都有基于时间轮算法实现延时队列,下边主要实践Netty的延时队列讲一下时间轮是什么原理。

先来看一张时间轮的原理图,解读一下时间轮的几个基本概念
在这里插入图片描述
wheel :时间轮,图中的圆盘可以看作是钟表的刻度。比如一圈round 长度为24秒,刻度数为 8,那么每一个刻度表示 3秒。那么时间精度就是 3秒。时间长度 / 刻度数值越大,精度越大。

当添加一个定时、延时任务A,假如会延迟25秒后才会执行,可时间轮一圈round 的长度才24秒,那么此时会根据时间轮长度和刻度得到一个圈数 round和对应的指针位置 index,也是就任务A会绕一圈指向0格子上,此时时间轮会记录该任务的roundindex信息。当round=0,index=0 ,指针指向0格子 任务A并不会执行,因为 round=0不满足要求。

所以每一个格子代表的是一些时间,比如1秒25秒 都会指向0格子上,而任务则放在每个格子对应的链表中,这点和HashMap的数据有些类似。

Netty构建延时队列主要用HashedWheelTimerHashedWheelTimer底层数据结构依然是使用DelayedQueue,只是采用时间轮的算法来实现。

下面我们用Netty 简单实现延时队列,HashedWheelTimer构造函数比较多,解释一下各参数的含义。

  • ThreadFactory :表示用于生成工作线程,一般采用线程池;
  • tickDurationunit:每格的时间间隔,默认100ms;
  • ticksPerWheel:一圈下来有几格,默认512,而如果传入数值的不是2的N次方,则会调整为大于等于该参数的一个2的N次方数值,有利于优化hash值的计算。
public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel) {
        this(threadFactory, tickDuration, unit, ticksPerWheel, true);
    }
  • TimerTask:一个定时任务的实现接口,其中run方法包装了定时任务的逻辑。
  • Timeout:一个定时任务提交到Timer之后返回的句柄,通过这个句柄外部可以取消这个定时任务,并对定时任务的状态进行一些基本的判断。
  • Timer:是HashedWheelTimer实现的父接口,仅定义了如何提交定时任务和如何停止整个定时机制。
public class NettyDelayQueue {

    public static void main(String[] args) {

        final Timer timer = new HashedWheelTimer(Executors.defaultThreadFactory(), 5, TimeUnit.SECONDS, 2);

        //定时任务
        TimerTask task1 = new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                System.out.println("order1  5s 后执行 ");
                timer.newTimeout(this, 5, TimeUnit.SECONDS);//结束时候再次注册
            }
        };
        timer.newTimeout(task1, 5, TimeUnit.SECONDS);
        TimerTask task2 = new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                System.out.println("order2  10s 后执行");
                timer.newTimeout(this, 10, TimeUnit.SECONDS);//结束时候再注册
            }
        };

        timer.newTimeout(task2, 10, TimeUnit.SECONDS);

        //延迟任务
        timer.newTimeout(new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                System.out.println("order3  15s 后执行一次");
            }
        }, 15, TimeUnit.SECONDS);

    }
}

从执行的结果看,order3order3延时任务只执行了一次,而order2order1为定时任务,按照不同的周期重复执行。

order1  5s 后执行 
order2  10s 后执行
order3  15s 后执行一次
order1  5s 后执行 
order2  10s 后执行

总结

为了让大家更容易理解,上边的代码写的都比较简单粗糙,几种实现方式的demo已经都提交到github 地址:https://github.com/chengxy-nds/delayqueue,感兴趣的小伙伴可以下载跑一跑。

这篇文章肝了挺长时间,写作一点也不比上班干活轻松,查证资料反复验证demo的可行性,搭建各种RabbitMQRedis环境,只想说我太难了!

可能写的有不够完善的地方,如哪里有错误或者不明了的,欢迎大家踊跃指正!!!

最后

原创不易,码字不易,来点个赞吧~

整理了几百本各类技术电子书和视频课程,送给小伙伴们。同名公号【程序员内点事】内自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!

查看原文

赞 27 收藏 15 评论 0

夜店小新新 赞了问题 · 2019-04-11

JavaScript中,作用域,作用域链,执行环境分别是什么时候创建好的,以及函数在定义和调用时发生了什么?

先抛砖引玉一下:

代码1:

var object = {
    name: "My Object",
    getName: function() {
        return console.log(this.name)//the window
    }
};
(object.getName = object.getName)();//空

代码2:

    var x=10;
    function foo(){
        console.log(x)
    }
    !function(){
        var x=20;
        console.log(x);//20!
        foo();//10!
    }();

    function foo2(){
        var x=30;
        foo();//10;
        !function(){
            console.log(x)//30
        }()
    }
    foo2();

代码3:

var Fn = {};
Fn.method = function(){
    this.name = 'jack';
    function test(){
      console.log(this === window);
    }
    test();
}
Fn.method();//true

问题来了:

1.函数在定义时发生生了什么?
2.函数在不同方式调用时,又发生了什么?
3.作用域,作用域链,执行环境都是在什么时候形成的?
4.如何解释代码2中的结果?
5.函数执行的地方和函数定义的地方有什么样的联系?
6.this指向与作用域,作用域链,执行环境的关系,以及闭包的三个是什么时候创建和有效的?
7.这方面有什么文章或者书籍的章节比较清晰明朗的?

7月1日追加问题

感谢各位大神,现在对整过过程还有点蒙,不知道我理解的对吗,见代码和注释

    var a=1;
    var fn1=function(){
        console.log(this);
    }

    function fn2(arguments){
        var a=1;
        var c=1;
        console.log(a,c);
        function fn1_1(){
            var c1=0;
            console.log(c1);
        }
    }

    function fn3(){
        console.log(this);
        fn2();
    }
    fn3();
/*
    整个解释过程是这样的:

    页面加载;

    创建window全局对象,并生成全局作用域;

    然后生成执行上下文,预解析变量(变量提升),生成全局变量对象;

    然后逐行解析,执行流依次执行每行/块代码;

    直至运行到fn3();

    执行流将fn3压入环境栈中;

    创建fn3的执行环境,创建定义域和定义域链,根据执行上文创建变量对象;

    在创建变量对象的过程中沿定义域链逐级线上搜索变量,并将结果存在函数变量对象中,其中第一活动对象为arguments;

*/

关注 12 回答 4

夜店小新新 收藏了问题 · 2019-04-11

JavaScript中,作用域,作用域链,执行环境分别是什么时候创建好的,以及函数在定义和调用时发生了什么?

先抛砖引玉一下:

代码1:

var object = {
    name: "My Object",
    getName: function() {
        return console.log(this.name)//the window
    }
};
(object.getName = object.getName)();//空

代码2:

    var x=10;
    function foo(){
        console.log(x)
    }
    !function(){
        var x=20;
        console.log(x);//20!
        foo();//10!
    }();

    function foo2(){
        var x=30;
        foo();//10;
        !function(){
            console.log(x)//30
        }()
    }
    foo2();

代码3:

var Fn = {};
Fn.method = function(){
    this.name = 'jack';
    function test(){
      console.log(this === window);
    }
    test();
}
Fn.method();//true

问题来了:

1.函数在定义时发生生了什么?
2.函数在不同方式调用时,又发生了什么?
3.作用域,作用域链,执行环境都是在什么时候形成的?
4.如何解释代码2中的结果?
5.函数执行的地方和函数定义的地方有什么样的联系?
6.this指向与作用域,作用域链,执行环境的关系,以及闭包的三个是什么时候创建和有效的?
7.这方面有什么文章或者书籍的章节比较清晰明朗的?

7月1日追加问题

感谢各位大神,现在对整过过程还有点蒙,不知道我理解的对吗,见代码和注释

    var a=1;
    var fn1=function(){
        console.log(this);
    }

    function fn2(arguments){
        var a=1;
        var c=1;
        console.log(a,c);
        function fn1_1(){
            var c1=0;
            console.log(c1);
        }
    }

    function fn3(){
        console.log(this);
        fn2();
    }
    fn3();
/*
    整个解释过程是这样的:

    页面加载;

    创建window全局对象,并生成全局作用域;

    然后生成执行上下文,预解析变量(变量提升),生成全局变量对象;

    然后逐行解析,执行流依次执行每行/块代码;

    直至运行到fn3();

    执行流将fn3压入环境栈中;

    创建fn3的执行环境,创建定义域和定义域链,根据执行上文创建变量对象;

    在创建变量对象的过程中沿定义域链逐级线上搜索变量,并将结果存在函数变量对象中,其中第一活动对象为arguments;

*/

夜店小新新 赞了问题 · 2018-11-30

解决JavaScript既然是单线程的,那么异步要怎么理解?

不能理解异步到底说的是什么

关注 27 回答 12

夜店小新新 收藏了问题 · 2018-11-30

JavaScript既然是单线程的,那么异步要怎么理解?

不能理解异步到底说的是什么

夜店小新新 关注了用户 · 2018-07-06

代码宇宙 @universe_of_code

我愿与你一同居住于代码宇宙中,用我的芯为你增加能量,和你一起守护只属于我们的世界。(六翼天使)

关注 11805

夜店小新新 收藏了问题 · 2018-07-05

关于javascript 的event loop如何理解event queue的优先级?

最近看了javascript的eventloop相关资料,对event queue队列优先级没有理解到位还请各位大神帮我指点一二。
第一篇是阮一峰老师的JavaScript 运行机制详解:再谈Event Loop;这里面有讲到eventloop循环机制,其中有一个event queue队列,里面存放着异步任务的callback;在执行栈执行完成时就会从队列中读取队列任务并执行。这里并没有涉及到优先级,只介绍了event queue是FIFO数据结构。
然后看了第二篇波同学的前端基础进阶(十二):深入核心,详解事件循环机制
波同学在里面讲到了队列分为宏任务和微任务,而且微任务会在执行栈执行完后立即执行,而宏任务要等到下一次的event loop才会被执行;

这里有一个疑问就是eventloop循环机制中到底是只有一个queue队列还是有宏队列和微队列?因为他们俩讲的不同

不管是微任务队列还是宏任务队列都是遵循FIFO,那队列中的任务被读取到执行栈时也应该是按照这个顺序来执行,那该如何理解队列中任务的优先级?比如dom事件操作,ajax请求的回调、以及setTimeout他们之间是否有优先级?

console.log('start');
var xhr = new XMLHttpRequest();
var url = 'test.json';
xhr.open('GET', url,true);
xhr.onreadystatechange = function() {
    if (xhr.status == 200 && xhr.readyState == 4) {
        console.log(xhr.responseText)
    }
}
xhr.send();
setTimeout(function() {
    console.log('timeout')
}, 0)
for (var i = 0; i < 3000000000; i++) {}

var btn = document.querySelector("#btn");
btn.onclick = function() {
    console.log('click')
}

new Promise((reslove, reject) => {
    console.log('promise');
    reslove();
}).then((res) => {
    console.log('promise finish')
})
console.log('end');

输出结果:

start
 promise
 end
 promise finish
 click
 {
    "test":"aa"
 }
 timeout

这里和我预期的有点不一样,我以为应该是ajax的回调先被执行到的,因为我故意停留了2-3秒来点击那个按钮,按理说ajax率先返回了应该被先加入到队列中,结果是click先先输出。。。这里是不是有优先级的存在?

------------------------------------------这是分割线2017-04-07更新---------------------------

经过一番折腾后,查看了很多大佬在这方面的博客文章在这里大概总结一些event loop的认识:

  1. 首先要明白的是event loop是由javascript宿主环境(像浏览器)来实现的,js引擎他不关心也不知道event
    loop机制的运行和存在,他只负责从事件队列里面读取事件来执行,他不会也不会知道怎样向事件队列中push事件任务,这些都由宿主来完成。理解这第一点很重要要先知道是谁在做这件事情。

  2. 第二点就是一个宿主环境 只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。相同任务源的任务,只能放到一个任务队列中。不同任务源的任务,可以放到不同任务队列中。然后js引擎做的事就是不断的去读取这些队列里面的任务来执行,

  3. 任务可分为宏任务和微任务;他们的执行过程和顺序上面给的链接的波同学已经讲的很清楚明朗了。我再大概复述一遍:js引擎逐句的执行script整体代码,当遇到异步任务时,js的运行环境就会在适时的时候将这些事件任务push到相应的队列中去,等待着被js引擎去执行,而如果异步没有产生回调(callback)或者说是事件任务,那他就不会push到队列里面去,当js执行栈执行完成后,然后他把微任务队列中的任务读取过,并进行执行,在这执行过程中如果有产生新的异步任务也会按照上述的方式进行处理,当微任务执行完成后他会去读取宏任务队列中的任务并执行,然后周而复始的反复执行,直到把队列中的任务全部执行完。
    macro-task(宏任务): script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering;micro-task(微任务): process.nextTick, Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver

  4. 至于队列中的优先级,有一个大概的顺序process.nextTick > promise.then > setTimeout > setImmediate;优先级执行顺序可能还会和具体的宿主环境有关;边城大神也说的对对于异步我们更不应该去依赖他们的执行顺序既然是异步就当作无序的来处理。

参考资料:
1.Promise的队列与setTimeout的队列有何关联?
2.www.w3.org
3.http://stackoverflow.com/ques...
4.https://github.com/youngwind/...

夜店小新新 关注了问题 · 2018-07-05

关于javascript 的event loop如何理解event queue的优先级?

最近看了javascript的eventloop相关资料,对event queue队列优先级没有理解到位还请各位大神帮我指点一二。
第一篇是阮一峰老师的JavaScript 运行机制详解:再谈Event Loop;这里面有讲到eventloop循环机制,其中有一个event queue队列,里面存放着异步任务的callback;在执行栈执行完成时就会从队列中读取队列任务并执行。这里并没有涉及到优先级,只介绍了event queue是FIFO数据结构。
然后看了第二篇波同学的前端基础进阶(十二):深入核心,详解事件循环机制
波同学在里面讲到了队列分为宏任务和微任务,而且微任务会在执行栈执行完后立即执行,而宏任务要等到下一次的event loop才会被执行;

这里有一个疑问就是eventloop循环机制中到底是只有一个queue队列还是有宏队列和微队列?因为他们俩讲的不同

不管是微任务队列还是宏任务队列都是遵循FIFO,那队列中的任务被读取到执行栈时也应该是按照这个顺序来执行,那该如何理解队列中任务的优先级?比如dom事件操作,ajax请求的回调、以及setTimeout他们之间是否有优先级?

console.log('start');
var xhr = new XMLHttpRequest();
var url = 'test.json';
xhr.open('GET', url,true);
xhr.onreadystatechange = function() {
    if (xhr.status == 200 && xhr.readyState == 4) {
        console.log(xhr.responseText)
    }
}
xhr.send();
setTimeout(function() {
    console.log('timeout')
}, 0)
for (var i = 0; i < 3000000000; i++) {}

var btn = document.querySelector("#btn");
btn.onclick = function() {
    console.log('click')
}

new Promise((reslove, reject) => {
    console.log('promise');
    reslove();
}).then((res) => {
    console.log('promise finish')
})
console.log('end');

输出结果:

start
 promise
 end
 promise finish
 click
 {
    "test":"aa"
 }
 timeout

这里和我预期的有点不一样,我以为应该是ajax的回调先被执行到的,因为我故意停留了2-3秒来点击那个按钮,按理说ajax率先返回了应该被先加入到队列中,结果是click先先输出。。。这里是不是有优先级的存在?

------------------------------------------这是分割线2017-04-07更新---------------------------

经过一番折腾后,查看了很多大佬在这方面的博客文章在这里大概总结一些event loop的认识:

  1. 首先要明白的是event loop是由javascript宿主环境(像浏览器)来实现的,js引擎他不关心也不知道event
    loop机制的运行和存在,他只负责从事件队列里面读取事件来执行,他不会也不会知道怎样向事件队列中push事件任务,这些都由宿主来完成。理解这第一点很重要要先知道是谁在做这件事情。

  2. 第二点就是一个宿主环境 只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。相同任务源的任务,只能放到一个任务队列中。不同任务源的任务,可以放到不同任务队列中。然后js引擎做的事就是不断的去读取这些队列里面的任务来执行,

  3. 任务可分为宏任务和微任务;他们的执行过程和顺序上面给的链接的波同学已经讲的很清楚明朗了。我再大概复述一遍:js引擎逐句的执行script整体代码,当遇到异步任务时,js的运行环境就会在适时的时候将这些事件任务push到相应的队列中去,等待着被js引擎去执行,而如果异步没有产生回调(callback)或者说是事件任务,那他就不会push到队列里面去,当js执行栈执行完成后,然后他把微任务队列中的任务读取过,并进行执行,在这执行过程中如果有产生新的异步任务也会按照上述的方式进行处理,当微任务执行完成后他会去读取宏任务队列中的任务并执行,然后周而复始的反复执行,直到把队列中的任务全部执行完。
    macro-task(宏任务): script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering;micro-task(微任务): process.nextTick, Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver

  4. 至于队列中的优先级,有一个大概的顺序process.nextTick > promise.then > setTimeout > setImmediate;优先级执行顺序可能还会和具体的宿主环境有关;边城大神也说的对对于异步我们更不应该去依赖他们的执行顺序既然是异步就当作无序的来处理。

参考资料:
1.Promise的队列与setTimeout的队列有何关联?
2.www.w3.org
3.http://stackoverflow.com/ques...
4.https://github.com/youngwind/...

关注 5 回答 2

认证与成就

  • 获得 82 次点赞
  • 获得 40 枚徽章 获得 1 枚金徽章, 获得 6 枚银徽章, 获得 33 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-07-13
个人主页被 1.7k 人浏览