限流的目的是通過對並發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務。
前幾天在DD的公眾號,看了一篇關於使用 瓜娃 實現單應用限流的方案,參考《redis in action》 實現了一個jedis版本的,都屬於業務層次限制。 實際場景中常用的限流策略:
- Nginx接入層限流
按照一定的規則如帳號、IP、系統調用邏輯等在Nginx層面做限流 - 業務應用系統限流
通過業務代碼控制流量這個流量可以被稱為信號量,可以理解成是一種鎖,它可以限制一項資源最多能同時被多少進程訪問。代碼實現
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.ZParams;import java.util.List;
import java.util.UUID;/**
* email wangiegie@gmail.com
* @data 2017-08
*/
public class RedisRateLimiter {
private static final String BUCKET = "BUCKET";
private static final String BUCKET_COUNT = "BUCKET_COUNT";
private static final String BUCKET_MONITOR = "BUCKET_MONITOR";static String acquireTokenFromBucket(
Jedis jedis, int limit, long timeout) {
String identifier = UUID.randomUUID().toString();
long now = System.currentTimeMillis();
Transaction transaction = jedis.multi();//刪除信號量
transaction.zremrangeByScore(BUCKET_MONITOR.getBytes(), "-inf".getBytes(), String.valueOf(now - timeout).getBytes());
ZParams params = new ZParams();
params.weightsByDouble(1.0,0.0);
transaction.zinterstore(BUCKET, params, BUCKET, BUCKET_MONITOR);//計數器自增
transaction.incr(BUCKET_COUNT);
List<Object> results = transaction.exec();
long counter = (Long) results.get(results.size() - 1);transaction = jedis.multi();
transaction.zadd(BUCKET_MONITOR, now, identifier);
transaction.zadd(BUCKET, counter, identifier);
transaction.zrank(BUCKET, identifier);
results = transaction.exec();
//獲取排名,判斷請求是否取得了信號量
long rank = (Long) results.get(results.size() - 1);
if (rank < limit) {
return identifier;
} else {//沒有獲取到信號量,清理之前放入redis 中垃圾數據
transaction = jedis.multi();
transaction.zrem(BUCKET_MONITOR, identifier);
transaction.zrem(BUCKET, identifier);
transaction.exec();
}
return null;
}
} -
調用
12345678910111213測試接口調用@GetMapping("/")public void index(HttpServletResponse response) throws IOException {Jedis jedis = jedisPool.getResource();String token = RedisRateLimiter.acquireTokenFromBucket(jedis, LIMIT, TIMEOUT);if (token == null) {response.sendError(500);}else{//TODO 你的業務邏輯}jedisPool.returnResource(jedis);}優化
使用攔截器 + 注解優化代碼
攔截器
1234567891011121314151617181920212223242526272829303132@Configurationstatic class WebMvcConfigurer extends WebMvcConfigurerAdapter {private Logger logger = LoggerFactory.getLogger(WebMvcConfigurer.class);@Autowiredprivate JedisPool jedisPool;public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new HandlerInterceptorAdapter() {public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);if (rateLimiter != null){int limit = rateLimiter.limit();int timeout = rateLimiter.timeout();Jedis jedis = jedisPool.getResource();String token = RedisRateLimiter.acquireTokenFromBucket(jedis, limit, timeout);if (token == null) {response.sendError(500);return false;}logger.debug("token -> {}",token);jedis.close();}return true;}}).addPathPatterns("/*");}}定義注解
1234567891011121314/*** email wangiegie@gmail.com* @data 2017-08* 限流注解*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RateLimiter {int limit() default 5;int timeout() default 1000;}使用
12345@RateLimiter(limit = 2, timeout = 5000)@GetMapping("/test")public void test() {}並發測試
工具:apache-jmeter-3.2
說明: 沒有獲取到信號量的接口返回500,status是紅色,獲取到信號量的接口返回200,status是綠色。
當限制請求信號量為2,並發5個線程:
當限制請求信號量為5,並發10個線程:
資料
總結
- 對於信號量的操作,使用事務操作。
- 不要使用時間戳作為信號量的排序分數,因為在分布式環境中,各個節點的時間差的原因,會出現不公平信號量的現象。
- 可以使用把這塊代碼抽成@rateLimiter注解,然后再方法上使用就會很方便啦
- 不同接口的流控,可以參考源碼的里面RedisRateLimiterPlus,無非是每個接口生成一個監控參數
- 源碼http://git.oschina.net/boding1/pig-cloud