用java配合redis 在springboot上实现令牌桶算法

令牌桶算法配合 Redis 在 Java 中的应用令牌桶算法是一种常用的限流算法,适用于控制请求的频率,防止系统过载。结合 Redis 使用可以实现高效的分布式限流。

一.、引入依赖首先,需要在 pom.xml 文件中引入 spring-boot-starter-data-redis 依赖,这个依赖提供了与 Redis 交互的客户端和工具类。

XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

二.、配置 Redis 连接 (我随便用了application.properties ,也可以用 application.yml)

XML 复制代码
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
spring.redis.timeout=2000

三.、实现令牌桶算法

使用 Lua 脚本和 Redis 操作来实现令牌桶算法。在resources创建一个 Lua 脚本文件 request_rate_limiter.lua

Lua 复制代码
-- 获取到限流资源令牌数的key和响应时间戳的key
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
-- 分别获取填充速率、令牌桶容量、当前时间戳、消耗令牌数
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
-- 计算出失效时间,大概是令牌桶填满时间的两倍
local fill_time = capacity / rate
local ttl = math.floor(fill_time * 2)
-- 获取到最近一次的剩余令牌数,如果不存在说明令牌桶是满的
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
-- 上次消耗令牌的时间戳,不存在视为0
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
-- 计算出间隔时间
local delta = math.max(0, now - last_refreshed)
-- 剩余令牌数量 = "令牌桶容量" 和 "最后令牌数+(填充速率*时间间隔)"之间的最小值
local filled_tokens = math.min(capacity, last_tokens + (delta * rate))
-- 如果剩余令牌数量大于等于消耗令牌的数量则流量通过,否则不通过
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end
-- 更新令牌桶状态
if ttl > 0 then
  redis.call("setex", tokens_key, ttl, new_tokens)
  redis.call("setex", timestamp_key, ttl, now)
end
return allowed_num

四、创建一个 TokenBucket 类,使用 StringRedisTemplate 执行 Lua 脚本。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import javax.xml.crypto.Data;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

@Component
public class TokenBucket {

    private static final String TOKEN_BUCKET_KEY_PREFIX = "rate_limiter:";
    private static final String LUA_SCRIPT_PATH = "request_rate_limiter.lua";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public boolean tryAccess(String key, int limitCount, int refillRate) {
        String luaScript = loadLuaScript();

        // 使用加载的Lua脚本创建一个RedisScript 对象。  DefaultRedisScript 是Spring Data Redis提供的一个类,用于封装Lua脚本。
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);

        // TOKEN_BUCKET_KEY_PREFIX + key + ":tokens":用于存储令牌桶中的令牌数量。•
        // TOKEN_BUCKET_KEY_PREFIX + key + ":timestamp":用于存储上一次令牌桶被填充的时间戳。
        List<String> keys = Arrays.asList(TOKEN_BUCKET_KEY_PREFIX + key + ":tokens", TOKEN_BUCKET_KEY_PREFIX + key + ":timestamp");
        //执行Lua脚本
        /**
         * 使用  StringRedisTemplate 执行Lua脚本。execute方法的参数包括:
         * • redisScript  :Lua脚本对象。
         * • keys:Redis键列表。
         * • String.valueOf(refillRate):令牌桶的填充速率。
         * • String.valueOf(limitCount):令牌桶的最大容量。
         * • String.valueOf(System.currentTimeMillis() / 1000):当前时间戳(秒级)。
         * •  "1"  :请求的令牌数量(这里假设每次请求需要1个令牌)。
         */
        Long result = stringRedisTemplate.execute(
                redisScript,   //Lua脚本对象。
                keys,      //Redis键列表
                String.valueOf(refillRate),  //令牌桶的填充速率
                String.valueOf(limitCount),    //令牌桶的最大容量
                String.valueOf(System.currentTimeMillis() / 1000), //当前时间戳(秒级)
                "1"   //请求的令牌数量(这里假设每次请求需要1个令牌)。
        );
        return result != null && result == 1;
    }

    private String loadLuaScript() {
        InputStreamReader reader = null;
        try {
            //使用类加载器从指定路径 LUA_SCRIPT_PATH 获取资源流。我的lua文件在resources根目录下面
            //使用 InputStreamReader 将输入流 resourceStream 包装成一个字符流,并指定字符编码为UTF-8。这样可以确保读取的文件内容是正确的编码格式
            reader = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(LUA_SCRIPT_PATH), StandardCharsets.UTF_8);

             /*
                 创建一个Scanner对象,用于读取字符流。
                 useDelimiter("\\A"):设置分隔符为文件的开始标记 \\A  。这意味着Scanner会将整个文件内容视为一个单一的字符串。
                 next()  :读取并返回整个文件内容
            */
            return new java.util.Scanner(reader).useDelimiter("\\A").next();
        } catch (Exception e) {
            throw new RuntimeException("Failed to load Lua script", e);
        }
    }
}

五、测试

java 复制代码
package com.lhx.testany.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimiterController {

    @Autowired
    private  TokenBucket tokenBucket;

    @GetMapping("/test")
    public String testRateLimiter() {
      //为了测试,把最多设成2个令牌,每秒只生成1个令牌, 我快速在浏览器调用时,
      //生成的令牌,不足于被消耗,就会执行esle,这样就能做限流
        if (tokenBucket.tryAccess("api1", 2, 1)) {
            return "Request allowed";
        } else {
            return "Request denied due to rate limit";
        }
    }
}
相关推荐
Javatutouhouduan10 分钟前
Java全栈面试进阶宝典:内容全面,题目高频!
java·高并发·java面试·java面试题·后端开发·java程序员·java八股文
SEO-狼术24 分钟前
RAD Studio 13.1 Florence adds
java
ywf121534 分钟前
Spring Boot接收参数的19种方式
java·spring boot·后端
敲代码的瓦龙1 小时前
Java?面向对象三大特性!!!
java·开发语言
架构师沉默1 小时前
AI 写的代码,你敢上线吗?
java·后端·架构
野犬寒鸦1 小时前
Redis复习记录day1
服务器·开发语言·数据库·redis·缓存
骑龙赶鸭1 小时前
java开发项目中遇到的难点,面试!
java·开发语言·面试
NGC_66112 小时前
Java线程池七大核心参数介绍
java·开发语言