Tuesday, March 15, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


css 的 filter属性竟然如此好玩

Posted: 14 Mar 2022 09:58 PM PDT

css 的 filter属性竟然如此好玩

背景

     在此之前我对css里面的filter属性不是很了解, 只知道使用这个属性来改变svg图片的颜色, 最近恰好查了很多相关文档并做了大量实验, 并有了一些启发与想法, 索性就在这里分享出来。

一、filter 滤镜

     "滤镜"这个名字很贴切了, 可以理解成为元素添加各种显示效果, 先不用记各种名词咱们直接看效果, 使用方法 & 效果图:

<style>   #lulu {      filter: grayscale(1);   } </style>  <body>   <img id="lulu" src="./img/头像.jpeg" /> </body>

image.png

     看上图里的这些效果, 比如第一排第一个, 我们会想到在某些特定的纪念日网站整体会变成灰色的样式, 应该就是用的这个属性, 第二排的第一张就可以用与某些事物被"雷击"?

二、做一个'抖动'特效

image.png
     看到这张图我第一个想法就是做个抖动的特效, 就是那种很动感的效果:

     当然配合上一旋转效果也不错:

     原理就是两个图片层叠在一起, 上面的图片进行放大与旋转动画:

<!DOCTYPE html> <html lang="en">  <head>   <meta charset="UTF-8" />   <meta http-equiv="X-UA-Compatible" content="IE=edge" />   <meta name="viewport" content="width=device-width, initial-scale=1.0" />   <title>Document</title>   <style>     .box {       position: relative;       border: 1px solid gray;       display: flex;       overflow: hidden;       width: 110px;       padding: 0px;       margin-top: 100px;       margin-left: 300px;     }      .box>img {       width: 100px;       height: 100px;       margin-left: 6px;       filter: invert(1);     }      .mk {       position: absolute;       top: 0;       left: 0;       opacity: 0.5;       animation: cc 0.5s linear infinite;     }      @keyframes cc {       from {         transform: scale(1.2);       }        to {         transform: scale(1);       }     }      .mk2 {       position: absolute;       top: 0;       left: 0;       opacity: 0.5;       animation: cc2 0.5s linear infinite alternate;       border-radius: 50%;       overflow: hidden;     }      @keyframes cc2 {       from {         transform: scale(2.2) rotate(30deg);       }        to {         transform: scale(1) rotate(0deg);       }     }   </style> </head>  <body>   <div class="box">     <img src="./img/头像.jpeg" />     <img class="mk" src="./img/头像.jpeg" />   </div>    <div class="box">     <img src="./img/头像.jpeg" />     <img class="mk2" src="./img/头像.jpeg" />   </div> </body>  </html>

三、drop-shadow 阴影

     filter属性通过设置drop-shadow为元素添加阴影, 可是早就有box-shadow属性了呀, 那这两个属性有什么区别了?

image.png

<!DOCTYPE html> <html lang="en">  <head>   <meta charset="UTF-8" />   <meta http-equiv="X-UA-Compatible" content="IE=edge" />   <meta name="viewport" content="width=device-width, initial-scale=1.0" />   <title>Document</title>   <style>     #w1 {       width: 50px;       height: 50px;       font-size: 36px;       font-weight: 900;       box-shadow: 0 0 2px red;     }      #w2 {       width: 50px;       height: 50px;       font-size: 36px;       font-weight: 900;       filter: drop-shadow(0px 0px 2px red);     }   </style> </head>  <body>   <p id="w1">九</p>   <p id="w2">九</p> </body>  </html>

     上图可知, box-shadow是针对整个dom元素进行阴影的产生, 但是drop-shadow会忽略掉"透明"的部分。

四、drop-shadow 复制 (做一个看图猜人物游戏)

     注意: 我这里使用的都是svg图片。

     既然与box-shadow都有为元素设置阴影的能力, 那么box-shadow有复制自身样的能力drop-shadow是都也有?

     所谓box-shadow的复制自身样式如图所示, box-shadow可以制作n个与元素本身形状相同或不同的样式, 下图右侧红色的方块就是左图的阴影:

image.png

     再看一下drop-shadow的表现:

image.png

     看到上面的图我第一反应就是"猜人物"小游戏, 我们把人物的轮廓也就是右图显示出来, 然后在公布答案的时候展示左侧的原图即可。

赋值gif图有bug

     赋值gif图会有bug, 效果如下:

五、drop-shadow 批量复制

     box-shadow属性是可以写多个属性值的, 我一般会利用这个属性进行一个单一样式的dom元素的复制 效果如下图:

image.png

     drop-shadow有点'狠', 他的每一次复制都是基于上次的整体效果进行的阴影投射:

image.png

     上图可以看出, 第一个复制后是出现了横排的2个, 第二次投射是产生了下方的两个, 并且每次投射都是叠加的, 下面我们看一组更夸张的:

image.png

     可想而知这种增长方式有多可怕, 稍微写几遍就可以覆盖满屏幕了。

"找不同"小游戏

     我们可以做一片阴影, 但是其中某个我们单独做一个样式进行覆盖, 考考大家的眼力, 就如图例所示:

image.png

     这里就是利用drop-shadow产生阴影, 然后再进行一点修改, 正确答案在这里:

image.png

     所以只要再写两段代码, 就可以让这个8x8 变成16x16那么多, 应该还挺好玩的。

image.png

六、drop-shadow 与 box-shadow的联合

     drop-shadowbox-shadow 都有投射的能力, 那么他两个属性共同作用于一个元素会是怎样的:
image.png

     box-shadow会基于drop-shadow属性产生的全部投影进行透射阴影, 第一排是drop-shadow的投影, 第二排是box-shadow的投影, 具体怎么玩我还没想到太适合的。

<!DOCTYPE html> <html lang="en">  <head>     <meta charset="UTF-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>Document</title>     <style>         #wrap {             position: relative;             height: 350px;             width: 500px;             margin: 50px auto;             overflow: hidden;         }          #n {             border: 1px solid gray;             width: 50px;             box-shadow: 0 200px;             transform: rotate(10deg);             filter: drop-shadow(70px 0) drop-shadow(140px 0px);         }     </style> </head>  <body>     <div id="wrap">         <img id="n" src="./svg/人.svg" /     </div> </body>  </html>

七、drop-shadow 复制后的'运动'

     既然可以投射出那么多投影, 那么如果我元素进行旋转的话, 投影是否也会进行旋转? 并且它是以什么规律运动的那?

     下面演示的是, 物体投影 + 物体本身旋转:

<!DOCTYPE html> <html lang="en">  <head>     <meta charset="UTF-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>Document</title>     <style>         #wrap {             position: relative;             height: 350px;             width: 400px;             margin: 250px auto;         }          #n {             width: 20px;             filter: drop-shadow(25px 0) drop-shadow(50px 0px) drop-shadow(100px 0);             animation: rr 2s linear infinite;         }          @keyframes rr {             0% {                 transform: rotate(0);             }              100% {                 transform: rotate(360deg);             }         }     </style> </head>  <body>     <div id="wrap">         <img id="n" src="./svg/人.svg" />     </div> </body>  </html>

     上面是整体以'元素'本身为旋转点进行旋转, 那要如何让'元素'的每个投影都以自身为原点旋转那?

     这里的思路就是, 在img外包裹一层div, 我们对外层div进行投影, 内部的img负责旋转:

<!DOCTYPE html> <html lang="en">  <head>     <meta charset="UTF-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>Document</title>     <style>         #wrap {             position: relative;             height: 350px;             width: 400px;             margin: 250px auto;         }          #box {             width: 20px;             height: 20px;             filter: drop-shadow(25px 0) drop-shadow(50px 0px) drop-shadow(100px 0);         }          #n {             width: 20px;             animation: rr 2s linear infinite;         }          @keyframes rr {             0% {                 transform: rotate(0);             }              100% {                 transform: rotate(360deg);             }         }     </style> </head>  <body>     <div id="wrap">         <div id="box">             <img id="n" src="./svg/人.svg" />         </div>     </div> </body>  </html>

八、filter属性着色(svg + png)图片

     改变svg颜色最直接的方法就是改其本身的fill属性, 这里不做探讨, 这里要研究的是到底为什么filter可以改变图片的颜色, 是什么原理? 这里我们就一起探究一下(这里只讨论纯色图片)。

轮廓的形成

     并不是所有的图片被赋予drop-shadow属性后都会呈现出物体的轮廓, 投影会忽略透明背景的地方, 所以png这种可以定义透明背景的图片才可以被投射出相应的轮廓而不是矩形轮廓, svg同理。

     比如jpg图片无法设置透明的背景, 所以其投影效果就与box-shadow相同了。

svg + png 投影变色

     我们可以利用drop-shadow制作一个指定颜色的投影, 然后只要将元素本身隐藏, 只留下投影就ok了。

image.png

image.png

image.png

<!DOCTYPE html> <html lang="en">  <head>     <meta charset="UTF-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>Document</title>     <style>         #box2 {             display: inline-block;             overflow: hidden;         }          #glasses2 {             filter: drop-shadow(200px 0 red);             transform: translateX(-200px);         }     </style> </head>  <body>     <div>         <div id="box2">             <img id="glasses2" src="./img/太阳镜.png" />         </div>     </div> </body> </html>
颜色的叠加变色有点强!

     有没有办法是直接改变元素本身的颜色? 我尝试将drop-shadow投射在自身位置, 但是投射的阴影永远在元素后面, 我尝试将元素的opacity改小, 阴影也会随之变小, 如果设置opacity:0则投影也不可见了。

     不管什么颜色无非是三原色合成的颜色, filter属性可以定义那么多种滤镜, 那是不是代表着某些滤镜效果的叠加态就是我们想要的目标颜色:

image.png

     手动生成那么多的属性不现实, 顺着这个思路我找到了一个真的这样做的网站:

为图片混合调色官网

