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分布式限流如何实现的详细内容。
   
 
   