一、处理高并发

  1.1高并发处理方案: 

  • 缓存 缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级 降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开
  • 限流 限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

  1、2限流方式: mq、ratelimiter

   

二、ratelimiter是基于令牌桶算法来做的 

  guava的RateLimiter使用的是令牌桶算法,也就是以固定的频率向桶中放入令牌,例如一秒钟10枚令牌,实际业务在每次响应请求之前都从桶中获取令牌,只有取到令牌的请求才会被成功响应,获取的方式有两种:阻塞等待令牌或者取不到立即返回失败

                                                                                    

 另外简单介绍漏铜算法:

  请求以一定速度进入漏桶中,如果请求速度>处理请求速度则溢出,漏桶算法能强行处理请求速率。但如果在处理高并发时,突然来大量请求这种方案不合适  

                                                                          

 2 基于ArrayBlockingQueue的实现jiava令牌桶实现

参考: https://blog.csdn.net/weixin_41750142/article/details/128082225?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ECtr-4-128082225-blog-144816739.235%5Ev43%5Econtrol&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ECtr-4-128082225-blog-144816739.235%5Ev43%5Econtrol&utm_relevant_index=8

public class RateLimiter {
   private static final String TOKEN = "token"; 
  private int capbility;  //令牌桶容量
  private int amount;  //每次令牌添加数量
  private int period; //令牌添加频率
  private ArrayBlockingQueue<String> arrayBlockingQueue; //阻塞队列用于令牌添加和消费
    
    public void RateLimiter(int capbility, int rate){
      this.capbility=capbility;
      this.rate=rate;
       arrayBlockingQueue = new ArrayBlockingQueue<>(limit);
      init();  //初始化加载一次
   }
   
  //初始化令牌桶,将令牌桶以最大容量方式填满
   public void init(){
     for(int i=0;i<capbility;i++){
        arrayBlockingQueue.offer(TOKEN);
     }
  }

  //生成令牌,周期性线程池,启动后延迟500毫秒,每500毫秒执行一次
  public void startGenerateToken(){
     Executor.newScheduledThreadPool((1).scheduleAtFixedRate(() -> {
            synchronized (lock) {
                // 往令牌桶添加令牌
                addToken();
                lock.notifyAll();
            }
        }, 500, this.period, TimeUnit.MILLISECONDS); //延迟500ms后,每隔period执行一次
    }
  }
  
  public void addToken(){
    for(int i=0;i<amount;i++){
        arrayBlockingQueue.offer(TOKEN);  //队列溢出返回false
     }
  }

  //获取令牌 
  public boolean tryAcquire(){
     return arrayBlockingQueue.poll()==null; //队列没有数据即没有令牌
  }
  
}


public class TestTokenLimiter {
    final static Object LOCK = new Object();
   
