中文字幕日韩精品一区二区免费_精品一区二区三区国产精品无卡在_国精品无码专区一区二区三区_国产αv三级中文在线

分布式限流,你想知道的都在這里

2021-02-17    分類: 網(wǎng)站建設(shè)

前言

在一個(gè)高并發(fā)系統(tǒng)中對(duì)流量的把控是非常重要的,當(dāng)巨大的流量直接請(qǐng)求到我們的服務(wù)器上沒(méi)多久就可能造成接口不可用,不處理的話甚至?xí)斐烧麄€(gè)應(yīng)用不可用。

比如最近就有個(gè)這樣的需求,我作為客戶端要向kafka生產(chǎn)數(shù)據(jù),而kafka的消費(fèi)者則再源源不斷的消費(fèi)數(shù)據(jù),并將消費(fèi)的數(shù)據(jù)全部請(qǐng)求到web服務(wù)器,雖說(shuō)做了負(fù)載(有4臺(tái)web服務(wù)器)但業(yè)務(wù)數(shù)據(jù)的量也是巨大的,每秒鐘可能有上萬(wàn)條數(shù)據(jù)產(chǎn)生。如果生產(chǎn)者直接生產(chǎn)數(shù)據(jù)的話極有可能把web服務(wù)器拖垮。

對(duì)此就必須要做限流處理,每秒鐘生產(chǎn)一定限額的數(shù)據(jù)到kafka,這樣就能極大程度的保證web的正常運(yùn)轉(zhuǎn)。

其實(shí)不管處理何種場(chǎng)景,本質(zhì)都是降低流量保證應(yīng)用的高可用。

常見(jiàn)算法

對(duì)于限流常見(jiàn)有兩種算法:

  • 漏桶算法
  • 令牌桶算法

漏桶算法比較簡(jiǎn)單,就是將流量放入桶中,漏桶同時(shí)也按照一定的速率流出,如果流量過(guò)快的話就會(huì)溢出(漏桶并不會(huì)提高流出速率)。溢出的流量則直接丟棄。

如下圖所示:

分布式限流,你想知道的都在這里

這種做法簡(jiǎn)單粗暴。

漏桶算法雖說(shuō)簡(jiǎn)單,但卻不能應(yīng)對(duì)實(shí)際場(chǎng)景,比如突然暴增的流量。

這時(shí)就需要用到令牌桶算法:

令牌桶會(huì)以一個(gè)恒定的速率向固定容量大小桶中放入令牌,當(dāng)有流量來(lái)時(shí)則取走一個(gè)或多個(gè)令牌。當(dāng)桶中沒(méi)有令牌則將當(dāng)前請(qǐng)求丟棄或阻塞。

相比之下令牌桶可以應(yīng)對(duì)一定的突發(fā)流量。

RateLimiter實(shí)現(xiàn)

對(duì)于令牌桶的代碼實(shí)現(xiàn),可以直接使用Guava包中的RateLimiter。

  1. @Override 
  2. public BaseResponse<UserResVO> getUserByFeignBatch(@RequestBody UserReqVO userReqVO) { 
  3.  //調(diào)用遠(yuǎn)程服務(wù) 
  4.  OrderNoReqVO vo = new OrderNoReqVO() ; 
  5.  vo.setReqNo(userReqVO.getReqNo()); 
  6.  RateLimiter limiter = RateLimiter.create(2.0) ; 
  7.  //批量調(diào)用 
  8.  for (int i = 0 ;i< 10 ; i++){ 
  9.  double acquire = limiter.acquire(); 
  10.  logger.debug("獲取令牌成功!,消耗=" + acquire); 
  11.  BaseResponse<OrderNoResVO> orderNo = orderServiceClient.getOrderNo(vo); 
  12.  logger.debug("遠(yuǎn)程返回:"+JSON.toJSONString(orderNo)); 
  13.  } 
  14.  UserRes userRes = new UserRes() ; 
  15.  userRes.setUserId(123); 
  16.  userRes.setUserName("張三"); 
  17.  userRes.setReqNo(userReqVO.getReqNo()); 
  18.  userRes.setCode(StatusEnum.SUCCESS.getCode()); 
  19.  userRes.setMessage("成功"); 
  20.  return userRes ; 

詳見(jiàn)此。

調(diào)用結(jié)果如下:

代碼可以看出以每秒向桶中放入兩個(gè)令牌,請(qǐng)求一次消耗一個(gè)令牌。所以每秒鐘只能發(fā)送兩個(gè)請(qǐng)求。按照?qǐng)D中的時(shí)間來(lái)看也確實(shí)如此(返回值是獲取此令牌所消耗的時(shí)間,差不多也是每500ms一個(gè))。