image.png

  1. 需要多点几次Compute Filters按钮, 直到生成差异度较小的属性。
  2. 如果我们的元素不是纯黑色, 需要先赋予 filter: brightness(0) saturate(100%) 将其变为纯黑, 因为不同的底色需要变成目标颜色的filter属性不同。
  3. 当然啦这里属于头脑一波, 实际项目中不会这样去做的。

九、局部清晰

     这里所谓的局部清晰可以想象为, 某张图全部都是模糊的, 但是我们把一个放大镜放在某处, 此处就会变得清晰, 先看我做的效果:

image.png

     这里的原理是这样的, 一共两层, 下层是模糊滤镜的图片,上层是一个圆形的div, 并且这个div的背景图是图片的清晰版, 设置background-position, 在拖动div的同时, 实时变换背景的background-position位置, 就实现了图里的效果。

<!DOCTYPE html> <html lang="en">  <head>   <meta charset="UTF-8" />   <meta http-equiv="X-UA-Compatible" content="IE=edge" />   <meta name="viewport" content="width=device-width, initial-scale=1.0" />   <title>Document</title>   <style>     * {       box-sizing: border-box;     }      #wrap {       position: relative;     }      #acc {       position: absolute;       left: 0;       right: 0;       width: 540px;       filter: blur(7px);       pointer-events: none;     }      #mk {       z-index: 2;       height: 100px;       width: 100px;       border-radius: 50%;       overflow: hidden;       border: 1px solid blue;       position: absolute;       left: 0;       top: 0;       background-image: url("./img/利姆露jpeg.jpeg");       background-size: 540px 562px;       background-position: 0 0;       background-repeat: no-repeat;     }   </style> </head>  <body>   <div id="wrap">     <div id="mk"></div>     <img id="acc" src="./img/利姆露jpeg.jpeg" />   </div>   <script>     function drag(elementId) {       const element = document.getElementById(elementId);       const position = {         offsetX: 0,         offsetY: 0,         state: 0,       }       function getEvent(event) {         return event || window.event;       }       element.addEventListener(         "mousedown",         function (event) {           var e = getEvent(event);           position.offsetX = e.offsetX;           position.offsetY = e.offsetY;           position.state = 1;         },         false       );       document.addEventListener(         "mousemove",         function (event) {           var e = getEvent(event);           if (position.state) {             position.endX = e.clientX;             position.endY = e.clientY;             element.style.top = position.endY - position.offsetY + "px";             element.style.left = position.endX - position.offsetX + "px";             element.style.backgroundPositionX = "-" + element.style.left;             element.style.backgroundPositionY = "-" + element.style.top;           }         },         false       );       element.addEventListener(         "mouseup",         function (event) {           position.state = 0;         },         false       );     }     drag("mk");   </script> </body>  </html>

end

     这次就是这样, 希望与你一起进步。

5种限流算法,7种限流方式,挡住突发流量?

Posted: 14 Mar 2022 06:52 PM PDT

大家好啊,我是阿朗,最近工作中需要用到限流,这篇文章介绍常见的限流方式。

文章持续更新,可以关注公众号程序猿阿朗或访问未读代码博客
本文 Github.com/niumoo/JavaNotes 已经收录,欢迎Star。

前言

最近几年,随着微服务的流行,服务和服务之间的依赖越来越强,调用关系越来越复杂,服务和服务之间的稳定性越来越重要。在遇到突发的请求量激增,恶意的用户访问,亦或请求频率过高给下游服务带来较大压力时,我们常常需要通过缓存、限流、熔断降级、负载均衡等多种方式保证服务的稳定性。其中限流是不可或缺的一环,这篇文章介绍限流相关知识。

1. 限流

限流顾名思义,就是对请求或并发数进行限制;通过对一个时间窗口内的请求量进行限制来保障系统的正常运行。如果我们的服务资源有限、处理能力有限,就需要对调用我们服务的上游请求进行限制,以防止自身服务由于资源耗尽而停止服务。

在限流中有两个概念需要了解。

  • 阈值:在一个单位时间内允许的请求量。如 QPS 限制为10,说明 1 秒内最多接受 10 次请求。
  • 拒绝策略:超过阈值的请求的拒绝策略,常见的拒绝策略有直接拒绝、排队等待等。

2. 固定窗口算法

固定窗口算法又叫计数器算法,是一种简单方便的限流算法。主要通过一个支持原子操作的计数器来累计 1 秒内的请求次数,当 1 秒内计数达到限流阈值时触发拒绝策略。每过 1 秒,计数器重置为 0 开始重新计数。

2.1. 代码实现

下面是简单的代码实现,QPS 限制为 2,这里的代码做了一些优化,并没有单独开一个线程去每隔 1 秒重置计数器,而是在每次调用时进行时间间隔计算来确定是否先重置计数器。