    public static void main(String[] args) throws InterruptedException {
        int period = 500;
        // 创建令牌桶
        TokenLimiter limiter = new TokenLimiter(2, period, 2);
        // 生产令牌
        limiter.start(LOCK);

        // 这里主要是保证让线程池初始先产生2个令牌,
        // 其实init方法里已经加了2个令牌了,而队列长度限定为2,
        // 所以初始时addToken()并不会成功。
        // 如果这里不使用wait和notify配合,可能客户端获取令牌会先执行,打破节奏了
        synchronized (LOCK) {
         // 该线程被阻塞挂起,直到其他线程调用了该共享对象的notify()或者notifyAll()方法,才返回
         LOCK.wait();          
        }
   
        // 启动4个线程,模拟4个客户端各自每隔500ms获取令牌
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                while (true) {
                    String name = Thread.currentThread().getName();
                    if (limiter.tryAcquire()) {
                        System.out.println(name + ":拿到令牌");
                    } else {
                        System.out.println(name + ":没有令牌");
                    }

                  // 如果不sleep  线程没法切到生产的去执行
                  try {
                        Thread.sleep(period);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

 

 

 对于 BlockingQueue 的阻塞队列提供了四种处理方法:

 

抛出异常: 是指当阻塞队列满时候,再往队列里插入元素,会抛出java.lang.IllegalStateException: Queue full异常。当队列为空时,从队列里获取元素时会抛出java.util.NoSuchElementException异常 。
返回特殊值: 插入方法会返回是否成功,成功则返回true,否则返回false。移除方法,则是从队列里拿出一个元素,如果没有元素则返回null。
一直阻塞: 当阻塞队列满时,如果生产者线程继续往队列里put元素,队列会一直阻塞生产者线程,直到把数据put进去,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到从队列中take到元素为止。
超时退出: 当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出

3 基于Guava RateLimiter实现令牌桶

参考 https://cloud.tencent.com/developer/article/1891525

使用Guava RateLimiter实现令牌桶主要采用如下RateLimiter自带函数

  • acquire():从 RateLimiter 获取一个许可令牌,该方法会被阻塞直到获取到请求。

  • acquire(int permits):从 RateLimiter 获取指定许可令牌数,该方法会被阻塞直到获取到请求。

  • create(double permitsPerSecond):每秒创建permitsPerSecond个许可令牌

 

  • create(double permitsPerSecond, long warmupPeriod, TimeUnit unit):根据指定的稳定吞吐率和预热期来创建 RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指 QPS,每秒多少个请求量),在这段预热时间内,RateLimiter 每秒分配的许可许可数会平稳地增长直到预热期结束时达到其最大速率。(只要存在足够请求数来使其饱和)
  • getRate():返回 RateLimiter 配置中的稳定速率,该速率单位是每秒多少许可令牌数。
  • setRate(double permitsPerSecond):更新 RateLimite 的稳定速率,参数 permitsPerSecond 由构造 RateLimiter 的工厂方法提供。
  • tryAcquire():从 RateLimiter 获取令牌,如果该许可令牌可以在无延迟下的情况下立即获取得到的话
  • tryAcquire(int permits)从 RateLimiter 获取许可令牌数,如果该许可令牌数可以在无延迟下的情况下立即获取得到的话
  • tryAcquire(int permits, long timeout, TimeUnit unit):从 RateLimiter 获取指定许可令牌数如果该许可令牌数可以在不超过 timeout 的时间内获取得到的话,或者如果无法在 timeout 过期之前获取得到许可令牌数的话,那么立即返回 false (无需等待)
  • tryAcquire(long timeout, TimeUnit unit):从 RateLimiter 获取许可如果该许可令牌可以在不超过 timeout 的时间内获取得到的话,或者如果无法在 timeout 过期之前获取得到许可令牌的话,那么立即返回 false(无需等待)

 

@Service
public class GuavaRateLimiterService {
    /*每秒控制5个许可*/
    RateLimiter rateLimiter = RateLimiter.create(5.0);
 
    /**
     * 获取令牌
     *
     * @return
     */
    public boolean tryAcquire() {
        return rateLimiter.tryAcquire();
    }
    
}
  @Autowired
    private GuavaRateLimiterService rateLimiterService;
    
    @ResponseBody
    @RequestMapping("/ratelimiter")
    public Result testRateLimiter(){
        if(rateLimiterService.tryAcquire()){
            return ResultUtil.success1(1001,"成功获取许可令牌");
        }
        return ResultUtil.success1(1002,"未获取到许可令牌");
    }

 

meter起10个线程并发访问接口,测试结果如下:

可以发现,10个并发访问总是只有6个能获取到许可令牌,结论就是能获取到RateLimiter.create(n)中n+1个许可令牌,总体来看Guava的RateLimiter是比较优雅的

pom文件引入guava依赖:

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>

4 基于Redis+Lua令牌桶实现

参考 https://blog.csdn.net/weixin_42536694/article/details/145962710

Lua脚本可以将多个Reids指令作为一个原子性操作,保证线程安全

具体实现:

Redis存储令牌

Lua 以原子性进行检查+令牌数量更新

  • Lua脚本

  local key = KEYS[1] -- Redis Key(存储令牌数)
  local rate = tonumber(ARGV[1]) -- 令牌添加速率(每秒增加多少个)
  local capacity = tonumber(ARGV[2]) -- 令牌桶的最大容量
  local request = tonumber(ARGV[4]) -- 请求的令牌数

  local now = tonumber(ARGV[3])  -- 当前时间戳(秒)

  local lastUpate=tonumber(redis.call("GET", key .. ":time")) or now  -- 上次刷新时间

  local gaps=now-lastUpdate  --上次刷新和当前时间间距

  local lastTokenNums=tonumber(redis.call("GET", key)) or capacity  -- 计算上次令牌数量(默认满桶)

   local currentTokenNums=min(capbility,lastTokenNums+rate*gaps) --计算当前令牌数量(上次刷新和当前时间间距*rate=这段时间令牌桶里新增多少令牌,要拿这段时间新增令牌+上次剩余令牌值和令牌桶容量做对比找出较小值作为当前令牌桶中令牌数量)

  -- 判断是否允许请求

  if currentTokenNums >= request then
  -- 允许请求,扣除令牌
    redis.call("SET", key, currentTokenNums - request)
    redis.call("SET", key .. ":time", now)
    return 1 -- 允许访问
  else
    -- 令牌不足,拒绝请求
    return 0
  end

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.util.Pool;
import java.time.Instant;
import java.util.Collections;

public class RedisRateLimiter {
    private static final String LUA_SCRIPT =
            "local key = KEYS[1] " +
            "local rate = tonumber(ARGV[1]) " +
            "local capacity = tonumber(ARGV[2]) " +
            "local now = tonumber(ARGV[3]) " +
            "local request = tonumber(ARGV[4]) " +
            "local last_tokens = tonumber(redis.call('GET', key)) or capacity " +
            "local last_update = tonumber(redis.call('GET', key .. ':time')) or now " +
            "local gap = now - last_time " +
            "local current_token = math.min(capacity, last_tokens + (gap * rate)) " +
            "if current_token >= request then " +
            "    redis.call('SET', key, current_token - request) " +
            "    redis.call('SET', key .. ':time', now) " +
            "    return 1 " +
            "else " +
            "    return 0 " +
            "end";

    private final Pool<Jedis> jedisPool;

    public RedisRateLimiter(Pool<Jedis> jedisPool) {
        this.jedisPool = jedisPool;
    }

    public boolean tryAcquire(String key, int rate, int capacity, int requested) {
        try (Jedis jedis = jedisPool.getResource()) {
            long now = Instant.now().getEpochSecond();
            Object result = jedis.eval(LUA_SCRIPT, 
                                       Collections.singletonList(key), 
                                       Arrays.asList(String.valueOf(rate),
                                                     String.valueOf(capacity),
                                                     String.valueOf(now),
                                                     String.valueOf(requested)));
            return Integer.parseInt(result.toString()) == 1;
        }
    }

    public static void main(String[] args) {
        JedisPool pool = new JedisPool("localhost", 6379);
        RedisRateLimiter limiter = new RedisRateLimiter(pool);

        // 测试限流:每秒最多 5 个请求,最大容量 10,当前请求 1 个
        for (int i = 0; i < 20; i++) {
            boolean allowed = limiter.tryAcquire("rate_limit", 5, 10, 1);
            System.out.println("请求 " + (i + 1) + (allowed ? " ✅ 通过" : " ❌ 拒绝"));
            try {
                Thread.sleep(200); // 200ms 发起一个请求
            } catch (InterruptedException ignored) {}
        }

        pool.close();
    }
}

结果


请求 1 ✅ 通过
请求 2 ✅ 通过
请求 3 ✅ 通过
...
请求 11 ❌ 拒绝
请求 12 ❌ 拒绝

 

 
 
 
posted on 2019-07-17 15:12  colorfulworld  阅读(3362)  评论(0)    收藏  举报