您好,欢迎访问一九零五行业门户网

SpringBoot+Redis+Lua分布式限流如何实现

redis支持lua脚本的主要优势
lua脚本的融合将使redis数据库产生更多的使用场景,迸发更多新的优势:
高效性:减少网络开销及时延,多次redis服务器网络请求的操作,使用lua脚本可以用一个请求完成
数据可靠性:redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
复用性:lua脚本执行后会永久存储在redis服务器端,其他客户端可以直接复用
可嵌入性:可嵌入java,c#等多种编程语言,支持不同操作系统跨平台交互
简单强大:小巧轻便,资源占用率低,支持过程化和对象化的编程语言
自己也是第一次在工作中使用lua这种语言,记录一下
创建lua文件req_ratelimit.lua
local key = keys[1] --限流keylocal limitcount = tonumber(argv[1]) --限流大小local limittime = tonumber(argv[2]) --限流时间local current = redis.call('get', key);if current then if current + 1 > limitcount then --如果超出限流大小 return 0 else redis.call("incrby", key,"1") return current + 1 endelse redis.call("set", key,"1") redis.call("expire", key,limittime) return 1end
自定义注解ratelimiter
package com.shinedata.ann; import java.lang.annotation.elementtype;import java.lang.annotation.retention;import java.lang.annotation.retentionpolicy;import java.lang.annotation.target; @target({elementtype.type, elementtype.method})@retention(retentionpolicy.runtime)public @interface ratelimiter { /** * 限流唯一标识 * @return */ string key() default "rate.limit:"; /** * 限流时间 * @return */ int time() default 1; /** * 限流次数 * @return */ int count() default 100; /** *是否限制ip,默认 否 * @return */ boolean restrictionsip() default false;}
定义切面ratelimiteraspect
package com.shinedata.aop; import com.shinedata.ann.ratelimiter;import com.shinedata.config.redis.redisutils;import com.shinedata.exception.ratelimiterexception;import org.apache.commons.lang3.stringutils;import org.aspectj.lang.proceedingjoinpoint;import org.aspectj.lang.annotation.around;import org.aspectj.lang.annotation.aspect;import org.aspectj.lang.reflect.methodsignature;import org.slf4j.logger;import org.slf4j.loggerfactory;import org.springframework.beans.factory.annotation.autowired;import org.springframework.context.annotation.configuration;import org.springframework.core.io.classpathresource;import org.springframework.data.redis.core.redistemplate;import org.springframework.data.redis.core.script.defaultredisscript;import org.springframework.scripting.support.resourcescriptsource;import org.springframework.stereotype.component;import org.springframework.web.context.request.requestcontextholder;import org.springframework.web.context.request.servletrequestattributes; import javax.annotation.postconstruct;import javax.servlet.http.httpservletrequest;import java.io.serializable;import java.lang.reflect.method;import java.util.collections;import java.util.list; /** * @classname ratelimiteraspect * @author yupanpan * @date 2020/5/6 13:46 */@aspect@componentpublic class ratelimiteraspect { private final logger logger = loggerfactory.getlogger(this.getclass()); private static threadlocal<string> ipthreadlocal=new threadlocal(); private defaultredisscript<number> redisscript; @postconstruct public void init(){ redisscript = new defaultredisscript<number>(); redisscript.setresulttype(number.class); redisscript.setscriptsource(new resourcescriptsource(new classpathresource("redis/req_ratelimit.lua"))); } @around("@annotation(com.shinedata.ann.ratelimiter)") public object interceptor(proceedingjoinpoint joinpoint) throws throwable { try { methodsignature signature = (methodsignature) joinpoint.getsignature(); method method = signature.getmethod(); class<?> targetclass = method.getdeclaringclass(); ratelimiter ratelimit = method.getannotation(ratelimiter.class); if (ratelimit != null) { httpservletrequest request = ((servletrequestattributes) requestcontextholder.getrequestattributes()).getrequest(); boolean restrictionsip = ratelimit.restrictionsip(); if(restrictionsip){ ipthreadlocal.set(getipaddr(request)); } stringbuffer stringbuffer = new stringbuffer(); stringbuffer.append(ratelimit.key()); if(stringutils.isnotblank(ipthreadlocal.get())){ stringbuffer.append(ipthreadlocal.get()).append("-"); } stringbuffer.append("-").append(targetclass.getname()).append("- ").append(method.getname()); list<string> keys = collections.singletonlist(stringbuffer.tostring()); number number = redisutils.execute(redisscript, keys, ratelimit.count(), ratelimit.time()); if (number != null && number.intvalue() != 0 && number.intvalue() <= ratelimit.count()) { logger.info("限流时间段内访问第:{} 次", number.tostring()); return joinpoint.proceed(); }else { logger.error("已经到设置限流次数,当前次数:{}",number.tostring()); throw new ratelimiterexception("服务器繁忙,请稍后再试"); } } else { return joinpoint.proceed(); } }finally { ipthreadlocal.remove(); } } public static string getipaddr(httpservletrequest request) { string ipaddress = null; try { ipaddress = request.getheader("x-forwarded-for"); if (ipaddress == null || ipaddress.length() == 0 || "unknown".equalsignorecase(ipaddress)) { ipaddress = request.getheader("proxy-client-ip"); } if (ipaddress == null || ipaddress.length() == 0 || "unknown".equalsignorecase(ipaddress)) { ipaddress = request.getheader("wl-proxy-client-ip"); } if (ipaddress == null || ipaddress.length() == 0 || "unknown".equalsignorecase(ipaddress)) { ipaddress = request.getremoteaddr(); } // 对于通过多个代理的情况,第一个ip为客户端真实ip,多个ip按照','分割 if (ipaddress != null && ipaddress.length() > 15) { // "***.***.***.***".length()= 15 if (ipaddress.indexof(",") > 0) { ipaddress = ipaddress.substring(0, ipaddress.indexof(",")); } } } catch (exception e) { ipaddress = ""; } return ipaddress; }}
spring data redis提供了defaultredisscript来使用lua和redis进行交互,具体的详情网上很多文章,这里使用threadlocal是因为ip存在可变的,保证自己的线程的ip不会被其他线程所修改,切记要最后清理threadlocal,防止内存泄漏
redisutils工具类(方法太多,只展示execute方法)
package com.shinedata.config.redis; import org.checkerframework.checker.units.qual.k;import org.springframework.beans.factory.annotation.autowired;import org.springframework.beans.factory.annotation.qualifier;import org.springframework.data.redis.core.redistemplate;import org.springframework.data.redis.core.script.defaultredisscript;import org.springframework.data.redis.core.script.redisscript;import org.springframework.stereotype.component;import org.springframework.util.collectionutils; import javax.annotation.postconstruct;import java.util.list;import java.util.map;import java.util.set;import java.util.concurrent.timeunit; /** * @classname redisutils * @author yupanpan * @date 2019/11/20 13:38 */@componentpublic class redisutils { @autowired @qualifier("redistemplate") private redistemplate<string, object> redistemplate; private static redisutils redisutils; @postconstruct public void init() { redisutils = this; redisutils.redistemplate = this.redistemplate; } public static number execute(defaultredisscript<number> script, list keys, object... args) { return redisutils.redistemplate.execute(script, keys,args); }}
自己配置的redistemplate
package com.shinedata.config.redis; import org.apache.log4j.logmanager;import org.apache.log4j.logger;import org.springframework.beans.factory.annotation.qualifier;import org.springframework.context.annotation.bean;import org.springframework.context.annotation.configuration;import org.springframework.data.redis.connection.redisconnectionfactory;import org.springframework.data.redis.connection.jedis.jedisconnectionfactory;import org.springframework.data.redis.core.redistemplate;import org.springframework.data.redis.serializer.genericjackson2jsonredisserializer;import org.springframework.data.redis.serializer.stringredisserializer;import redis.clients.jedis.jedispoolconfig; /** * @classname redisconfig * @author yupanpan * @date 2019/11/20 13:26 */@configurationpublic class redisconfig extends redisproperties{ protected logger log = logmanager.getlogger(redisconfig.class); /** * jedispoolconfig 连接池 * @return */ @bean("jedispoolconfig") public jedispoolconfig jedispoolconfig() { jedispoolconfig jedispoolconfig = new jedispoolconfig(); // 最大空闲数 jedispoolconfig.setmaxidle(500); jedispoolconfig.setminidle(100); // 连接池的最大数据库连接数 jedispoolconfig.setmaxtotal(6000); // 最大建立连接等待时间 jedispoolconfig.setmaxwaitmillis(5000); // 逐出连接的最小空闲时间 默认1800000毫秒(30分钟) jedispoolconfig.setminevictableidletimemillis(100); // 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3// jedispoolconfig.setnumtestsperevictionrun(numtestsperevictionrun); // 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 jedispoolconfig.settimebetweenevictionrunsmillis(600); // 是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个 jedispoolconfig.settestonborrow(true); // 在空闲时检查有效性, 默认false jedispoolconfig.settestwhileidle(false); return jedispoolconfig; } /** * jedisconnectionfactory * @param jedispoolconfig */ @bean("jedisconnectionfactory") public jedisconnectionfactory jedisconnectionfactory(@qualifier("jedispoolconfig")jedispoolconfig jedispoolconfig) { jedisconnectionfactory jedisconnectionfactory = new jedisconnectionfactory(jedispoolconfig); // 连接池 jedisconnectionfactory.setpoolconfig(jedispoolconfig); // ip地址 jedisconnectionfactory.sethostname(redishost); // 端口号 jedisconnectionfactory.setport(redisport); // 如果redis设置有密码 jedisconnectionfactory.setpassword(redispassword); // 客户端超时时间单位是毫秒 jedisconnectionfactory.settimeout(10000); return jedisconnectionfactory; } /** * 实例化 redistemplate 对象代替原有的redistemplate<string, string> * @return */ @bean("redistemplate") public redistemplate<string, object> functiondomainredistemplate(@qualifier("jedisconnectionfactory") redisconnectionfactory redisconnectionfactory) { redistemplate<string, object> redistemplate = new redistemplate<>(); initdomainredistemplate(redistemplate, redisconnectionfactory); return redistemplate; } /** * 设置数据存入 redis 的序列化方式 * @param redistemplate * @param factory */ private void initdomainredistemplate(redistemplate<string, object> redistemplate, redisconnectionfactory factory) { // 如果不配置serializer,那么存储的时候缺省使用string,比如如果用user类型存储,那么会提示错误user can't cast // to string! redistemplate.setkeyserializer(new stringredisserializer()); redistemplate.sethashkeyserializer(new stringredisserializer()); redistemplate.sethashvalueserializer(new genericjackson2jsonredisserializer()); redistemplate.setvalueserializer(new genericjackson2jsonredisserializer()); // 开启事务/true必须手动释放连接,false会自动释放连接 如果调用方有用@transactional做事务控制,可以开启事务,spring会处理连接问题 redistemplate.setenabletransactionsupport(false); redistemplate.setconnectionfactory(factory); }}
全局controller异常处理globalexceptionhandler
package com.shinedata.exception; import com.fasterxml.jackson.databind.jsonmappingexception;import com.shinedata.util.resultdata;import org.apache.commons.lang3.stringutils;import org.slf4j.logger;import org.slf4j.loggerfactory;import org.springframework.http.httpstatus;import org.springframework.web.bind.annotation.exceptionhandler;import org.springframework.web.bind.annotation.responsestatus;import org.springframework.web.bind.annotation.restcontrolleradvice; @restcontrolleradvicepublic class globalexceptionhandler { private logger logger = loggerfactory.getlogger(globalexceptionhandler.class); @exceptionhandler(value = ratelimiterexception.class) @responsestatus(httpstatus.ok) public resultdata runtimeexceptionhandler(ratelimiterexception e) { logger.error("系统错误:", e); return resultdata.getresulterror(stringutils.isnotblank(e.getmessage()) ? e.getmessage() : "处理失败"); } @exceptionhandler(value = exception.class) @responsestatus(httpstatus.ok) public resultdata runtimeexceptionhandler(runtimeexception e) { throwable cause = e.getcause(); logger.error("系统错误:", e); logger.error(e.getmessage()); if (cause instanceof jsonmappingexception) { return resultdata.getresulterror("参数错误"); } return resultdata.getresulterror(stringutils.isnotblank(e.getmessage()) ? e.getmessage() : "处理失败"); } }
使用就很简单了,一个注解搞定
补充:优化了lua为
local key = keys[1]local limitcount = tonumber(argv[1])local limittime = tonumber(argv[2])local current = redis.call('get', key);if current then redis.call("incrby", key,"1") return current + 1else redis.call("set", key,"1") redis.call("expire", key,limittime) return 1end
以上就是springboot+redis+lua分布式限流如何实现的详细内容。
其它类似信息

推荐信息