使用RateLimiter有幾個(gè)值得注意的地方:

允許先消費(fèi),后付款,意思就是它可以來(lái)一個(gè)請(qǐng)求的時(shí)候一次性取走幾個(gè)或者是剩下所有的令牌甚至多取,但是后面的請(qǐng)求就得為上一次請(qǐng)求買單,它需要等待桶中的令牌補(bǔ)齊之后才能繼續(xù)獲取令牌。

總結(jié)

針對(duì)于單個(gè)應(yīng)用的限流 RateLimiter 夠用了,如果是分布式環(huán)境可以借助 Redis 來(lái)完成。

來(lái)做演示。

在 Order 應(yīng)用提供的接口中采取了限流。首先是配置了限流工具的 Bean:

  1. @Configuration 
  2. public class RedisLimitConfig { 
  3.  @Value("${redis.limit}") 
  4.  private int limit; 
  5.  @Autowired 
  6.  private JedisConnectionFactory jedisConnectionFactory; 
  7.  @Bean 
  8.  public RedisLimit build() { 
  9.  RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection(); 
  10.  JedisCluster jedisCluster = (JedisCluster) clusterConnection.getNativeConnection(); 
  11.  RedisLimit redisLimit = new RedisLimit.Builder<>(jedisCluster) 
  12.  .limit(limit) 
  13.  .build(); 
  14.  return redisLimit; 
  15.  } 

接著在 Controller 使用組件:

  1. @Autowired 
  2. private RedisLimit redisLimit ; 
  3. @Override 
  4. @CheckReqNo 
  5. public BaseResponse<OrderNoResVO> getOrderNo(@RequestBody OrderNoReqVO orderNoReq) { 
  6.  BaseResponse<OrderNoResVO> res = new BaseResponse(); 
  7.  //限流 
  8.  boolean limit = redisLimit.limit(); 
  9.  if (!limit){ 
  10.  res.setCode(StatusEnum.REQUEST_LIMIT.getCode()); 
  11.  res.setMessage(StatusEnum.REQUEST_LIMIT.getMessage()); 
  12.  return res ; 
  13.  } 
  14.  res.setReqNo(orderNoReq.getReqNo()); 
  15.  if (null == orderNoReq.getAppId()){ 
  16.  throw new SBCException(StatusEnum.FAIL); 
  17.  } 
  18.  OrderNoResVO orderNoRes = new OrderNoResVO() ; 
  19.  orderNoRes.setOrderId(DateUtil.getLongTime()); 
  20.  res.setCode(StatusEnum.SUCCESS.getCode()); 
  21.  res.setMessage(StatusEnum.SUCCESS.getMessage()); 
  22.  res.setDataBody(orderNoRes); 
  23.  return res ; 

為了方便使用,也提供了注解:

  1. @Override 
  2. @ControllerLimit 
  3. public BaseResponse<OrderNoResVO> getOrderNoLimit(@RequestBody OrderNoReqVO orderNoReq) { 
  4.  BaseResponse<OrderNoResVO> res = new BaseResponse(); 
  5.  // 業(yè)務(wù)邏輯 
  6.  return res ; 

該注解攔截了 http 請(qǐng)求,會(huì)再請(qǐng)求達(dá)到閾值時(shí)直接返回。

普通方法也可使用:

  1. @CommonLimit 
  2. public void doSomething(){} 

會(huì)在調(diào)用達(dá)到閾值時(shí)拋出異常。

為了模擬并發(fā),在 User 應(yīng)用中開(kāi)啟了 10 個(gè)線程調(diào)用 Order(限流次數(shù)為5) 接口(也可使用專業(yè)的并發(fā)測(cè)試工具 JMeter 等)。

  1. @Override 
  2. public BaseResponse<UserResVO> getUserByFeign(@RequestBody UserReqVO userReq) { 
  3.  //調(diào)用遠(yuǎn)程服務(wù) 
  4.  OrderNoReqVO vo = new OrderNoReqVO(); 
  5.  vo.setAppId(1L); 
  6.  vo.setReqNo(userReq.getReqNo()); 
  7.  for (int i = 0; i < 10; i++) { 
  8.  executorService.execute(new Worker(vo, orderServiceClient)); 
  9.  } 
  10.  UserRes userRes = new UserRes(); 
  11.  userRes.setUserId(123); 
  12.  userRes.setUserName("張三"); 
  13.  userRes.setReqNo(userReq.getReqNo()); 
  14.  userRes.setCode(StatusEnum.SUCCESS.getCode()); 
  15.  userRes.setMessage("成功"); 
  16.  return userRes; 
  17. private static class Worker implements Runnable { 
  18.  private OrderNoReqVO vo; 
  19.  private OrderServiceClient orderServiceClient; 
  20.  public Worker(OrderNoReqVO vo, OrderServiceClient orderServiceClient) { 
  21.  this.vo = vo; 
  22.  this.orderServiceClient = orderServiceClient; 
  23.  } 
  24.  @Override 
  25.  public void run() { 
  26.  BaseResponse<OrderNoResVO> orderNo = orderServiceClient.getOrderNoCommonLimit(vo); 
  27.  logger.info("遠(yuǎn)程返回:" + JSON.toJSONString(orderNo)); 
  28.  } 

為了驗(yàn)證分布式效果啟動(dòng)了兩個(gè) Order 應(yīng)用。

效果如下:

實(shí)現(xiàn)原理

實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單。既然要達(dá)到分布式全局限流的效果,那自然需要一個(gè)第三方組件來(lái)記錄請(qǐng)求的次數(shù)。

其中 Redis 就非常適合這樣的場(chǎng)景。

  • 每次請(qǐng)求時(shí)將當(dāng)前時(shí)間(精確到秒)作為 Key 寫(xiě)入到 Redis 中,超時(shí)時(shí)間設(shè)置為 2 秒,Redis 將該 Key 的值進(jìn)行自增。
  • 當(dāng)達(dá)到閾值時(shí)返回錯(cuò)誤。
  • 寫(xiě)入 Redis 的操作用 Lua 腳本來(lái)完成,利用 Redis 的單線程機(jī)制可以保證每個(gè) Redis 請(qǐng)求的原子性。

Lua 腳本如下:

--lua 下標(biāo)從 1 開(kāi)始-- 限流 keylocal key = KEYS[1]-- 限流大小local limit = tonumber(ARGV[1])-- 獲取當(dāng)前流量大小local curentLimit = tonumber(redis.call('get', key) or "0")if curentLimit + 1 > limit then -- 達(dá)到限流大小 返回 return 0;else -- 沒(méi)有達(dá)到閾值 value + 1 redis.call("INCRBY", key, 1) redis.call("EXPIRE", key, 2) return curentLimit + 1end

Java 中的調(diào)用邏輯:

  1. --lua 下標(biāo)從 1 開(kāi)始 
  2. -- 限流 key 
  3. local key = KEYS[1] 
  4. -- 限流大小 
  5. local limit = tonumber(ARGV[1]) 
  6. -- 獲取當(dāng)前流量大小 
  7. local curentLimit = tonumber(redis.call('get', key) or "0") 
  8. if curentLimit + 1 > limit then 
  9.  -- 達(dá)到限流大小 返回 
  10.  return 0; 
  11. else 
  12.  -- 沒(méi)有達(dá)到閾值 value + 1 
  13.  redis.call("INCRBY", key, 1) 
  14.  redis.call("EXPIRE", key, 2) 
  15.  return curentLimit + 1 
  16. end 

所以只需要在需要限流的地方調(diào)用該方法對(duì)返回值進(jìn)行判斷即可達(dá)到限流的目的。

當(dāng)然這只是利用 Redis 做了一個(gè)粗暴的計(jì)數(shù)器,如果想實(shí)現(xiàn)類似于上文中的令牌桶算法可以基于 Lua 自行實(shí)現(xiàn)。

Builder 構(gòu)建器

在設(shè)計(jì)這個(gè)組件時(shí)想盡量的提供給使用者清晰、可讀性、不易出錯(cuò)的 API。

比如第一步,如何構(gòu)建一個(gè)限流對(duì)象。

最常用的方式自然就是構(gòu)造函數(shù),如果有多個(gè)域則可以采用重疊構(gòu)造器的方式:

  1. public A(){} 
  2. public A(int a){} 
  3. public A(int a,int b){} 

缺點(diǎn)也是顯而易見(jiàn)的:如果參數(shù)過(guò)多會(huì)導(dǎo)致難以閱讀,甚至如果參數(shù)類型一致的情況下客戶端顛倒了順序,但不會(huì)引起警告從而出現(xiàn)難以預(yù)測(cè)的結(jié)果。

第二種方案可以采用 JavaBean 模式,利用 setter 方法進(jìn)行構(gòu)建:

  1. A a = new A(); 
  2. a.setA(a); 
  3. a.setB(b); 

這種方式清晰易讀,但卻容易讓對(duì)象處于不一致的狀態(tài),使對(duì)象處于線程不安全的狀態(tài)。

所以這里采用了第三種創(chuàng)建對(duì)象的方式,構(gòu)建器:

  1. public class RedisLimit { 
  2.  private JedisCommands jedis; 
  3.  private int limit = 200; 
  4.  private static final int FAIL_CODE = 0; 
  5.  /** 
  6.  * lua script 
  7.  */ 
  8.  private String script; 
  9.  private RedisLimit(Builder builder) { 
  10.  this.limit = builder.limit ; 
  11.  this.jedis = builder.jedis ; 
  12.  buildScript(); 
  13.  } 
  14.  /** 
  15.  * limit traffic 
  16.  * @return if true 
  17.  */ 
  18.  public boolean limit() { 
  19.  String key = String.valueOf(System.currentTimeMillis() / 1000); 
  20.  Object result = null; 
  21.  if (jedis instanceof Jedis) { 
  22.  result = ((Jedis) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); 
  23.  } else if (jedis instanceof JedisCluster) { 
  24.  result = ((JedisCluster) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); 
  25.  } else { 
  26.  //throw new RuntimeException("instance is error") ; 
  27.  return false; 
  28.  } 
  29.  if (FAIL_CODE != (Long) result) { 
  30.  return true; 
  31.  } else { 
  32.  return false; 
  33.  } 
  34.  } 
  35.  /** 
  36.  * read lua script 
  37.  */ 
  38.  private void buildScript() { 
  39.  script = ScriptUtil.getScript("limit.lua"); 
  40.  } 
  41.  /** 
  42.  * the builder 
  43.  * @param <T> 
  44.  */ 
  45.  public static class Builder<T extends JedisCommands>{ 
  46.  private T jedis = null ; 
  47.  private int limit = 200; 
  48.  public Builder(T jedis){ 
  49.  this.jedis = jedis ; 
  50.  } 
  51.  public Builder limit(int limit){ 
  52.  this.limit = limit ; 
  53.  return this; 
  54.  } 
  55. 標(biāo)題名稱:分布式限流,你想知道的都在這里
    URL網(wǎng)址:http://www.rwnh.cn/news15/101365.html

    成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站導(dǎo)航、網(wǎng)頁(yè)設(shè)計(jì)公司、定制網(wǎng)站App設(shè)計(jì)、建站公司、關(guān)鍵詞優(yōu)化

    廣告

    聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

    外貿(mào)網(wǎng)站制作
    磐安县| 如东县| 万全县| 黄浦区| 富民县| 深泽县| 孙吴县| 屏南县| 无锡市| 集贤县| 柘城县| 吴江市| 防城港市| 江山市| 黑龙江省| 花垣县| 新田县| 洱源县| 绍兴县| 灵台县| 柳河县| 璧山县| 沁源县| 齐河县| 临洮县| 阜新市| 广平县| 海宁市| 宜宾市| 唐山市| 安吉县| 沈丘县| 凤城市| 汉寿县| 贡觉县| 泗洪县| 德州市| 东乌珠穆沁旗| 宾川县| 红桥区| 临沭县|