漏桶:漏桶可以看作是一個漏斗類似,水可以以任意速度流入,桶保存一定量的水,水以一定的速率流出。
令牌桶:桶會以一個恆定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。
從原理上看,令牌桶算法和漏桶算法是相反的,一個“進水”,一個是“漏水”。
在單機上的實現
漏桶
import java.time.LocalDateTime; /** * 漏桶 */ public class LeakyBucket { //流水速率 固定 private double rate; //桶的大小 private double burst; //最后更新時間 private int refreshTime; //private Long refreshTime; //桶里面的水量 private int water; public LeakyBucket(double rate,double burst){ this.rate=rate; this.burst=burst; } /** * 刷新桶的水量 */ private void refreshWater(){ //long now = System.currentTimeMillis(); //毫秒生成 LocalDateTime time=LocalDateTime.now(); //每秒生成 int now = time.getSecond(); //現在時間-上次更新的時間 中間花費的時間(秒)*流水速率=流水量(處理的請求的數量) 通過上次水總量減去流水量等於現在的水量 //如果流水量太多導致桶里都沒那么多水就應該置0, 所以通過math.max函數實現 water = (int)Math.max(0,water-(now-refreshTime)*rate); //更新上次時間 refreshTime = now; } /** * 獲取令牌 */ public synchronized boolean tryAcquire(){ //刷新桶的水量 refreshWater(); //如果桶的水量小於桶的容量就可以添加進來 if(water<burst){ water++; return true; }else { return false; } } }
import java.util.concurrent.CountDownLatch;public class LeakyBucketTest { public static LeakyBucket leakyBucket = new LeakyBucket(10,100); public static void main(String[] args) {long start = System.currentTimeMillis(); for (int i=0;i<10;i++){ new Thread(new Runnable() { @Override public void run() { System.out.println(leakyBucket.tryAcquire()); } }).start(); } System.out.println("總花費:"+(System.currentTimeMillis()-start)); System.out.println("線程執行完畢"); } }
令牌桶
Google開源項目Guava中的RateLimiter使用的就是令牌桶控制算法,所以我們直接使用Guava即可實現。
加入依賴
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>21.0</version> </dependency>
import com.google.common.util.concurrent.RateLimiter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; /** * 令牌桶 */ public class TokenBucket { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); /** * permitsPerSecond為每秒生成的令牌 * */ /** 平衡穩定 * * 創建一個穩定輸出令牌的RateLimiter,保證了平均每秒不超過permitsPerSecond個請求 * * 當請求到來的速度超過了permitsPerSecond,保證每秒只處理permitsPerSecond個請求 * * 當這個RateLimiter使用不足(即請求到來速度小於permitsPerSecond),會囤積最多permitsPerSecond個請求 */ /**平衡預熱 * 創建一個穩定輸出令牌的RateLimiter,保證了平均每秒不超過permitsPerSecond個請求 * 還包含一個熱身期(warmup period),熱身期內,RateLimiter會平滑的將其釋放令牌的速率加大,直到起達到最大速率 * 同樣,如果RateLimiter在熱身期沒有足夠的請求(unused),則起速率會逐漸降低到冷卻狀態 * 設計這個的意圖是為了滿足那種資源提供方需要熱身時間,而不是每次訪問都能提供穩定速率的服務的情況(比如帶緩存服務,需要定期刷新緩存的) * 參數warmupPeriod和unit決定了其從冷卻狀態到達最大速率的時間 */ private static final RateLimiter rateLimiter = RateLimiter.create(10,2L, TimeUnit.SECONDS); //private static final RateLimiter rateLimiter = RateLimiter.create(10); /** * tryAcquire嘗試獲取permit,默認超時時間是0,意思是拿不到就立即返回false * @return */ public String sayHello(){ if(rateLimiter.tryAcquire()){ //一次拿一個 System.out.println(sdf.format(new Date())); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }else { return "no"; } return "hello"; } /** * acquire拿不到就等待,拿到為止 * @return */ public String sayHi(){ rateLimiter.acquire(1); //一次拿5個 意思就是生成10個令牌才去全部拿去給一個請求 System.out.println(sdf.format(new Date())); return "hi"; } }
import java.util.concurrent.CountDownLatch;public class LeakyBucketTest { private static TokenBucket tokenBucket = new TokenBucket(); public static void main(String[] args) {long start = System.currentTimeMillis(); for (int i=0;i<10;i++){ new Thread(new Runnable() { @Override public void run() { System.out.println(tokenBucket.sayHi()); } }).start(); } System.out.println("總花費:"+(System.currentTimeMillis()-start)); System.out.println("線程執行完畢"); } }
區別:
漏桶
漏桶的出水速度是恆定的,那么意味着如果瞬時大流量的話,將有大部分請求被丟棄掉(也就是所謂的溢出)。
令牌桶
生成令牌的速度是恆定的,而請求去拿令牌是沒有速度限制的。這意味,面對瞬時大流量,該算法可以在短時間內請求拿到大量令牌,而且拿令牌的過程並不是消耗很大的事情。
最后,不論是對於令牌桶拿不到令牌被拒絕,還是漏桶的水滿了溢出,都是為了保證大部分流量的正常使用,而犧牲掉了少部分流量,這是合理的,如果因為極少部分流量需要保證的話,那么就可能導致系統達到極限而掛掉,得不償失。