/**  * @author https://www.wdbyte.com  */ public class RateLimiterSimpleWindow {     // 阈值     private static Integer QPS = 2;     // 时间窗口(毫秒)     private static long TIME_WINDOWS = 1000;     // 计数器     private static AtomicInteger REQ_COUNT = new AtomicInteger();          private static long START_TIME = System.currentTimeMillis();      public synchronized static boolean tryAcquire() {         if ((System.currentTimeMillis() - START_TIME) > TIME_WINDOWS) {             REQ_COUNT.set(0);             START_TIME = System.currentTimeMillis();         }         return REQ_COUNT.incrementAndGet() <= QPS;     }      public static void main(String[] args) throws InterruptedException {         for (int i = 0; i < 10; i++) {             Thread.sleep(250);             LocalTime now = LocalTime.now();             if (!tryAcquire()) {                 System.out.println(now + " 被限流");             } else {                 System.out.println(now + " 做点什么");             }         }     } }

运行结果:

20:53:43.038922 做点什么 20:53:43.291435 做点什么 20:53:43.543087 被限流 20:53:43.796666 做点什么 20:53:44.050855 做点什么 20:53:44.303547 被限流 20:53:44.555008 被限流 20:53:44.809083 做点什么 20:53:45.063828 做点什么 20:53:45.314433 被限流

从输出结果中可以看到大概每秒操作 3 次,由于限制 QPS 为 2,所以平均会有一次被限流。看起来可以了,不过我们思考一下就会发现这种简单的限流方式是有问题的,虽然我们限制了 QPS 为 2,但是当遇到时间窗口的临界突变时,如 1s 中的后 500 ms 和第 2s 的前 500ms 时,虽然是加起来是 1s 时间,却可以被请求 4 次。

固定窗口算法

简单修改测试代码,可以进行验证:

// 先休眠 400ms,可以更快的到达时间窗口。 Thread.sleep(400); for (int i = 0; i < 10; i++) {     Thread.sleep(250);     if (!tryAcquire()) {         System.out.println("被限流");     } else {         System.out.println("做点什么");     } }

得到输出中可以看到连续 4 次请求,间隔 250 ms 没有却被限制。:

20:51:17.395087 做点什么 20:51:17.653114 做点什么 20:51:17.903543 做点什么 20:51:18.154104 被限流 20:51:18.405497 做点什么 20:51:18.655885 做点什么 20:51:18.906177 做点什么 20:51:19.158113 被限流 20:51:19.410512 做点什么 20:51:19.661629 做点什么

3. 滑动窗口算法

我们已经知道固定窗口算法的实现方式以及它所存在的问题,而滑动窗口算法是对固定窗口算法的改进。既然固定窗口算法在遇到时间窗口的临界突变时会有问题,那么我们在遇到下一个时间窗口前也调整时间窗口不就可以了吗?

下面是滑动窗口的示意图。

滑动窗口算法

上图的示例中,每 500ms 滑动一次窗口,可以发现窗口滑动的间隔越短,时间窗口的临界突变问题发生的概率也就越小,不过只要有时间窗口的存在,还是有可能发生时间窗口的临界突变问题

3.1. 代码实现

下面是基于以上滑动窗口思路实现的简单的滑动窗口限流工具类。

package com.wdbyte.rate.limiter;  import java.time.LocalTime; import java.util.concurrent.atomic.AtomicInteger;  /**  * 滑动窗口限流工具类  *  * @author https://www.wdbyte.com  */ public class RateLimiterSlidingWindow {     /**      * 阈值      */     private int qps = 2;     /**      * 时间窗口总大小(毫秒)      */     private long windowSize = 1000;     /**      * 多少个子窗口      */     private Integer windowCount = 10;     /**      * 窗口列表      */     private WindowInfo[] windowArray = new WindowInfo[windowCount];      public RateLimiterSlidingWindow(int qps) {         this.qps = qps;         long currentTimeMillis = System.currentTimeMillis();         for (int i = 0; i < windowArray.length; i++) {             windowArray[i] = new WindowInfo(currentTimeMillis, new AtomicInteger(0));         }     }      /**      * 1. 计算当前时间窗口      * 2. 更新当前窗口计数 & 重置过期窗口计数      * 3. 当前 QPS 是否超过限制      *      * @return      */     public synchronized boolean tryAcquire() {         long currentTimeMillis = System.currentTimeMillis();         // 1. 计算当前时间窗口         int currentIndex = (int)(currentTimeMillis % windowSize / (windowSize / windowCount));         // 2.  更新当前窗口计数 & 重置过期窗口计数         int sum = 0;         for (int i = 0; i < windowArray.length; i++) {             WindowInfo windowInfo = windowArray[i];             if ((currentTimeMillis - windowInfo.getTime()) > windowSize) {                 windowInfo.getNumber().set(0);                 windowInfo.setTime(currentTimeMillis);             }             if (currentIndex == i && windowInfo.getNumber().get() < qps) {                 windowInfo.getNumber().incrementAndGet();             }             sum = sum + windowInfo.getNumber().get();         }         // 3. 当前 QPS 是否超过限制         return sum <= qps;     }      private class WindowInfo {         // 窗口开始时间         private Long time;         // 计数器         private AtomicInteger number;          public WindowInfo(long time, AtomicInteger number) {             this.time = time;             this.number = number;         }         // get...set...     } }

下面是测试用例,设置 QPS 为 2,测试次数 20 次,每次间隔 300 毫秒,预计成功次数在 12 次左右。

public static void main(String[] args) throws InterruptedException {     int qps = 2, count = 20, sleep = 300, success = count * sleep / 1000 * qps;     System.out.println(String.format("当前QPS限制为:%d,当前测试次数:%d,间隔:%dms,预计成功次数:%d", qps, count, sleep, success));     success = 0;     RateLimiterSlidingWindow myRateLimiter = new RateLimiterSlidingWindow(qps);     for (int i = 0; i < count; i++) {         Thread.sleep(sleep);         if (myRateLimiter.tryAcquire()) {             success++;             if (success % qps == 0) {                 System.out.println(LocalTime.now() + ": success, ");             } else {                 System.out.print(LocalTime.now() + ": success, ");             }         } else {             System.out.println(LocalTime.now() + ": fail");         }     }     System.out.println();     System.out.println("实际测试成功次数:" + success); }

下面是测试的结果。

当前QPS限制为:2,当前测试次数:20,间隔:300ms,预计成功次数:12 16:04:27.077782: success, 16:04:27.380715: success,  16:04:27.684244: fail 16:04:27.989579: success, 16:04:28.293347: success,  16:04:28.597658: fail 16:04:28.901688: fail 16:04:29.205262: success, 16:04:29.507117: success,  16:04:29.812188: fail 16:04:30.115316: fail 16:04:30.420596: success, 16:04:30.725897: success,  16:04:31.028599: fail 16:04:31.331047: fail 16:04:31.634127: success, 16:04:31.939411: success,  16:04:32.242380: fail 16:04:32.547626: fail 16:04:32.847965: success,  实际测试成功次数:11

4. 滑动日志算法

滑动日志算法是实现限流的另一种方法,这种方法比较简单。基本逻辑就是记录下所有的请求时间点,新请求到来时先判断最近指定时间范围内的请求数量是否超过指定阈值,由此来确定是否达到限流,这种方式没有了时间窗口突变的问题,限流比较准确,但是因为要记录下每次请求的时间点,所以占用的内存较多

4.1. 代码实现

下面是简单实现的 一个滑动日志算法,因为滑动日志要每次请求单独存储一条记录,可能占用内存过多。所以下面这个实现其实不算严谨的滑动日志,更像一个把 1 秒时间切分成 1000 个时间窗口的滑动窗口算法。

package com.wdbyte.rate.limiter;  import java.time.LocalTime; import java.util.HashSet; import java.util.Set; import java.util.TreeMap;  /**  * 滑动日志方式限流  * 设置 QPS 为 2.  *  * @author https://www.wdbyte.com  */ public class RateLimiterSildingLog {      /**      * 阈值      */     private Integer qps = 2;     /**      * 记录请求的时间戳,和数量      */     private TreeMap<Long, Long> treeMap = new TreeMap<>();      /**      * 清理请求记录间隔, 60 秒      */     private long claerTime = 60 * 1000;      public RateLimiterSildingLog(Integer qps) {         this.qps = qps;     }      public synchronized boolean tryAcquire() {         long now = System.currentTimeMillis();         // 清理过期的数据老数据,最长 60 秒清理一次         if (!treeMap.isEmpty() && (treeMap.firstKey() - now) > claerTime) {             Set<Long> keySet = new HashSet<>(treeMap.subMap(0L, now - 1000).keySet());             for (Long key : keySet) {                 treeMap.remove(key);             }         }         // 计算当前请求次数         int sum = 0;         for (Long value : treeMap.subMap(now - 1000, now).values()) {             sum += value;         }         // 超过QPS限制,直接返回 false         if (sum + 1 > qps) {             return false;         }         // 记录本次请求         if (treeMap.containsKey(now)) {             treeMap.compute(now, (k, v) -> v + 1);         } else {             treeMap.put(now, 1L);         }         return sum <= qps;     }      public static void main(String[] args) throws InterruptedException {         RateLimiterSildingLog rateLimiterSildingLog = new RateLimiterSildingLog(3);         for (int i = 0; i < 10; i++) {             Thread.sleep(250);             LocalTime now = LocalTime.now();             if (rateLimiterSildingLog.tryAcquire()) {                 System.out.println(now + " 做点什么");             } else {                 System.out.println(now + " 被限流");             }         }     } }

代码中把阈值 QPS 设定为 3,运行可以得到如下日志:

20:51:17.395087 做点什么 20:51:17.653114 做点什么 20:51:17.903543 做点什么 20:51:18.154104 被限流 20:51:18.405497 做点什么 20:51:18.655885 做点什么 20:51:18.906177 做点什么 20:51:19.158113 被限流 20:51:19.410512 做点什么 20:51:19.661629 做点什么

5. 漏桶算法

漏桶算法中的漏桶是一个形象的比喻,这里可以用生产者消费者模式进行说明,请求是一个生产者,每一个请求都如一滴水,请求到来后放到一个队列(漏桶)中,而桶底有一个孔,不断的漏出水滴,就如消费者不断的在消费队列中的内容,消费的速率(漏出的速度)等于限流阈值。即假如 QPS 为 2,则每 1s / 2= 500ms 消费一次。漏桶的桶有大小,就如队列的容量,当请求堆积超过指定容量时,会触发拒绝策略。

下面是漏桶算法的示意图。

漏桶算法

由介绍可以知道,漏桶模式中的消费处理总是能以恒定的速度进行,可以很好的保护自身系统不被突如其来的流量冲垮;但是这也是漏桶模式的缺点,假设 QPS 为 2,同时 2 个请求进来,2 个请求并不能同时进行处理响应,因为每 1s / 2= 500ms 只能处理一个请求。

6. 令牌桶算法

令牌桶算法同样是实现限流是一种常见的思路,最为常用的 Google 的 Java 开发工具包 Guava 中的限流工具类 RateLimiter 就是令牌桶的一个实现。令牌桶的实现思路类似于生产者和消费之间的关系。

系统服务作为生产者,按照指定频率向桶(容器)中添加令牌,如 QPS 为 2,每 500ms 向桶中添加一个令牌,如果桶中令牌数量达到阈值,则不再添加。

请求执行作为消费者,每个请求都需要去桶中拿取一个令牌,取到令牌则继续执行;如果桶中无令牌可取,就触发拒绝策略,可以是超时等待,也可以是直接拒绝本次请求,由此达到限流目的。

下面是令牌桶限流算法示意图。

令牌桶算法

思考令牌桶的实现可以以下特点。

  1. 1s / 阈值(QPS) = 令牌添加时间间隔。
  2. 桶的容量等于限流的阈值,令牌数量达到阈值时,不再添加。
  3. 可以适应流量突发,N 个请求到来只需要从桶中获取 N 个令牌就可以继续处理。
  4. 有启动过程,令牌桶启动时桶中无令牌,然后按照令牌添加时间间隔添加令牌,若启动时就有阈值数量的请求过来,会因为桶中没有足够的令牌而触发拒绝策略,不过如 RateLimiter 限流工具已经优化了这类问题。

6.1. 代码实现

Google 的 Java 开发工具包 Guava 中的限流工具类 RateLimiter 就是令牌桶的一个实现,日常开发中我们也不会手动实现了,这里直接使用 RateLimiter 进行测试。

引入依赖:

<exclusion>       <groupId>com.google.guava</groupId>     <artifactId>guava</artifactId>       <version>31.0.1-jre</version> </exclusion>

RateLimiter 限流体验:

// qps 2 RateLimiter rateLimiter = RateLimiter.create(2); for (int i = 0; i < 10; i++) {     String time = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);     System.out.println(time + ":" + rateLimiter.tryAcquire());     Thread.sleep(250); }

代码中限制 QPS 为 2,也就是每隔 500ms 生成一个令牌,但是程序每隔 250ms 获取一次令牌,所以两次获取中只有一次会成功。

17:19:06.797557:true 17:19:07.061419:false 17:19:07.316283:true 17:19:07.566746:false 17:19:07.817035:true 17:19:08.072483:false 17:19:08.326347:true 17:19:08.577661:false 17:19:08.830252:true 17:19:09.085327:false

6.2. 思考

虽然演示了 Google Guava 工具包中的 RateLimiter 的实现,但是我们需要思考一个问题,就是令牌的添加方式,如果按照指定间隔添加令牌,那么需要开一个线程去定时添加,如果有很多个接口很多个 RateLimiter 实例,线程数会随之增加,这显然不是一个好的办法。显然 Google 也考虑到了这个问题,在 RateLimiter 中,是在每次令牌获取时才进行计算令牌是否足够的。它通过存储的下一个令牌生成的时间,和当前获取令牌的时间差,再结合阈值,去计算令牌是否足够,同时再记录下一个令牌的生成时间以便下一次调用。

下面是 Guava 中 RateLimiter 类的子类 SmoothRateLimiter 的 resync() 方法的代码分析,可以看到其中的令牌计算逻辑。

void resync(long nowMicros) { // 当前微秒时间     // 当前时间是否大于下一个令牌生成时间     if (nowMicros > this.nextFreeTicketMicros) {            // 可生成的令牌数 newPermits = (当前时间 - 下一个令牌生成时间)/ 令牌生成时间间隔。           // 如果 QPS 为2,这里的 coolDownIntervalMicros 就是 500000.0 微秒(500ms)         double newPermits = (double)(nowMicros - this.nextFreeTicketMicros) / this.coolDownIntervalMicros();                 // 更新令牌库存 storedPermits。           this.storedPermits = Math.min(this.maxPermits, this.storedPermits + newPermits);                 // 更新下一个令牌生成时间 nextFreeTicketMicros           this.nextFreeTicketMicros = nowMicros;     } }

7. Redis 分布式限流

Redis 是一个开源的内存数据库,可以用来作为数据库、缓存、消息中间件等。Redis 是单线程的,又在内存中操作,所以速度极快,得益于 Redis 的各种特性,所以使用 Redis 实现一个限流工具是十分方便的。

下面的演示都基于Spring Boot 项目,并需要以下依赖。

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

配置 Redis 信息。

spring:   redis:     database: 0     password:      port: 6379     host: 127.0.0.1     lettuce:       shutdown-timeout: 100ms       pool:         min-idle: 5         max-idle: 10         max-active: 8         max-wait: 1ms

7.1. 固定窗口限流

Redis 中的固定窗口限流是使用 incr 命令实现的,incr 命令通常用来自增计数;如果我们使用时间戳信息作为 key,自然就可以统计每秒的请求量了,以此达到限流目的。

这里有两点要注意。

  1. 对于不存在的 key,第一次新增时,value 始终为 1。
  2. INCR 和 EXPIRE 命令操作应该在一个原子操作中提交,以保证每个 key 都正确设置了过期时间,不然会有 key 值无法自动删除而导致的内存溢出。

由于 Redis 中实现事务的复杂性,所以这里直接只用 lua 脚本来实现原子操作。下面是 lua 脚本内容。

local count = redis.call("incr",KEYS[1]) if count == 1 then   redis.call('expire',KEYS[1],ARGV[2]) end if count > tonumber(ARGV[1]) then   return 0 end return 1

下面是使用 Spring Boot 中 RedisTemplate 来实现的 lua 脚本调用测试代码。

/**  * @author https://www.wdbyte.com  */ @SpringBootTest class RedisLuaLimiterByIncr {     private static String KEY_PREFIX = "limiter_";     private static String QPS = "4";     private static String EXPIRE_TIME = "1";      @Autowired     private StringRedisTemplate stringRedisTemplate;      @Test     public void redisLuaLimiterTests() throws InterruptedException, IOException {         for (int i = 0; i < 15; i++) {             Thread.sleep(200);             System.out.println(LocalTime.now() + " " + acquire("user1"));         }     }      /**      * 计数器限流      *      * @param key      * @return      */     public boolean acquire(String key) {         // 当前秒数作为 key         key = KEY_PREFIX + key + System.currentTimeMillis() / 1000;         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();         redisScript.setResultType(Long.class);         //lua文件存放在resources目录下         redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limiter.lua")));         return stringRedisTemplate.execute(redisScript, Arrays.asList(key), QPS, EXPIRE_TIME) == 1;     } }

代码中虽然限制了 QPS 为 4,但是因为这种限流实现是把毫秒时间戳作为 key 的,所以会有临界窗口突变的问题,下面是运行结果,可以看到因为时间窗口的变化,导致了 QPS 超过了限制值 4。

17:38:23.122044 true 17:38:23.695124 true 17:38:23.903220 true # 此处有时间窗口变化,所以下面继续 true 17:38:24.106206 true 17:38:24.313458 true 17:38:24.519431 true 17:38:24.724446 true 17:38:24.932387 false 17:38:25.137912 true 17:38:25.355595 true 17:38:25.558219 true 17:38:25.765801 true 17:38:25.969426 false 17:38:26.176220 true 17:38:26.381918 true

7.3. 滑动窗口限流

通过对上面的基于 incr 命令实现的 Redis 限流方式的测试,我们已经发现了固定窗口限流所带来的问题,在这篇文章的第三部分已经介绍了滑动窗口限流的优势,它可以大幅度降低因为窗口临界突变带来的问题,那么如何使用 Redis 来实现滑动窗口限流呢?

这里主要使用 ZSET 有序集合来实现滑动窗口限流,ZSET 集合有下面几个特点:

  1. ZSET 集合中的 key 值可以自动排序。
  2. ZSET 集合中的 value 不能有重复值。
  3. ZSET 集合可以方便的使用 ZCARD 命令获取元素个数。
  4. ZSET 集合可以方便的使用 ZREMRANGEBYLEX 命令移除指定范围的 key 值。

基于上面的四点特性,可以编写出基于 ZSET 的滑动窗口限流 lua 脚本。

--KEYS[1]: 限流 key --ARGV[1]: 时间戳 - 时间窗口 --ARGV[2]: 当前时间戳(作为score) --ARGV[3]: 阈值 --ARGV[4]: score 对应的唯一value -- 1. 移除时间窗口之前的数据 redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1]) -- 2. 统计当前元素数量 local res = redis.call('zcard', KEYS[1]) -- 3. 是否超过阈值 if (res == nil) or (res < tonumber(ARGV[3])) then     redis.call('zadd', KEYS[1], ARGV[2], ARGV[4])     return 1 else     return 0 end

下面是使用 Spring Boot 中 RedisTemplate 来实现的 lua 脚本调用测试代码。

@SpringBootTest class RedisLuaLimiterByZset {      private String KEY_PREFIX = "limiter_";     private String QPS = "4";      @Autowired     private StringRedisTemplate stringRedisTemplate;      @Test     public void redisLuaLimiterTests() throws InterruptedException, IOException {         for (int i = 0; i < 15; i++) {             Thread.sleep(200);             System.out.println(LocalTime.now() + " " + acquire("user1"));         }     }      /**      * 计数器限流      *      * @param key      * @return      */     public boolean acquire(String key) {         long now = System.currentTimeMillis();         key = KEY_PREFIX + key;         String oldest = String.valueOf(now - 1_000);         String score = String.valueOf(now);         String scoreValue = score;         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();         redisScript.setResultType(Long.class);         //lua文件存放在resources目录下         redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limiter2.lua")));         return stringRedisTemplate.execute(redisScript, Arrays.asList(key), oldest, score, QPS, scoreValue) == 1;     } }

代码中限制 QPS 为 4,运行结果信息与之一致。

17:36:37.150370 true 17:36:37.716341 true 17:36:37.922577 true 17:36:38.127497 true 17:36:38.335879 true 17:36:38.539225 false 17:36:38.745903 true 17:36:38.952491 true 17:36:39.159497 true 17:36:39.365239 true 17:36:39.570572 false 17:36:39.776635 true 17:36:39.982022 true 17:36:40.185614 true 17:36:40.389469 true

这里介绍了 Redis 实现限流的两种方式,当然使用 Redis 也可以实现漏桶和令牌桶两种限流算法,这里就不做演示了,感兴趣的可以自己研究下。

8. 总结

这篇文章介绍实现限流的几种方式,主要是窗口算法和桶算法,两者各有优势。

  • 窗口算法实现简单,逻辑清晰,可以很直观的得到当前的 QPS 情况,但是会有时间窗口的临界突变问题,而且不像桶一样有队列可以缓冲。
  • 桶算法虽然稍微复杂,不好统计 QPS 情况,但是桶算法也有优势所在。

    • 漏桶模式消费速率恒定,可以很好的保护自身系统,可以对流量进行整形,但是面对突发流量不能快速响应。
    • 令牌桶模式可以面对突发流量,但是启动时会有缓慢加速的过程,不过常见的开源工具中已经对此优化。

单机限流与分布式限流

上面演示的基于代码形式的窗口算法和桶算法限流都适用于单机限流,如果需要分布式限流可以结合注册中心、负载均衡计算每个服务的限流阈值,但这样会降低一定精度,如果对精度要求不是太高,可以使用。

而 Redis 的限流,由于 Redis 的单机性,本身就可以用于分布式限流。使用 Redis 可以实现各种可以用于限流算法,如果觉得麻烦也可以使用开源工具如 redisson,已经封装了基于 Redis 的限流。

其他限流工具

文中已经提到了 Guava 的限流工具包,不过它毕竟是单机的,开源社区中也有很多分布式限流工具,如阿里开源的 Sentinel 就是不错的工具,Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

一如既往,文章中的代码存放在:github.com/niumoo/JavaNotes

参考

Redis INCR:https://redis.io/commands/incr

Rate Limiting Wikipedia:https://en.wikipedia.org/wiki/Rate_limiting

SpringBoot Redis:https://www.cnblogs.com/lenve/p/10965667.html

订阅

可以微信搜一搜程序猿阿朗或访问未读代码博客阅读。
本文 Github.com/niumoo/JavaNotes 已经收录,欢迎Star。

petite-vue源码剖析-双向绑定`v-model`的工作原理

Posted: 14 Mar 2022 12:03 AM PDT

前言

双向绑定v-model不仅仅是对可编辑HTML元素(select, input, textarea和附带[contenteditable=true])同时附加v-bindv-on,而且还能利用通过petite-vue附加给元素的_value_trueValue_falseValue属性提供存储非字符串值的能力。

深入v-model工作原理

export const model: Directive<   HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement > = ({ el, exp, get, effect, modifers }) => {   const type = el.type   // 通过`with`对作用域的变量/属性赋值   const assign = get(`val => { ${exp} = val }`)   // 若type为number则默认将值转换为数字   const { trim, number = type ==== 'number'} = modifiers || {}    if (el.tagName === 'select') {     const sel = el as HTMLSelectElement     // 监听控件值变化,更新状态值     listen(el, 'change', () => {       const selectedVal = Array.prototype.filter         .call(sel.options, (o: HTMLOptionElement) => o.selected)         .map((o: HTMLOptionElement) => number ? toNumber(getValue(o)) : getValue(o))       assign(sel.multiple ? selectedVal : selectedVal[0])     })      // 监听状态值变化,更新控件值     effect(() => {       value = get()       const isMultiple = sel.muliple       for (let i = 0, l = sel.options.length; i < i; i++) {         const option = sel.options[i]         const optionValue = getValue(option)         if (isMulitple) {           // 当为多选下拉框时,入参要么是数组,要么是Map           if (isArray(value)) {             option.selected = looseIndexOf(value, optionValue) > -1           }           else {             option.selected = value.has(optionValue)           }         }         else {           if (looseEqual(optionValue, value)) {             if (sel.selectedIndex !== i) sel.selectedIndex = i             return           }         }       }     })   }   else if (type === 'checkbox') {     // 监听控件值变化,更新状态值     listen(el, 'change', () => {       const modelValue = get()       const checked = (el as HTMLInputElement).checked       if (isArray(modelValue)) {         const elementValue = getValue(el)         const index = looseIndexOf(modelValue, elementValue)         const found = index !== -1         if (checked && !found) {           // 勾选且之前没有被勾选过的则加入到数组中           assign(modelValue.concat(elementValue))         }         else if (!checked && found) {           // 没有勾选且之前已勾选的排除后在重新赋值给数组           const filered = [...modelValue]           filteed.splice(index, 1)           assign(filtered)         }         // 其它情况就啥都不干咯       }       else {         assign(getCheckboxValue(el as HTMLInputElement, checked))       }     })      // 监听状态值变化,更新控件值     let oldValue: any     effect(() => {       const value = get()       if (isArray(value)) {         ;(el as HTMLInputElement).checked =            looseIndexOf(value, getValue(el)) > -1       }       else if (value !== oldValue) {         ;(el as HTMLInputElement).checked = looseEqual(           value,           getCheckboxValue(el as HTMLInputElement, true)         )       }       oldValue = value     })   }   else if (type === 'radio') {     // 监听控件值变化,更新状态值     listen(el, 'change', () => {       assign(getValue(el))     })      // 监听状态值变化,更新控件值     let oldValue: any     effect(() => {       const value = get()       if (value !== oldValue) {         ;(el as HTMLInputElement).checked = looseEqual(value, getValue(el))       }     })   }   else {     // input[type=text], textarea, div[contenteditable=true]     const resolveValue = (value: string) => {       if (trim) return val.trim()       if (number) return toNumber(val)       return val     }      // 监听是否在输入法编辑器(input method editor)输入内容     listen(el, 'compositionstart', onCompositionStart)     listen(el, 'compositionend', onCompositionEnd)     // change事件是元素失焦后前后值不同时触发,而input事件是输入过程中每次修改值都会触发     listen(el, modifiers?.lazy ? 'change' : 'input', () => {       // 元素的composing属性用于标记是否处于输入法编辑器输入内容的状态,如果是则不执行change或input事件的逻辑       if ((el as any).composing) return       assign(resolveValue(el.value))     })     if (trim) {       // 若modifiers.trim,那么当元素失焦时马上移除值前后的空格字符       listen(el, 'change', () => {         el.value = el.value.trim()       })     }      effect(() => {       if ((el as any).composing) {         return       }       const curVal = el.value       const newVal = get()       // 若当前元素处于活动状态(即得到焦点),并且元素当前值进行类型转换后值与新值相同,则不用赋值;       // 否则只要元素当前值和新值类型或值不相同,都会重新赋值。那么若新值为数组[1,2,3],赋值后元素的值将变成[object Array]       if (document.activeElement === el && resolveValue(curVal) === newVal) {         return       }       if (curVal !== newVal) {         el.value = newVal       }     })   } }  // v-bind中使用_value属性保存任意类型的值,在v-modal中读取 const getValue = (el: any) => ('_value' in el ? el._value : el.value)  const getCheckboxValue = (   el: HTMLInputElement & {_trueValue?: any, _falseValue?: any}, // 通过v-bind定义的任意类型值   checked: boolean // checkbox的默认值是true和false ) => {   const key = checked ? '_trueValue' : '_falseValue'   return key in el ? el[key] : checked }  const onCompositionStart = (e: Event) => {   // 通过自定义元素的composing元素,用于标记是否在输入法编辑器中输入内容   ;(e.target as any).composing = true }    const onCompositionEnd = (e: Event) => {   const target = e.target as any   if (target.composing) {     // 手动触发input事件     target.composing = false     trigger(target, 'input')   } }  const trigger = (el: HTMLElement, type: string) => {   const e = document.createEvent('HTMLEvents')   e.initEvent(type, true, true)   el.dispatchEvent(e) }

compositionstartcompositionend是什么?

compositionstart是开始在输入法编辑器上输入字符触发,而compositionend则是在输入法编辑器上输入字符结束时触发,另外还有一个compositionupdate是在输入法编辑器上输入字符过程中触发。

当我们在输入法编辑器敲击键盘时会按顺序执行如下事件:
compositionstart -> (compositionupdate -> input)+ -> compositionend -> 当失焦时触发change
当在输入法编辑器上输入ri后按空格确认字符,则触发如下事件
compositionstart(data="") -> compositionupdate(data="r") -> input -> compositionupdate(data="ri") -> input -> compositionupdate(data="日") -> input -> compositionend(data="日")

由于在输入法编辑器上输入字符时会触发input事件,所以petite-vue中通过在对象上设置composing标识是否执行input逻辑。

事件对象属性如下:

readonly target: EventTarget // 指向触发事件的HTML元素 readolny type: DOMString // 事件名称,即compositionstart或compositionend readonly bubbles: boolean // 事件是否冒泡 readonly cancelable: boolean // 事件是否可取消 readonly view: WindowProxy // 当前文档对象所属的window对象(`document.defaultView`) readonly detail: long readonly data: DOMString // 最终填写到元素的内容,compositionstart为空,compositionend事件中能获取如"你好"的内容 readonly locale: DOMString

编码方式触发事件

DOM Level2的事件中包含HTMLEvents, MouseEvents、MutationEvents和UIEvents,而DOM Level3则增加如CustomEvent等事件类型。

enum EventType {   // DOM Level 2 Events   UIEvents,   MouseEvents, // event.initMouseEvent   MutationEvents, // event.initMutationEvent   HTMLEvents, // event.initEvent   // DOM Level 3 Events   UIEvent,   MouseEvent, // event.initMouseEvent   MutationEvent, // event.initMutationEvent   TextEvent, // TextEvents is also supported, event.initTextEvent   KeyboardEvent, // KeyEvents is also supported, use `new KeyboardEvent()` to create keyboard event   CustomEvent, // event.initCustomEvent   Event, // Basic events module, event.initEvent }
  • HTMLEvents包含abort, blur, change, error, focus, load, reset, resize, scroll, select, submit, unload, input
  • UIEvents包含DOMActive, DOMFocusIn, DOMFocusOut, keydown, keypress, keyup
  • MouseEvents包含click, mousedown, mousemove, mouseout, mouseover, mouseup
  • MutationEvents包含DOMAttrModified,DOMNodeInserted,DOMNodeRemoved,DOMCharacterDataModified,DOMNodeInsertedIntoDocument,DOMNodeRemovedFromDocument,DOMSubtreeModified

创建和初始化事件对象

MouseEvent

方法1

const e: Event = document.createEvent('MouseEvent') e.initMouseEvent(   type: string,   bubbles: boolean,   cancelable: boolean,   view: AbstractView, // 指向与事件相关的视图,一般为document.defaultView   detail: number, // 供事件回调函数使用,一般为0   screenX: number, // 相对于屏幕的x坐标   screenY: number, // 相对于屏幕的Y坐标   clientX: number, // 相对于视口的x坐标   clientY: number, // 相对于视口的Y坐标   ctrlKey: boolean, // 是否按下Ctrl键   altKey: boolean, // 是否按下Ctrl键   shiftKey: boolean, // 是否按下Ctrl键   metaKey: boolean, // 是否按下Ctrl键   button: number, // 按下按个鼠标键,默认为0.0左,1中,2右   relatedTarget: HTMLElement // 指向于事件相关的元素,一般只有在模拟mouseover和mouseout时使用 )

方法2

const e: Event = new MouseEvent('click', {   bubbles: false,   // ...... })

KeyboardEvent

const e = new KeyboardEvent(   typeArg: string, // 如keypress   {     ctrlKey: true,     // ......   } )

https://developer.mozilla.org...

Event的初始方法

/**  * 选项的属性  * @param {string} name - 事件名称, 如click,input等  * @param {boolean} [cancelable=false] - 指定事件是否可冒泡  * @param {boolean} [cancelable=false] - 指定事件是否可被取消  * @param {boolean} [composed=false] - 指定事件是否会在Shadow DOM根节点外触发事件回调函数  */ const e = new Event('input', {   name: string,    bubbles: boolean = false,    cancelable: boolean = false,    composed: boolean = false })

CustomEvent

方法1

const e: Event = document.createEvent('CustomEvent') e.initMouseEvent(   type: string,   bubbles: boolean,   cancelable: boolean,   detail: any )

方法2

/**  * 选项的属性  * @param {string} name - 事件名称, 如click,input等,可随意定义  * @param {boolean} [cancelable=false] - 指定事件是否可冒泡  * @param {boolean} [cancelable=false] - 指定事件是否可被取消  * @param {any} [detail=null] - 事件初始化时传递的数据  */ const e = new CustomEvent('hi', {   name: string,    bubbles: boolean = false,    cancelable: boolean = false,    detail: any = null })

HTMLEvents

const e: Event = document.createEvent('HTMLEvents') e.initMouseEvent(   type: string,   bubbles: boolean,   cancelable: boolean )

添加监听和发布事件

element.addEventListener(type: string) element.dispatchEvent(e: Event)

针对petite-vue进行分析

const onCompositionEnd = (e: Event) => {   const target = e.target as any   if (target.composing) {     // 手动触发input事件     target.composing = false     trigger(target, 'input')   } } const trigger = (el: HTMLElement, type: string) => {   const e = document.createEvent('HTMLEvents')   e.initEvent(type, true, true)   el.dispatchEvent(e) }

当在输入法编辑器操作完毕后会手动触发input事件,但当事件绑定修饰符设置为lazy后并没有绑定input事件回调函数,此时在输入法编辑器操作完毕后并不会自动更新状态,我们又有机会可以贡献代码了:)

// change事件是元素失焦后前后值不同时触发,而input事件是输入过程中每次修改值都会触发     listen(el, modifiers?.lazy ? 'change' : 'input', () => {       // 元素的composing属性用于标记是否处于输入法编辑器输入内容的状态,如果是则不执行change或input事件的逻辑       if ((el as any).composing) return       assign(resolveValue(el.value))     })

外番:IE的事件模拟

var e = document.createEventObject() e.shiftKey = false e.button = 0 document.getElementById('click').fireEvent('onclick', e)

总结

整合LayUI等DOM-based框架时免不了使用this.$ref获取元素实例,下一篇《petite-vue源码剖析-ref的工作原理》我们一起来探索吧!
尊重原创,转载请注明来自:https://www.cnblogs.com/fsjoh... 肥仔John

Gitlab CI/CD教程及npm包构建发布实战

Posted: 12 Mar 2022 02:03 AM PST

!!!实践过程中请留意文档版本号跟你实际使用Gitlab的版本号

👉🏻中文文档-14.8-pre

👉🏻Gitlab版本—12.9

前置知识

  1. yaml语法,教程👉🏻YAML 入门教程
  2. Docker相关知识,教程👉🏻Docker教程
  3. linux命令,教程👉🏻Linux 命令大全

自定义配置目录

默认配置文件目录是在mono repo的根目录,文件名为.gitlab-ci.yml

若需要自定义设置CI脚本文件的路径,如下:

Xnip2022-03-12_09-17-36

流水线配置

.gitlab-ci.yml文件中对流水线配置大致可以分为2个环节:

  1. 全局配置,运行在单个stage之前或者之后。
  2. 单个stage配置

结构大致可以如下:

# 指定脚本执行的镜像环境,如下为node环境为14.17.1 image: node:14.17.1  # 单个job执行之前执行 before_script:   - echo '====== 准备构建中 ========='  # 配置单个stage的执行顺序,串行 stages:   - install   - build  # 单个stage配置 # 安装依赖 npm_install:   only:     - master   stage: install   script:     - yarn     - ls -al  # 单个stage配置 # 构建 webpack_build:   only:     - master   stage: build   script:     - yarn build  # 单个job全部执行完之后执行 after_script:   - echo "====== 构建结束 =========" 

重要概念

Pipeline

流水线,一次流水线相当于一次构建任务,里面可以包含多个阶段,比如install -> eslint -> build -> deploy等流程 ;

stages

表示构建阶段,每个stage串行同步执行。一旦有一个stage中的一个job失败了,那么下一个stage的任务便不会执行。如果当前stage定义了多个任务,那么其中一个任务失败,另外一个任务还是会被继续执行。但是只有当所有 stages 成功完成后,该构建任务 (Pipeline) 才算成功。

jobs

job表示某个stage里面执行的工作 ,一个stage里面可以定义多个job 。

jobs有如下特点 :

  • 相同 stage 中的jobs 会并行执行
  • 相同 stage 中的 jobs 都执行成功时,该 stage 才会成功
  • 如果任何一个job 失败,那么该 stage 失败,即该构建任务 (Pipeline) 失败

gitlab runner

执行构建任务的一个服务,里面包含了持续集成的的环境,一般由docker创建。它可以在不同的主机上部署,也可以在同一个主机上设置多个gitlab-runner ,还可以根据不同的环境设置不同的环境,比如我们需要区分研发环境,测试环境以及正式环境等。

关键字

image

CI/CD脚本运行环境的docker镜像,镜像就是一种文件存储形式,可以理解为是一个环境的集合,内含多种文件。如指定node环境镜像:

# 最新版本node环境 image: node:@latest

tags

指定gitlab 在执行脚本时使用哪个runner。

before_script

在单个stage执行之前执行的脚本内容,内容以数组形式配置,如上例子中:

before_script:   - echo '====== 准备构建中 ========='

stages

CI允许我们进行自定义的流水线阶段配置,可以将一个流水线拆分为多个阶段(stage),stages会串行执行。

stages:   - install   - build

script

执行脚本,脚本内容以数组形式配置。如上例子中stage为npm_install阶段执行的脚本为:

script:     - yarn     - ls -al

先执行yarn命令安装依赖,结束后查看了当前目录下文件及目录的具体信息,是个串行执行的过程。

cache

缓存多个流水线任务之间共用的文件和目录,缓存相关概念下文详情讲述。

only & except

设置流水线任务执行时机:使用 only 来定义job何时运行,使用 except 定义job 运行的时间。

  • 指定分支触发执行时机

    job:    only:     - branches@gitlab-org/gitlab   except:     - main@gitlab-org/gitlab     - /^release/.*$/@gitlab-org/gitlab

    此示例为 gitlab-org/gitlab 上的所有分支运行 job,除了 main 和以 release/ 开头的分支。

  • 在合并请求时触发

    job1:   script:     - echo "This job runs in merge request pipelines"   only:     - merge_requests
  • 在push的时候触发

    job1:   script:     - echo "branch push"   only:     - pushes
  • 手动触发

    在Gitlab Runner/pipeline里面点击run pipeline时触发

    job1:   only:     - web
  • 根据git提交消息或者判断分支触发执行时机

    build:   script:        - yarn build   except:     variables:       - $CI_COMMIT_MESSAGE =~ /test/ || $CI_COMMIT_BRANCH == "main"

    git commit 消息为"test"的push跟提交分支为"main"的push不触发此job。

  • 根据文件修改判断执行时机

    build:   script: yarn build   except:     changes:       - "*.md"

    只要有md文件修改就不执行此job。

retry

job重试次数,默认为0,最大重试次数为2,其中when可设置在特定失败原因的情况下执行。

rules:if

此字段可以在单个流水线job或者workflow字段下进行配置。

rules关键词下可以进行if语句配置,如果if满足的话可执行某些自定义配置。

rules:   - if: $CI_COMMIT_REF_NAME =~ /feature/

注意: only & exceptrules:if都是用来决定单个job执行时机的,在配置时只能存在一个,否则会报错。

workflow

rules配合用来控制流水线的执行动作,在最外层进行配置,workflow: rules 接受这些关键字:

  • if:检查此规则以确定何时运行流水线。
  • when:指定当if为 true 时要做什么。

    • 要运行流水线,请设置为 always
    • 要阻止流水线运行,请设置为 never
  • variables:如果未定义,则使用在别处定义的变量。适用版本13.11 ~14.0

当没有规则为 true 时,流水线不会运行。

以下示例中,前两天规则都匹配到不执行时机,当else时,流水线执行。

workflow:   rules:     - if: '$CI_PIPELINE_SOURCE == "schedule"'       when: never     - if: '$CI_PIPELINE_SOURCE == "push"'       when: never     - when: always

when

控制上一个stage成功或者失败时,当前stage的行为。

  • on_success(默认值): 上一个stage成功了才会执行当阶段任务,或者之前失败的任务配置了allow_failure: true
  • on_failure :只有上一个阶段任务失败了才会执行当前任务。
  • always:无论上一个阶段的jobs状态如何,都会触发当前阶段的任务。
  • never:不运行当前任务。
  • manual:在gitlab网页中手动点击触发。

模块化

使用关键字include引入其他yml文件中的配置。

include:   - '/yml/job1_install.yml'   - '/yml/job2_lint.yml'   - '/yml/job3_build.yml'   - '/yml/job4_deploy.yml'

缓存

重要概念

在 GitLab CI/CD 中,我们所使用的 runner 是以 docker 的形式运行不同的任务。普通的 cache 机制(即不指定URL,No URL provided, cache will not be downloaded from shared cache server. Instead a local version of cache will be extracted. ),其 cache 是存储在本地,所以如果两个 job 实际运行的位置是不在宿主机上的,其相互之间的缓存是无法共享的。

分布式缓存

分布式缓存需要runner配置支持,开启后需要在cache中配置s3ServerAddress、s3BucketName等信息进行缓存跨runner共享。

缓存路径

在配置cache时,pathsfiles的文件/目录都是以项目的根目录为相对位置的,在store cache的时候也是以项目名区分缓存路径的,应用缓存的时候会在项目下配合key值去应用缓存,即使是分布式缓存也是按照这个策略。

image-20220311091210653

缓存文件信息上会有最后更新时间(重要信息)、文件权限、cache中设置的key等,其中更新时间跟缓存策略有联系,如果恶意篡改服务器时间,可能会出现依赖前后不一致导致打出来的包不符合预期的情况。

缓存绑定文件

缓存绑定到当前版本的文件。当这些文件之一发生变化时,将计算一个新的缓存键并创建一个新的缓存,如下:

cache-job:   script:     - echo "This job uses a cache."   cache:     key:       files:         - Gemfile.lock         - package.json     paths:       - vendor/ruby       - node_modules

此时的 key 是根据最近更改了每个列出的文件的提交计算得出的 SHA。如果在任何提交中都没有更改任何文件,则key就是默认值 default

多文件缓存

cache可以配置多个key,适用于13.10 ~ 13.12版本,其他版本可以在key下的filespaths配置多个路径/文件来实现。

禁用缓存

使用cache: {}来禁用缓存。

继承缓存

缓存配置可复用的情况下使用继承写法,可以在当前job下覆盖(重写)某个策略或者设置优先级

cache: &global_cache   key: $CI_COMMIT_REF_SLUG   paths:     - node_modules/     - public/     - vendor/   policy: pull-push  job:   cache:     # 继承全局缓存     <<: *global_cache     # 重写缓存策略     policy: pull

回退缓存键

13.4版本以上可应用回退缓存键,功能类似缓存备份。你可以使用 CACHE_FALLBACK_KEY 变量来指定一个备份缓存key,他会在你指定key的缓存不存在时去查找应用备份缓存。

variables:   CACHE_FALLBACK_KEY: fallback-key  job1:   script:     - echo   cache:     key: "$CI_COMMIT_REF_SLUG"     paths:       - binaries/

手动清除缓存

您可以在 GitLab UI 中清除缓存:

  1. 在顶部栏上,选择 菜单 > 项目 并找到您的项目。
  2. 在左侧边栏上,选择 CI/CD > 流水线 页面。
  3. 在右上角,选择 清除 Runner 缓存

在下一次提交时,您的 CI/CD 作业使用新的缓存。

实战:构建发布组件库到npm仓库

编写.gitlab-ci.yml基本流程,如下:

image: node:14.17.1  before_script:   - echo '====== 准备构建中 ========='  stages:   - install   - lint   - build   - deploy  ### 配置缓存 cache:   key:     files:       - package.json       - packages/ghost-weapp-ui/package.json   paths:     - node_modules/     - packages/ghost-weapp-ui/node_modules/     ### 直接缓存.npm,.npm中缓存了所有依赖,因为gitlab缓存是分项目的,所以两种方法个人觉得没有什么区别     # - .npm/  # eslint检测 job_lint:   only:     - master   stage: lint   before_script:     - echo 'eslint检测'     - ls -a   script:     - cd packages/ghost-weapp-ui     - ls -a     - yarn lint     - echo 'eslint检测完成'   retry: 0   when: 'on_success'  # 安装依赖 job_install:   only:     - master   stage: install   before_script:     - echo '安装依赖'   script:     - yarn config set registry https://registry.npm.taobao.org/     - yarn install     - cd packages/ghost-weapp-ui     - ls -a     - yarn install     - ls -a     - echo '依赖安装完成'   retry: 0  # 打包编译 job_build:   only:     - master   stage: build   before_script:     - echo '开始打包'   script:     - cd packages/ghost-weapp-ui     - ls -a     - yarn     - ls -a     - yarn build     - echo '构建完成'   when: 'on_success'   retry: 0  # 发布 job_deploy:   only:     - master   stage: deploy   before_script:     - echo '更新补丁版本,准备发布'   script:     - cd packages/ghost-weapp-ui     - node deploy.js ${CI_COMMIT_REF_NAME}   when: 'on_success'   retry: 0  after_script:   - echo "====== 发布完成 =========" 

编写部署脚本,其中主要是模拟npm login流程替换为使用token进行登录并且执行npm publish,发布到npm的流程。

const fs = require('fs') const path = require('path') const os = require('os') const { exec } = require('child_process')  // 替换为自己npm账号的authToken // token获取方法:vim ~/.npmrc  const npmrcText = `registry=https://registry.npmjs.org/ home=https://www.npmjs.org //registry.npmjs.org/:_authToken=${authToken} `  // 获取命令中第三个参数,此例子中为'master' // node deploy.js ${CI_COMMIT_REF_NAME}, 分支名为master const env = process.argv[2]  // 拼接命令,执行npm publish,打tag function deploy() {     fs.writeFileSync(path.resolve(os.homedir(), '.npmrc'), npmrcText)     const argsArray = ['publish'].concat(['--tag', env === 'master' ? 'latest' : 'beta'])     execa('npm', argsArray); }  async function execa(a, arry = []) {     return new Promise((resolve, reject) => {         exec(`${a} ${arry.join(' ')}`, (err, stdout, stderr) => {             if (err) {                 console.error(err);                 reject(err);             }             resolve(stdout)         })     }) }  deploy();

执行结果:

Xnip2022-03-12_17-53-36

收到npm发布成功反馈邮件:

image-20220312175447181

以上就是gitlab CI/CD的相关知识点以及实战发布npm包的示例,感谢阅读!

走进开源项目 - urlcat 源码分析

Posted: 13 Mar 2022 09:31 AM PDT

《走进开源项目 - urlcat》中,对项目整体进行了分析,对如何做开源也有了进一步的了解,该篇再深入研究下 urlcat 源码。

该项目到底做了什么?

// 常规写法一 const API_URL = 'https://api.example.com/';  function getUserPosts(id, blogId, limit, offset) {   const requestUrl = `${API_URL}/users/${id}/blogs/${blogId}/posts?limit=${limit}&offset=${offset}`;   // send HTTP request }  // 常规写法二 const API_URL = 'https://api.example.com/';  function getUserPosts(id, blogId, limit, offset) {   const escapedId = encodeURIComponent(id);   const escapedBlogId = encodeURIComponent(blogId);   const path = `/users/${escapedId}/blogs/${escapedBlogId}`;   const url = new URL(path, API_URL);   url.search = new URLSearchParams({ limit, offset });   const requestUrl = url.href;   // send HTTP request }  // 使用 urlcat 之后的写法 const API_URL = 'https://api.example.com/';  function getUserPosts(id, limit, offset) {   const requestUrl = urlcat(API_URL, '/users/:id/posts', { id, limit, offset });   // send HTTP request }

源码共 267 行,其中注释占了近 110,代码只有 157 行。注释跟代码接近 1:1 ,接下来我们逐段分析。

第一段

import qs, { IStringifyOptions } from 'qs';  // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ParamMap = Record<string, any>; export type UrlCatConfiguration =   Partial<Pick<IStringifyOptions, 'arrayFormat'> & { objectFormat: Partial<Pick<IStringifyOptions, 'format'>> }>

该项目是在 qs 项目的基础上并使用 typescript 进行开发,其中定义了 2 个类型,有几个不太了解知识点 typeRecodePartialPick

interface 与 type 的区别

  • 相同点:都可以描述对象或者函数,且可以使用 extends 进行拓展
  • 不同点:

    • type 可以声明基本类型别名,联合类型,和元组等类型,但 interface 不行

      // 基本类型别名 type Name = string | number;  // 联合类型 interface Common {     name: string; } interface Person<T> extends Common {   age: T;   sex: string; }  type People<T> = {   age: T;   sex: string; } & Common;  type P1 = Person<number> | People<number>;  // 元组 type P2 = [Person<number>, People<number>];
    • 跟 typeof 结合使用

      const name = "小明";  type T= typeof name;

Record 的用途

Reacord 是 TypeScript 的一种工具类。

// 常规写法 interface Params {     [name: string]: any; }  // 高级写法 type Params = Recode<string, any>

Partial 的用途

将传入的属性变为可选项

interface DataModel {   name: string   age: number   address: string }  let store: DataModel = {   name: '',   age: 0,   address: '' }  function updateStore (   store: DataModel,   payload: Partial<DataModel> ):DataModel {   return {     ...store,     ...payload   } }  store = updateStore(store, {   name: 'lpp',   age: 18 })

Pick 的用途

从类型 Type 中,挑选一组属性组成一个新的类型返回。这组属性由 Keys 限定, Keys 是字符串或者字符串并集。

interface Person {   name: string   age: number   id: string }  // 幼儿没有id type Toddler = Pick<Person, 'name' | 'age'>

第二段

/**  * Builds a URL using the base template and specified parameters.  *  * @param {String} baseTemplate a URL template that contains zero or more :params  * @param {Object} params an object with properties that correspond to the :params  *   in the base template. Unused properties become query params.  *  * @returns {String} a URL with path params substituted and query params appended  *  * @example  * ```ts  * urlcat('http://api.example.com/users/:id', { id: 42, search: 'foo' })  * // -> 'http://api.example.com/users/42?search=foo  * ```  */ export default function urlcat(baseTemplate: string, params: ParamMap): string;  /**  * Concatenates the base URL and the path specified using '/' as a separator.  * If a '/' occurs at the concatenation boundary in either parameter, it is removed.  *  * @param {String} baseUrl the first part of the URL  * @param {String} path the second part of the URL  *  * @returns {String} the result of the concatenation  *  * @example  * ```ts  * urlcat('http://api.example.com/', '/users')  * // -> 'http://api.example.com/users  * ```  */ export default function urlcat(baseUrl: string, path: string): string;  /**  * Concatenates the base URL and the path specified using '/' as a separator.  * If a '/' occurs at the concatenation boundary in either parameter, it is removed.  * Substitutes path parameters with the properties of the @see params object and appends  * unused properties in the path as query params.  *  * @param {String} baseUrl the first part of the URL  * @param {String} path the second part of the URL  * @param {Object} params Object with properties that correspond to the :params  *   in the base template. Unused properties become query params.  *  * @returns {String} URL with path params substituted and query params appended  *  * @example  * ```ts  * urlcat('http://api.example.com/', '/users/:id', { id: 42, search: 'foo' })  * // -> 'http://api.example.com/users/42?search=foo  * ```  */ export default function urlcat(   baseUrl: string,   pathTemplate: string,   params: ParamMap ): string;  /**  * Concatenates the base URL and the path specified using '/' as a separator.  * If a '/' occurs at the concatenation boundary in either parameter, it is removed.  * Substitutes path parameters with the properties of the @see params object and appends  * unused properties in the path as query params.  *  * @param {String} baseUrl the first part of the URL  * @param {String} path the second part of the URL  * @param {Object} params Object with properties that correspond to the :params  *   in the base template. Unused properties become query params.  * @param {Object} config urlcat configuration object  *  * @returns {String} URL with path params substituted and query params appended  *  * @example  * ```ts  * urlcat('http://api.example.com/', '/users/:id', { id: 42, search: 'foo' }, {objectFormat: {format: 'RFC1738'}})  * // -> 'http://api.example.com/users/42?search=foo  * ```  */ export default function urlcat(   baseUrlOrTemplate: string,   pathTemplateOrParams: string | ParamMap,   maybeParams: ParamMap,   config: UrlCatConfiguration ): string;  export default function urlcat(   baseUrlOrTemplate: string,   pathTemplateOrParams: string | ParamMap,   maybeParams: ParamMap = {},   config: UrlCatConfiguration = {} ): string {   if (typeof pathTemplateOrParams === 'string') {     const baseUrl = baseUrlOrTemplate;     const pathTemplate = pathTemplateOrParams;     const params = maybeParams;     return urlcatImpl(pathTemplate, params, baseUrl, config);   } else {     const baseTemplate = baseUrlOrTemplate;     const params = pathTemplateOrParams;     return urlcatImpl(baseTemplate, params, undefined, config);   } }

这部分代码是利用 TypeScript 定义重载函数类型,采用连续多个重载声明 + 一个函数实现的方式来实现,其作用是为了保证在调用该函数时,函数的参数及返回值都要兼容所有的重载。

例如下图,第三个参数类型在重载函数类型中并不存在。

Untitled.png

第三段

以下代码是核心,作者通过职责分离的方式,将核心方法代码简化。

// 核心方法 function urlcatImpl(   pathTemplate: string,   params: ParamMap,   baseUrl: string | undefined,   config: UrlCatConfiguration ) {     // 第一步 path('/users/:id/posts', { id: 1, limit: 30 }) 返回 "/users/1/posts" 和 limit: 30   const { renderedPath, remainingParams } = path(pathTemplate, params);     // 第二步 移除 Null 或者 Undefined 属性   const cleanParams = removeNullOrUndef(remainingParams);     // 第三步 {limit: 30} 转 limit=30   const renderedQuery = query(cleanParams, config);     // 第四步 拼接返回 /users/1/posts?limit=30   const pathAndQuery = join(renderedPath, '?', renderedQuery);      // 第五步 当 baseUrl 存在时,执行完整 url 拼接   return baseUrl ? joinFullUrl(renderedPath, baseUrl, pathAndQuery) : pathAndQuery; }

总结

做开源并不一定要造个更好的轮子,但可以让这个轮子变得更好。通过该项目,也发现自己在 TypeScript 方面的不足,继续学习,再接再厉。

参考文章

拓展阅读

什么是 LRU 算法?

Posted: 13 Mar 2022 06:02 PM PDT

缓存 是我们写代码过程中常用的一种手段,是一种空间换时间的做法。就拿我们经常使用的 HTTP 协议,其中也存在强缓存和协商缓存两种缓存方式。当我们打开一个网站的时候,浏览器会查询该请求的响应头,通过判断响应头中是否有 Cache-ControlLast-ModifiedETag 等字段,来确定是否直接使用之前下载的资源缓存,而不是重新从服务器进行下载。

下面就是当我们访问百度时,某些资源命中了协商缓存,服务端返回 304 状态码,还有一部分资源命中了强缓存,直接读取了本地缓存。

但是,缓存并不是无限制的,会有大小的限制。无论是我们的 cookie(不同浏览器有所区别,一般在 4KB 左右),还是 localStorage(和 cookie 一样,不同浏览器有所区别,有些浏览器为 5MB,有些浏览器为 10MB),都会有大小限制。

这个时候就需要涉及到一种算法,需要将超出大小限制的缓存进行淘汰,一般的规则是淘汰掉最近没有被访问到的缓存,也就是今天要介绍的主角:LRULeast recently used:最近最少使用)。当然除了 LRU,常见的缓存淘汰还有 FIFO(first-in, first-out:先进先出) 和 LFU(Least frequently used:最少使用)。

什么是 LRU?

LRULeast recently used:最近最少使用)算法在缓存写满的时候,会根据所有数据的访问记录,淘汰掉未来被访问几率最低的数据。也就是说该算法认为,最近被访问过的数据,在将来被访问的几率最大。

为了方便理解 LRU 算法的全流程,画了一个简单的图:

  1. 假设我们有一块内存,一共能够存储 5 数据块;
  2. 依次向内存存入A、B、C、D、E,此时内存已经存满;
  3. 再次插入新的数据时,会将在内存存放时间最久的数据A淘汰掉;
  4. 当我们在外部再次读取数据B时,已经处于末尾的B会被标记为活跃状态,提到头部,数据C就变成了存放时间最久的数据;
  5. 再次插入新的数据G,存放时间最久的数据C就会被淘汰掉;

算法实现

下面通过一段简单的代码来实现这个逻辑。

class LRUCache {     list = [] // 用于标记先后顺序     cache = {} // 用于缓存所有数据     capacity = 0 // 缓存的最大容量     constructor (capacity) {     // 存储 LRU 可缓存的最大容量         this.capacity = capacity     } }

基本的结构如上所示,LRU需要实现的就是两个方法:getput

class LRUCache {   // 获取数据     get (key) { }   // 存储数据     put (key, value) { } }

我们现在看看如何进行数据的存储:

class LRUCache {   // 存储数据     put (key, value) {     // 存储之前需要先判断长度是否达到上限     if (this.list.length >= this.capacity) {       // 由于每次存储后,都会将 key 放入 list 最后,       // 所以,需要取出第一个 key,并删除cache中的数据。             const latest = this.list.shift()             delete this.cache[latest]         }     // 写入缓存         this.cache[key] = value     // 写入缓存后,需要将 key 放入 list 的最后         this.list.push(key)   } }

然后,在每次获取数据时,都需要更新 list,将当前获取的 key 放到 list 的最后。

class LRUCache {   // 获取数据     get (key) {         if (this.cache[key] !== undefined) {         // 如果 key 对应的缓存存在       // 在返回缓存之前,需要重新激活 key             this.active(key)             return this.cache[key]         }         return undefined   }   // 重新激活key,将指定 key 移动到 list 最后     active (key) {     // 先将 key 在 list 中删除         const idx = this.list.indexOf(key)         if (idx !== -1) {             this.list.splice(idx, 1)     }     // 然后将 key 放到 list 最后面         this.list.push(key)     } }

这个时候,其实还没有完全实现,因为除了 get 操作,put 操作也需要将对应的 key 重新激活。

class LRUCache {   // 存储数据     put (key, value) {         if (this.cache[key]) {             // 如果该 key 之前存在,将 key 重新激活             this.active(key)             this.cache[key] = value       // 而且此时缓存的长度不会发生变化       // 所以不需要进行后续的长度判断,可以直接返回             return         }      // 存储之前需要先判断长度是否达到上限     if (this.list.length >= this.capacity) {       // 由于每次存储后,都会将 key 放入 list 最后,       // 所以,需要取出第一个 key,并删除cache中的数据。             const latest = this.list.shift()             delete this.cache[latest]         }     // 写入缓存         this.cache[key] = value     // 写入缓存后,需要将 key 放入 list 的最后         this.list.push(key)   } }

可能会有人觉得这种算法在前端没有什么应用场景,说起来,在 Vue 的内置组件 keep-alive 中就使用到了 LRU 算法。

后续应该还会继续介绍一下 LFU 算法,敬请期待……

Spring Cloud Ribbon 中的 7 种负载均衡策略

Posted: 13 Mar 2022 05:49 PM PDT

负载均衡通器常有两种实现手段,一种是服务端负载均衡器,另一种是客户端负载均衡器,而我们今天的主角 Ribbon 就属于后者——客户端负载均衡器。

服务端负载均衡器的问题是,它提供了更强的流量控制权,但无法满足不同的消费者希望使用不同负载均衡策略的需求,而使用不同负载均衡策略的场景确实是存在的,所以客户端负载均衡就提供了这种灵活性。 然而客户端负载均衡也有其缺点,如果配置不当,可能会导致服务提供者出现热点,或者压根就拿不到任何服务的情况,所以我们本文就来了解一下这 7 种内置负载均衡策略的具体规则。

Ribbon 介绍

Ribbon 是 Spring Cloud 技术栈中非常重要的基础框架,它为 Spring Cloud 提供了负载均衡的能力,比如 Fegin 和 OpenFegin 都是基于 Ribbon 实现的,就连 Nacos 中的负载均衡也使用了 Ribbon 框架。

Ribbon 框架的强大之处在于,它不仅内置了 7 种负载均衡策略,同时还支持用户自定义负载均衡策略,所以其开放性和便利性也是它得以流行的主要原因。

服务端负载均衡器和客户端负载均衡器的区别如下图所示:
image.png
客户端负载均衡器的实现原理是通过注册中心,如 Nacos,将可用的服务列表拉取到本地(客户端),再通过客户端负载均衡器(设置的负载均衡策略)获取到某个服务器的具体 ip 和端口,然后再通过 Http 框架请求服务并得到结果,其执行流程如下图所示:image.png

负载均衡设置

以 Nacos 中的 Ribbon 负载均衡设置为例,在配置文件 application.yml 中设置如下配置即可:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #设置负载均衡策略

因为 Nacos 中已经内置了 Ribbon,所以在实际项目开发中无需再添加 Ribbon 依赖了,这一点我们在 Nacos 的依赖树中就可以看到,如下图所示:
image.png
Ribbon 默认的负载均衡策略是轮询模式,我们配置 3 个服务提供者的执行结果如下图所示:
轮询.gif
然后,我们再将 Ribbon 负载均衡策略设置为随机模式,配置内容如下:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #设置随机负载均衡

重启客户端,执行结果如下图所示:
随机策略.gif

7种负载均衡策略

1.轮询策略

轮询策略:RoundRobinRule,按照一定的顺序依次调用服务实例。比如一共有 3 个服务,第一次调用服务 1,第二次调用服务 2,第三次调用服务3,依次类推。
此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #设置负载均衡

2.权重策略

权重策略:WeightedResponseTimeRule,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。
它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大。
此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

3.随机策略

随机策略:RandomRule,从服务提供者的列表中随机选择一个服务实例。
此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #设置负载均衡

4.最小连接数策略

最小连接数策略:BestAvailableRule,也叫最小并发数策略,它是遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,那么会调用轮询策略进行选取。
此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #设置负载均衡

5.重试策略

重试策略:RetryRule,按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null。
此策略的配置设置如下:

ribbon:   ConnectTimeout: 2000 # 请求连接的超时时间   ReadTimeout: 5000 # 请求处理的超时时间 springcloud-nacos-provider: # nacos 中的服务 id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #设置负载均衡

6.可用性敏感策略

可用敏感性策略:AvailabilityFilteringRule,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例。
此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.AvailabilityFilteringRule

7.区域敏感策略

区域敏感策略:ZoneAvoidanceRule,根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。
此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id   ribbon:     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule

项目源码

https://gitee.com/mydb/spring-cloud-alibaba-example

总结

Ribbon 为客户端负载均衡器,相比于服务端负载均衡器的统一负载均衡策略来说,它提供了更多的灵活性。Ribbon 内置了 7 种负载均衡策略:轮询策略、权重策略、随机策略、最小连接数策略、重试策略、可用性敏感策略、区域性敏感策略,并且用户可以通过继承 RoundRibbonRule 来实现自定义负载均衡策略。

是非审之于己,毁誉听之于人,得失安之于数。

公众号:Java中文社群

Java面试合集:https://gitee.com/mydb/interview

No comments:

Post a Comment