Redis分布式限流組件設(shè)計(jì)與使用實(shí)例
本文主要講解基于 自定義注解+Aop+反射+Redis+Lua表達(dá)式 實(shí)現(xiàn)的限流設(shè)計(jì)方案。實(shí)現(xiàn)的限流設(shè)計(jì)與實(shí)際使用。
1.背景
在互聯(lián)網(wǎng)開(kāi)發(fā)中經(jīng)常遇到需要限流的場(chǎng)景一般分為兩種
- 業(yè)務(wù)場(chǎng)景需要(比如:5分鐘內(nèi)發(fā)送驗(yàn)證碼不超過(guò)xxx次);
- 對(duì)流量大的功能流量削峰;
一般我們衡量系統(tǒng)處理能力的指標(biāo)是每秒的QPS或者TPS,假設(shè)系統(tǒng)每秒的流量閾值是2000,
理論上第2001個(gè)請(qǐng)求進(jìn)來(lái)時(shí),那么這個(gè)請(qǐng)求就需要被限流。
本文演示項(xiàng)目使用的是 SpringBoot 項(xiàng)目,項(xiàng)目構(gòu)建以及其他配置,這里不做演示。文末附限流Demo源碼
2.Redis計(jì)數(shù)器限流設(shè)計(jì)
本文演示項(xiàng)目使用的是 SpringBoot 項(xiàng)目,這里僅挑選了重點(diǎn)實(shí)現(xiàn)代碼展示,
項(xiàng)目構(gòu)建以及其他配置,這里不做演示,詳細(xì)配置請(qǐng)參考源碼demo工程。
2.1Lua腳本
Lua 是一種輕量小巧的腳本語(yǔ)言可以理解為就是一組命令。
使用Redis的計(jì)數(shù)器達(dá)到限流的效果,表面上Redis自帶命令多個(gè)組合也可以支持了,那為什么還要用Lua呢?
因?yàn)橐WC原子性,這也是使用redis+Lua表達(dá)式原因,一組命令要么全成功,要么全失敗。
相比Redis事務(wù),Lua腳本的優(yōu)點(diǎn):
- 減少網(wǎng)絡(luò)開(kāi)銷:多個(gè)請(qǐng)求通過(guò)腳本一次發(fā)送,減少網(wǎng)絡(luò)延遲
- 原子操作:將腳本作為一個(gè)整體執(zhí)行,中間不會(huì)插入其他命令,無(wú)需使用事務(wù)
- 復(fù)用:客戶端發(fā)送的腳本永久存在redis中,其他客戶端可以復(fù)用腳本
- 可嵌入性:可嵌入JAVA,C#等多種編程語(yǔ)言,支持不同操作系統(tǒng)跨平臺(tái)交互
實(shí)現(xiàn)限流Lua腳本示例
# 定義計(jì)數(shù)變量 local count # 獲取調(diào)用腳本時(shí)傳入的第一個(gè)key值(用作限流的 key) count = redis.call('get',KEYS[1]) # 限流最大值比較,若超過(guò)最大值,則直接返回 if count and tonumber(count) > tonumber(ARGV[1]) then return count; end # incr 命令 執(zhí)行計(jì)算器累加 count = redis.call('incr',KEYS[1]) # 從第一次調(diào)用開(kāi)始限流,并設(shè)置失效時(shí)間 if tonumber(count) == 1 then redis.call('expire',KEYS[1],ARGV[2]) end return count;
參數(shù)說(shuō)明
- KEYS[1] - redis的Key
- ARGV[1] - 限流次數(shù)
- ARGV[2] - 失效時(shí)間
2.2自定義注解
支持范圍:任意接口
/** * 描述: 限流注解 * * @author 程序員小強(qiáng) **/ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { /** * 限流唯一標(biāo)示 key * 若同時(shí)使用 keyFiled 則當(dāng)前 key作為前綴 */ String key(); /** * 限流時(shí)間-單位:秒數(shù) * 默認(rèn) 60s */ int time() default 60; /** * 限流次數(shù) * 失效時(shí)間段內(nèi)最大放行次數(shù) */ int count(); /** * 可作為限流key-參數(shù)類中屬性名,動(dòng)態(tài)值 * 示例:phone、userId 等 */ String keyField() default ""; /** * 超過(guò)最大訪問(wèn)次數(shù)后的,提示內(nèi)容 */ String msg() default "over the max request times please try again"; }
屬性介紹
- key - 必填,限流key唯一標(biāo)識(shí),redis存儲(chǔ)key
- time -過(guò)期時(shí)間,單位 秒,默認(rèn)60s
- count - 必填,失效時(shí)間段內(nèi)最大放行次數(shù)
- keyField - 動(dòng)態(tài)限流key,比如參數(shù)是一個(gè)自定義的類,里面有屬性u(píng)serId 等??梢允褂胟eyField=“userId”,
這樣生成的key為參數(shù)中userId的值。一般與key屬性組合使用。不支持java基本類型參數(shù),
僅支持參數(shù)是一個(gè)對(duì)象的接口。
msg - 超過(guò)限流的提示內(nèi)容
示例:
@RateLimit(key = "limit-phone-key", time = 300, count = 10, keyField = "phone", msg = "5分鐘內(nèi),驗(yàn)證碼最多發(fā)送10次")
含義 - 5分鐘內(nèi)根據(jù)手機(jī)號(hào)限流10次
RedisKey- limit-phone-key:后面拼接的是參數(shù)中phone的值。
2.3限流組件
這里用的是jedis客戶端,配置就不列在這里的,詳見(jiàn)源碼,文末附源碼地址
/** * Redis限流組件 * * @author 程序員小強(qiáng) */ @Component public class RedisRateLimitComponent { private static final Logger logger = LoggerFactory.getLogger(RedisRateLimitComponent.class); private JedisPool jedisPool; @Autowired public RedisRateLimitComponent(JedisPool jedisPool) { this.jedisPool = jedisPool; } /** * 限流方法 * 1.執(zhí)行 lua 表達(dá)式 * 2.通過(guò) lua 表達(dá)式實(shí)現(xiàn)-限流計(jì)數(shù)器 * * @param redisKey * @param time 超時(shí)時(shí)間-秒數(shù) * @param rateLimitCount 限流次數(shù) */ public Long rateLimit(String redisKey, Integer time, Integer rateLimitCount) { Jedis jedis = null; try { jedis = jedisPool.getResource(); Object obj = jedis.evalsha(jedis.scriptLoad(this.buildLuaScript()), Collections.singletonList(redisKey), Arrays.asList(String.valueOf(rateLimitCount), String.valueOf(time))); return Long.valueOf(obj.toString()); } catch (JedisException ex) { logger.error("[ executeLua ] >> messages:{}", ex.getMessage(), ex); throw new RateLimitException("[ RedisRateLimitComponent ] >> jedis run lua script exception" + ex.getMessage()); } finally { if (jedis != null) { if (jedis.isConnected()) { jedis.close(); } } } } /** * 構(gòu)建lua 表達(dá)式 * KEYS[1] -- 參數(shù)key * ARGV[1]-- 失效時(shí)間段內(nèi)最大放行次數(shù) * ARGV[2]-- 失效時(shí)間|秒 */ private String buildLuaScript() { StringBuilder luaBuilder = new StringBuilder(); //定義變量 luaBuilder.append("local count"); //獲取調(diào)用腳本時(shí)傳入的第一個(gè)key值(用作限流的 key) luaBuilder.append("\ncount = redis.call('get',KEYS[1])"); // 獲取調(diào)用腳本時(shí)傳入的第一個(gè)參數(shù)值(限流大?。?- 調(diào)用不超過(guò)最大值,則直接返回 luaBuilder.append("\nif count and tonumber(count) > tonumber(ARGV[1]) then"); luaBuilder.append("\nreturn count;"); luaBuilder.append("\nend"); //執(zhí)行計(jì)算器自增 luaBuilder.append("\ncount = redis.call('incr',KEYS[1])"); //從第一次調(diào)用開(kāi)始限流 luaBuilder.append("\nif tonumber(count) == 1 then"); //設(shè)置過(guò)期時(shí)間 luaBuilder.append("\nredis.call('expire',KEYS[1],ARGV[2])"); luaBuilder.append("\nend"); luaBuilder.append("\nreturn count;"); return luaBuilder.toString(); } }
2.4限流切面實(shí)現(xiàn)
/** * 描述:限流切面實(shí)現(xiàn) * * @author 程序員小強(qiáng) **/ @Aspect @Configuration public class RateLimitAspect { private static final Logger logger = LoggerFactory.getLogger(RateLimitAspect.class); private RedisRateLimitComponent redisRateLimitComponent; @Autowired public RateLimitAspect(RedisRateLimitComponent redisRateLimitComponent) { this.redisRateLimitComponent = redisRateLimitComponent; } /** * 匹配所有使用以下注解的方法 * * @see RateLimit */ @Pointcut("@annotation(com.example.ratelimit.annotation.RateLimit)") public void pointCut() { } @Around("pointCut()&&@annotation(rateLimit)") public Object logAround(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodName = signature.getMethod().getName(); //組裝限流key String rateLimitKey = this.getRateLimitKey(joinPoint, rateLimit); //限流組件-通過(guò)計(jì)數(shù)方式限流 Long count = redisRateLimitComponent.rateLimit(rateLimitKey, rateLimit.time(), rateLimit.count()); logger.debug("[ RateLimit ] method={},rateLimitKey={},count={}", methodName, rateLimitKey, count); if (null != count && count.intValue() <= rateLimit.count()) { //未超過(guò)限流次數(shù)-執(zhí)行業(yè)務(wù)方法 return joinPoint.proceed(); } else { //超過(guò)限流次數(shù) logger.info("[ RateLimit ] >> over the max request times method={},rateLimitKey={},currentCount={},rateLimitCount={}", methodName, rateLimitKey, count, rateLimit.count()); throw new RateLimitException(rateLimit.msg()); } } /** * 獲取限流key * 默認(rèn)取 RateLimit > key 屬性值 * 若設(shè)置了 keyField 則從參數(shù)中獲取該字段的值拼接到key中 * 示例:user_phone_login_max_times:13235777777 * * @param joinPoint * @param rateLimit */ private String getRateLimitKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) { String fieldName = rateLimit.keyField(); if ("".equals(fieldName)) { return rateLimit.key(); } //處理自定義-參數(shù)名-動(dòng)態(tài)屬性key StringBuilder rateLimitKeyBuilder = new StringBuilder(rateLimit.key()); for (Object obj : joinPoint.getArgs()) { if (null == obj) { continue; } //過(guò)濾基本類型參數(shù) if (ReflectionUtil.isBaseType(obj.getClass())) { continue; } //屬性值 Object fieldValue = ReflectionUtil.getFieldByClazz(fieldName, obj); if (null != fieldValue) { rateLimitKeyBuilder.append(":").append(fieldValue.toString()); break; } } return rateLimitKeyBuilder.toString(); } }
由于演示項(xiàng)目中做了統(tǒng)一異常處理
在限流切面這里未做異常捕獲,若超過(guò)最大限流次數(shù)會(huì)拋出自定義限流異常??梢愿鶕?jù)業(yè)務(wù)自行處理。
/** * 反射工具 * * @author 程序員小強(qiáng) */ public class ReflectionUtil { private static final Logger logger = LoggerFactory.getLogger(ReflectionUtil.class); /** * 根據(jù)屬性名獲取屬性元素, * 包括各種安全范圍和所有父類 * * @param fieldName * @param object * @return */ public static Object getFieldByClazz(String fieldName, Object object) { Field field = null; Class<?> clazz = object.getClass(); try { for (; clazz != Object.class; clazz = clazz.getSuperclass()) { try { //子類中查詢不到屬性-繼續(xù)向父類查 field = clazz.getDeclaredField(fieldName); } catch (NoSuchFieldException ignored) { } } if (null == field) { return null; } field.setAccessible(true); return field.get(object); } catch (Exception e) { //通過(guò)反射獲取 屬性值失敗 logger.error("[ ReflectionUtil ] >> [getFieldByClazz] fieldName:{} ", fieldName, e); } return null; } /** * 判斷對(duì)象屬性是否是基本數(shù)據(jù)類型,包括是否包括string | BigDecimal * * @param clazz * @return */ public static boolean isBaseType(Class clazz) { if (null == clazz) { return false; } //基本類型 if (clazz.isPrimitive()) { return true; } //String if (clazz.equals(String.class)) { return true; } //Integer if (clazz.equals(Integer.class)) { return true; } //Boolean if (clazz.equals(Boolean.class)) { return true; } //BigDecimal if (clazz.equals(BigDecimal.class)) { return true; } //Byte if (clazz.equals(Byte.class)) { return true; } //Long if (clazz.equals(Long.class)) { return true; } //Double if (clazz.equals(Double.class)) { return true; } //Float if (clazz.equals(Float.class)) { return true; } //Character if (clazz.equals(Character.class)) { return true; } //Short return clazz.equals(Short.class); } }
3.測(cè)試一下
基本屬性已經(jīng)配置好了,寫個(gè)接口測(cè)試一下。
3.1方法限流示例
/** * 計(jì)數(shù)器 * 演示 demo 為了方便計(jì)數(shù) */ private static final AtomicInteger COUNTER = new AtomicInteger(); /** * 普通限流 * <p> * 30 秒中,可以訪問(wèn)10次 */ @RequestMapping("/limitTest") @RateLimit(key = "limit-test-key", time = 30, count = 10) public Response limitTest() { Map<String, Object> dataMap = new HashMap<>(); dataMap.put("date", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")); dataMap.put("times", COUNTER.incrementAndGet()); return Response.success(dataMap); }
3.2動(dòng)態(tài)入?yún)⑾蘖魇纠?/h3>
3.2.1場(chǎng)景一:5分鐘內(nèi),方法最多訪問(wèn)10次,根據(jù)入?yún)⑹謾C(jī)號(hào)限流
入?yún)㈩?/p>
public class UserPhoneCaptchaRateParam implements Serializable { private static final long serialVersionUID = -1L; private String phone; //省略 get/set }
private static final Map<String, AtomicInteger> COUNT_PHONE_MAP = new HashMap<>(); /** * 根據(jù)手機(jī)號(hào)限流-限制驗(yàn)證碼發(fā)送次數(shù) * <p> * 示例:5分鐘內(nèi),驗(yàn)證碼最多發(fā)送10次 */ @RequestMapping("/limitByPhone") @RateLimit(key = "limit-phone-key", time = 300, count = 10, keyField = "phone", msg = "5分鐘內(nèi),驗(yàn)證碼最多發(fā)送10次") public Response limitByPhone(UserPhoneCaptchaRateParam param) { Map<String, Object> dataMap = new HashMap<>(); dataMap.put("date", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")); if (COUNT_PHONE_MAP.containsKey(param.getPhone())) { COUNT_PHONE_MAP.get(param.getPhone()).incrementAndGet(); } else { COUNT_PHONE_MAP.put(param.getPhone(), new AtomicInteger(1)); } dataMap.put("times", COUNT_PHONE_MAP.get(param.getPhone()).intValue()); dataMap.put("reqParam", param); return Response.success(dataMap); }
3.2.2場(chǎng)景二:根據(jù)訂單ID限流
入?yún)㈩?/p>
@Data public class OrderRateParam implements Serializable { private static final long serialVersionUID = -1L; private String orderId; //省略 get\set }
private static final Map<String, AtomicInteger> COUNT_ORDER_MAP = new HashMap<>(); /** * 根據(jù)訂單ID限流示例 * <p> * 300 秒中,可以訪問(wèn)10次 */ @RequestMapping("/limitByOrderId") @RateLimit(key = "limit-order-key", time = 300, count = 10, keyField = "orderId", msg = "訂單飛走了,請(qǐng)稍后再試!") public Response limitByOrderId(OrderRateParam param) { Map<String, Object> dataMap = new HashMap<>(); dataMap.put("date", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")); if (COUNT_ORDER_MAP.containsKey(param.getOrderId())) { COUNT_ORDER_MAP.get(param.getOrderId()).incrementAndGet(); } else { COUNT_ORDER_MAP.put(param.getOrderId(), new AtomicInteger(1)); } dataMap.put("times", COUNT_ORDER_MAP.get(param.getOrderId()).intValue()); dataMap.put("reqParam", param); return Response.success(dataMap); }
4.其它擴(kuò)展
根據(jù)ip限流
在key中拼接IP即可;
5.源碼地址
到此這篇關(guān)于Redis分布式限流組件設(shè)計(jì)與使用實(shí)例的文章就介紹到這了,更多相關(guān)Redis分布式限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis中統(tǒng)計(jì)各種數(shù)據(jù)大小的方法
這篇文章主要介紹了Redis中統(tǒng)計(jì)各種數(shù)據(jù)大小的方法,本文使用PHP實(shí)現(xiàn)統(tǒng)計(jì)Redis內(nèi)存占用比較大的鍵,需要的朋友可以參考下2015-03-03Redis的數(shù)據(jù)類型和內(nèi)部編碼詳解
Redis是通過(guò)Key-Value的形式來(lái)組織數(shù)據(jù)的,而Key的類型都是String,而Value的類型可以有很多,在Redis中最通用的數(shù)據(jù)類型大致有這幾種:String、List、Set、Hash、Sorted Set,下面通過(guò)本文介紹Redis數(shù)據(jù)類型和內(nèi)部編碼,感興趣的朋友一起看看吧2024-04-04Redis數(shù)據(jù)庫(kù)分布式設(shè)計(jì)方案介紹
大家好,本篇文章主要講的是Redis數(shù)據(jù)庫(kù)分布式設(shè)計(jì)方案介紹,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-01-01