一,限流有哪些環節?
1,為什么要限流?
目的:通過對並發請求進行限速或者一個時間單位內的的請求進行限速,目的是保護系統可正常提供服務,避免被壓力太大無法響應服務.
如果達到限制速率則可以采取預定的處理:
例如:
拒絕服務(定向到錯誤頁面或返回錯誤提示信息)
排隊或等待(秒殺/評論/下單)
降級(只返回兜底數據或默認數據)
2,需要應用限流的環節
防火牆:firewalld/iptables層的限流,針對每台機器
接入層:nginx的limit_req模塊,對每單位時間的平均速率限流,針對某個站點或某個url
應用層:可以針對某個url或某個方法
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的說明
1,項目的原理:
如果僅僅是單機上對某個接口做限流,
可以直接使用google的guava包中的流量限制功能,
但如果是有多台機器上統一做限流,
則需要借助redis的功能
當后端接收到請求時,會把限流的方法名和ip做為key值保存到redis,
每次接收到請求,對key值加1,
當請求數量在指定時間內超過了限制數量,
則返回'訪問過於頻繁'的提示信息
2,項目在github上的地址:
https://github.com/liuhongdi/ratelimiter
3,項目的目錄結構:
三,lua代碼的說明:
ratelimit.lua
local key = KEYS[1] local limit = tonumber(KEYS[2]) local length = tonumber(KEYS[3]) --redis.log(redis.LOG_NOTICE,' length: '..length) local current = redis.call('GET', key) if current == false then --redis.log(redis.LOG_NOTICE,key..' is nil ') redis.call('SET', key,1) redis.call('EXPIRE',key,length) --redis.log(redis.LOG_NOTICE,' set expire end') return '1' else --redis.log(redis.LOG_NOTICE,key..' value: '..current) local num_current = tonumber(current) if num_current+1 > limit then return '0' else redis.call('INCRBY',key,1) return '1' end end
說明:
key:在redis中記錄訪問次數的index,在這里我們用method的名字加ip地址進行限制
limit: 單ip對此method最多可以訪問的次數
length: 限制次數生效的時長
原理:
key不存在時,新建一個key,value設置為1,並設定過期時間
如果key存在,看是否超過單位時間內允許訪問的最高次數,
如果超過,返回0,
如果不超過,返回1
說明:為什么使用lua腳本?
redis上的lua腳本的執行是原子性的,不存在多個線程的並發問題,
使用lua腳本能保證高並發時也不會出現超出流量限制
四,java代碼的說明:
1,RedisLuaUtil.java
@Service public class RedisLuaUtil { @Resource private StringRedisTemplate stringRedisTemplate; private static final Logger logger = LogManager.getLogger("bussniesslog"); /* run a lua script luaFileName: lua file name,no path keyList: list for redis key return 0: fail 1: success */ public String runLuaScript(String luaFileName,List<String> keyList) { DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/"+luaFileName))); redisScript.setResultType(String.class); String result = ""; String argsone = "none"; try { result = stringRedisTemplate.execute(redisScript, keyList,argsone); } catch (Exception e) { logger.error("發生異常",e); } return result; } }
說明:
DefaultRedisScript:負責封裝lua腳本
luaFileName: lua文件名
keyList: redis中的key列表,我們把參數放在這里面傳遞
stringRedisTemplate:負責執行腳本
argsone:值參數,我們傳一個空字串即可
2,RedisRateLimiterAspect,它負責調用RedisLuaUtil類,執行lua腳本:
/* * check is reach limit in time * run by lua * */ private boolean checkByRedis(RedisRateLimiter limit, String key) { List<String> keyList = new ArrayList(); keyList.add(key); keyList.add(String.valueOf(limit.count())); keyList.add(String.valueOf(limit.time())); String res = redisLuaUtil.runLuaScript("ratelimit.lua",keyList); System.out.println("------------------lua res:"+res); if (res.equals("1")) { return true; } else { return false; } }
說明:
keyList中我們添加了三個變量:
key: 在redis中記錄訪問次數的index,在這里我們用method的名字加ip地址進行限制
count: 同一個ip限制訪問的次數
time: 限制訪問的時間段
3,其他java代碼的說明:
RedisRateLimiter:定義了一個注解
RedisRateLimiterAspect:AOP的切面程序,使限流不侵入業務代碼
RateController: 控制器
在spring框架中使用AOP或Interceptor可以使通用的一些功能例如安全、檢驗等不影響業務代碼,
我們在這個例子中使用的是AOP,也可以選擇Interceptor,這里僅供參考
五,測試限流的效果:
1,查看controller中定義的值:
@RestController @RequestMapping("/rate") public class RateController { @RequestMapping("/redislimit") @RedisRateLimiter(count = 3, time = 1) public Object redisLimit() { return ServerResponseUtil.success(); } }
可以看到流量限制的值為:對redisLimit方法,同一個ip在1秒鍾內最多可訪問3次
2,用ab測試並發情況下的流量限制是否生效?
#-c:指定請求的並發數量
#-n:指定請求的總數量
[liuhongdi@localhost ~]$ ab -c 20 -n 20 http://127.0.0.1:8080/rate/redislimit
查看代碼運行中打印出的數據:
------------------lua res:1 ------------------lua res:1 ------------------lua res:1 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0
可見在20個並發中,只有3個是生效的,允許正常訪問,其他的超出了訪問的數量限制
六,查看spring boot的版本
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE)