單機限流和分散式應用限流
單機限流
在大資料量高併發訪問時,經常會出現服務或介面面對暴漲的請求而不可用的情況,甚至引發連鎖反映導致整個系統崩潰。此時你需要使用的技術手段之一就是限流,當請求達到一定的併發數或速率,就進行等待、排隊、降級、拒絕服務等。在限流時,常見的兩種演算法是漏桶和令牌桶演算法演算法。
單機限流演算法主要有:令牌桶(Token Bucket)、漏桶(leaky bucket)和計數器演算法是最常用的三種限流的演算法。
1. 令牌桶演算法
令牌桶演算法的原理是系統會以一個恆定的速度往桶裡放入令牌,而如果請求需要被處理,則需要先從桶裡獲取一個令牌,當桶裡沒有令牌可取時,則拒絕服務。 當桶滿時,新新增的令牌被丟棄或拒絕。
public class RateLimiterDemo {
private static RateLimiter limiter = RateLimiter.create(5);
public static void exec() {
limiter.acquire(1);
try {
// 處理核心邏輯
TimeUnit.SECONDS.sleep(1);
System.out.println("--" + System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Guava RateLimiter 提供了令牌桶演算法可用於平滑突發限流策略。
該示例為每秒中產生5個令牌,每200毫秒會產生一個令牌。
limiter.acquire() 表示消費一個令牌。當桶中有足夠的令牌時,則直接返回0,否則阻塞,直到有可用的令牌數才返回,返回的值為阻塞的時間。
2. 漏桶演算法
它的主要目的是控制資料注入到網路的速率,平滑網路上的突發流量,資料可以以任意速度流入到漏桶中。漏桶演算法提供了一種機制,通過它,突發流量可以被整形以便為網路提供一個穩定的流量。 漏桶可以看作是一個帶有常量服務時間的單伺服器佇列,如果漏桶為空,則不需要流出水滴,如果漏桶(包快取)溢位,那麼水滴會被溢位丟棄。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 漏斗限流演算法
*
* @author dadiyang
* @date 2018/9/28
*/
public class FunnelRateLimiter {
private Map<String, Funnel> funnelMap = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException {
FunnelRateLimiter limiter = new FunnelRateLimiter();
int testAccessCount = 30;
int capacity = 5;
int allowQuota = 5;
int perSecond = 30;
int allowCount = 0;
int denyCount = 0;
for (int i = 0; i < testAccessCount; i++) {
boolean isAllow = limiter.isActionAllowed("dadiyang", "doSomething", 5, 5, 30);
if (isAllow) {
allowCount++;
} else {
denyCount++;
}
System.out.println("訪問許可權:" + isAllow);
Thread.sleep(1000);
}
System.out.println("報告:");
System.out.println("漏斗容量:" + capacity);
System.out.println("漏斗流動速率:" + allowQuota + "次/" + perSecond + "秒");
System.out.println("測試次數=" + testAccessCount);
System.out.println("允許次數=" + allowCount);
System.out.println("拒絕次數=" + denyCount);
}
/**
* 根據給定的漏斗引數檢查是否允許訪問
*
* @param username 使用者名稱
* @param action 操作
* @param capacity 漏斗容量
* @param allowQuota 每單個單位時間允許的流量
* @param perSecond 單位時間(秒)
* @return 是否允許訪問
*/
public boolean isActionAllowed(String username, String action, int capacity, int allowQuota, int perSecond) {
String key = "funnel:" + action + ":" + username;
if (!funnelMap.containsKey(key)) {
funnelMap.put(key, new Funnel(capacity, allowQuota, perSecond));
}
Funnel funnel = funnelMap.get(key);
return funnel.watering(1);
}
private static class Funnel {
private int capacity;
private float leakingRate;
private int leftQuota;
private long leakingTs;
public Funnel(int capacity, int count, int perSecond) {
this.capacity = capacity;
// 因為計算使用毫秒為單位的
perSecond *= 1000;
this.leakingRate = (float) count / perSecond;
}
/**
* 根據上次水流動的時間,騰出已流出的空間
*/
private void makeSpace() {
long now = System.currentTimeMillis();
long time = now - leakingTs;
int leaked = (int) (time * leakingRate);
if (leaked < 1) {
return;
}
leftQuota += leaked;
// 如果剩餘大於容量,則剩餘等於容量
if (leftQuota > capacity) {
leftQuota = capacity;
}
leakingTs = now;
}
/**
* 漏斗漏水
*
* @param quota 流量
* @return 是否有足夠的水可以流出(是否允許訪問)
*/
public boolean watering(int quota) {
makeSpace();
int left = leftQuota - quota;
if (left >= 0) {
leftQuota = left;
return true;
}
return false;
}
}
}
3. 計數器限流演算法
數器限流演算法也是比較常用的,主要用來限制總併發數,比如資料庫連線池大小、執行緒池大小、程式訪問併發數等都是使用計數器演算法。
public class CountRateLimiterDemo {
private static Semaphore semphore = new Semaphore(5);
public static void exec() {
if(semphore.getQueueLength()>100){
System.out.println("當前等待排隊的任務數大於100,請稍候再試...");
}
try {
semphore.acquire();
// 處理核心邏輯
TimeUnit.SECONDS.sleep(1);
System.out.println("--" + System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semphore.release();
}
}
}
使用Semaphore訊號量來控制併發執行的次數,如果超過域值訊號量,則進入阻塞佇列中排隊等待獲取訊號量進行執行。如果阻塞佇列中排隊的請求過多超出系統處理能力,則可以在拒絕請求。
分散式限流
實現原理其實很簡單。既然要達到分散式全侷限流的效果,那自然需要一個第三方元件來記錄請求的次數。
其中 Redis 就非常適合這樣的場景。
- 每次請求時將方法名進行md5加密後作為Key 寫入到 Redis 中,超時時間設定為 2 秒,Redis 將該 Key 的值進行自增。
- 當達到閾值時返回錯誤。
- 寫入 Redis 的操作用 Lua 指令碼來完成,利用 Redis 的單執行緒機制可以保證每個 Redis 請求的原子性
Lua指令碼準備
local val = redis.call('incr', KEYS[1])
local ttl = redis.call('ttl', KEYS[1])
redis.log(redis.LOG_NOTICE, "incr "..KEYS[1].." "..val);
if val == 1 then
redis.call('expire', KEYS[1], tonumber(ARGV[1]))
else
if ttl == -1 then
redis.call('expire', KEYS[1], tonumber(ARGV[1]))
end
end
if val > tonumber(ARGV[2]) then
return 0
end
return 1
RateLimiter.java
@Component
public class RateLimiter {
@Autowired
private RedisClient redisClient;
@Value("${redis.limit.expire}")
private int expire;
@Value("${redis.limit.request.count}")
private int reqCount;
@Value("${redis.limit.script.name}")
private String scriptName;
public Long limit(String key) {
return redisClient.eval(key, scriptName, 1, expire, reqCount);
}
}
RateLimitAspect.java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.lzhsite.core.exception.OverLimitException;
import com.lzhsite.core.utils.MD5Util;
import com.lzhsite.technology.redis.limit.RateLimiter;
/**
* Created by hao.g on 18/5/17.
*/
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RateLimiter rateLimiter;
@Before("@annotation(com.lzhsite.technology.redis.limit.RateLimit)")
public void before(JoinPoint pjp) throws Throwable {
Signature sig = pjp.getSignature();
if (!(sig instanceof MethodSignature)) {
throw new IllegalArgumentException("該註解只能用在方法上");
}
MethodSignature msig = (MethodSignature) sig;
String methodName = pjp.getTarget().getClass().getName() + "." + msig.getName();
String limitKey = MD5Util.md5(methodName);
if (rateLimiter.limit(limitKey) != 1){
throw new OverLimitException("觸發限流控制");
}
}
}
RateLimit.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
String value() default "";
}
@RestController
public class ShopOrderController {
private static final Logger LOGGER = LoggerFactory.getLogger(ShopOrderController.class);
@Autowired
private ShopOrderService shopOrderService;
@RequestMapping("/seckill")
@RateLimit
public String index(Long stockId) {
try{
if (shopOrderService.createOrder(stockId)){
LOGGER.info(ConstantUtil.SNATCH_DEAL_SUCCESS);
return ConstantUtil.SNATCH_DEAL_SUCCESS;
}
} catch (Exception e){
LOGGER.error(e.getMessage());
return e.getMessage();
}
return ConstantUtil.SYSTEM_EXCEPTION;
}
}
當然這只是利用 Redis 做了一個粗暴的計數器,如果想實現類似於上文中的令牌桶演算法可以基於 Lua 自行實現。
完整程式碼
參考文章
相關文章
- 分散式限流分散式
- Java併發:分散式應用限流 Redis + Lua 實踐Java分散式Redis
- Redis 應用-限流Redis
- 基於kubernetes的分散式限流分散式
- sbc(四)應用限流
- Sentinel分散式限流元件,SpringCloud Alibaba整合分散式元件SpringGCCloud
- Go 分散式令牌桶限流 + 兜底策略Go分散式
- redis實際應用-限流Redis
- 詳解Redisson分散式限流的實現原理Redis分散式
- springboot + aop + Lua分散式限流的最佳實踐Spring Boot分散式
- [分散式][高併發]限流的四種策略分散式
- 淺談限流元件的應用和設計原則元件
- 10000字,圖解分散式系統限流平臺Sentinel圖解分散式
- 分散式系統限流演算法分析與實現分散式演算法
- 【分散式架構】(10)---基於Redis元件的特性,實現一個分散式限流分散式架構Redis元件
- 【分散式限流】你被12306的驗證碼坑過麼?分散式
- 為什麼分散式限流會出現不均衡的情況?分散式
- [分散式限流] 滑動視窗演算法的 Golang 實現分散式演算法Golang
- 簡單的限流過濾器過濾器
- Nginx限流Nginx
- 一個輕量級的基於RateLimiter的分散式限流實現MIT分散式
- 分散式系統關注點——想通關「限流」?只要這一篇分散式
- kube-apiserver限流機制原理APIServer
- 微服務 - 概念 · 應用 · 通訊 · 授權 · 跨域 · 限流微服務跨域
- 令牌桶、漏斗、冷啟動限流在sentinel的應用
- spring cloud alibaba系列(二)Sentinel應用的限流管理SpringCloud
- 分享一個簡單的redis限流Redis
- 限流場景&限流方案的一些思考
- 如何利用redis來進行分散式叢集系統的限流設計Redis分散式
- Nginx分片限流Nginx
- 服務限流
- .Net Core:限流
- Nginx限流特技Nginx
- Guava RateLimiter限流GuavaMIT
- 限流的概念
- rate-limit 一款 java 開源漸進式分散式限流框架使用介紹MITJava分散式框架
- Sentinel實現限流,竟是如此的簡單!
- 賬號層級限流-搶白名單功能