在項目中使用 (Redis + Lua)實現分佈式應用限流,基於自定義註解方式實現 實戰限流

今天講的 redis+lua 解決分佈式限流 任何架構使用。單體、集羣,分佈式都可以使用的分流方案實戰教程。

前面講一下單體項目,在項目中使用(guava的RateLimiter)基於自定義註解方式實現 實戰限流

個人推薦還是使用 redis+lua 解決分佈式限流,

微服務架構使用結合,基於Nginx的分佈式限流、基於網關層實現分佈式限流和基於Redis+Lua的分佈式限流,一起實現限流。

1、需要引入Redis的maven座標

<!--redis和 springboot集成的包 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.3.0.RELEASE</version>
</dependency>

redis 配置

spring:
  # Redis數據庫索引
  redis:
    database: 0
  # Redis服務器地址
    host: 127.0.0.1
  # Redis服務器連接端口
    port: 6379
  # Redis服務器連接密碼(默認爲空)
    password:
  # 連接池最大連接數(使用負值表示沒有限制)
    jedis:
      pool:
        max-active: 8
  # 連接池最大阻塞等待時間(使用負值表示沒有限制)
        max-wait: -1
  # 連接池中的最大空閒連接
        max-idle: 8
  # 連接池中的最小空閒連接
        min-idle: 0
  # 連接超時時間(毫秒)
    timeout: 10000

2、新建腳本放在該項目的 resources 目錄下,起名 limit.lua 即可,

注意需要放在字段有空格。不然會報錯。 

ERR Error compiling script (new function): user_script:1: unexpected symbol near 'ᅡ' 

local key =  KEYS[1] --限流KEY
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
  return 0
else  --請求數+1,並設置2秒過期
   redis.call("INCRBY", key,"1")
   redis.call("expire", key,"2")
   return  current + 1
end

解釋 Lua 腳本含義:

我們通過KEYS[1] 獲取傳入的key參數 
通過ARGV[1]獲取傳入的limit參數 
redis.call方法,從緩存中get和key相關的值,如果爲null那麼就返回0 
接着判斷緩存中記錄的數值是否會大於限制大小,如果超出表示該被限流,返回0 
如果未超過,那麼該key的緩存值+1,並設置過期時間爲1秒鐘以後,並返回緩存值+1

 

2、自定義限流注解

import java.lang.annotation.*;

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisRateLimiter {

   //往令牌桶放入令牌的速率
    double value() default  Double.MAX_VALUE;
    //獲取令牌的超時時間
    double limit() default  Double.MAX_VALUE;
}

2、自定義切面類 RedisLimiterAspect 類 ,修改掃描自己controller類

import com.imooc.annotation.RedisRateLimiter;
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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.assertj.core.util.Lists;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.List;

@Aspect
@Component
public class RedisLimiterAspect {
    @Autowired
    private HttpServletResponse response;

    /**
     * 注入redis操作類
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

     private DefaultRedisScript<List> redisScript;

    /**
     * 初始化 redisScript 類
     * 返回值爲 List
     */
    @PostConstruct
    public void init(){
        redisScript = new DefaultRedisScript<List>();
        redisScript.setResultType(List.class);
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
    }

    public final static Logger log = LoggerFactory.getLogger(RedisLimiterAspect.class);

    @Pointcut("execution( public * com.zz.controller.*.*(..))")
    public void pointcut(){

    }
    @Around("pointcut()")
    public Object process(ProceedingJoinPoint proceedingJoinPoint) throws  Throwable {
        MethodSignature  signature = (MethodSignature)proceedingJoinPoint.getSignature();
        //使用Java 反射技術獲取方法上是否有@RedisRateLimiter 註解類
        RedisRateLimiter redisRateLimiter = signature.getMethod().getDeclaredAnnotation(RedisRateLimiter.class);
        if(redisRateLimiter == null){
            //正常執行方法,執行正常業務邏輯
            return proceedingJoinPoint.proceed();
        }
        //獲取註解上的參數,獲取配置的速率
        double value = redisRateLimiter.value();
        double time = redisRateLimiter.limit();


        //list設置lua的keys[1]
        //取當前時間戳到單位秒
        String key = "ip:"+ System.currentTimeMillis() / 1000;

        List<String> keyList = Lists.newArrayList(key);

        //用戶Mpa設置Lua 的ARGV[1]
        //List<String> argList = Lists.newArrayList(String.valueOf(value));

        //調用腳本並執行
        List result = stringRedisTemplate.execute(redisScript, keyList, String.valueOf(value),String.valueOf(time));

        log.info("限流時間段內訪問第:{} 次", result.toString());

        //lua 腳本返回 "0" 表示超出流量大小,返回1表示沒有超出流量大小
        if(StringUtils.equals(result.get(0).toString(),"0")){
            //服務降級
            fullback();
            return null;
        }

        // 沒有限流,直接放行
        return proceedingJoinPoint.proceed();
    }

    /**
     * 服務降級方法
     */
    private  void  fullback(){
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer= response.getWriter();
            JSONObject o = new JSONObject();
            o.put("status",500);
            o.put("msg","Redis限流:請求太頻繁,請稍後重試!");
            o.put("data",null);
            writer.printf(o.toString()
            );

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(writer != null){
                writer.close();
            }
        }
    }
}

3、在需要限流的類添加註解: 

@RedisRateLimiter(value = 10, limit = 1) :表示一秒請求10併發請求,超過10 就提示請求請求頻繁。
import com.imooc.annotation.RedisRateLimiter;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@Api(value = "限流", tags = {"限流測試接口"})
@RequestMapping("limiter")
public class LimiterController {

    @ApiOperation(value = "Redis限流注解測試接口",notes = "Redis限流注解測試接口", httpMethod = "GET")
    @RedisRateLimiter(value = 10, limit = 1)
    @GetMapping("/redislimit")
    public IMOOCJSONResult redislimit(){

        System.out.println("Redis限流注解測試接口");
        return IMOOCJSONResult.ok();
    }


}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章