spring boot:用redis+lua實現基於ip地址的分布式流量限制(限流/簡單計數器算法)(spring boot 2.2.0)


一,限流有哪些環節?

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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM