'# Java最全可以了,基于Redis和Lua实现分布式令牌桶限流,分布式架构+RPC+kafka+多线程
一、背景与问题
在分布式系统中,随着系统规模的扩大,流量控制成为保障系统稳定性的关键环节。传统的单体应用可以通过本地缓存实现简单的限流控制,但在分布式架构下,不同节点之间的状态同步成为难题。
以电商系统为例,当秒杀活动开始时,大量请求会同时涌入系统。若不进行有效控制,可能导致:
- 服务端资源耗尽
- 数据库连接池爆满
- 系统响应延迟激增
- 业务逻辑错误率上升
传统的限流方案如Nginx的令牌桶算法,仅适用于单节点服务。在分布式场景下,需要一种跨节点的限流机制。而Redis的原子操作和Lua脚本特性,恰好能解决分布式限流中的状态同步问题。
二、基本原理
令牌桶算法的核心思想是通过预分配令牌来控制流量。其核心参数包括:
- 容量(capacity):桶的最大容量
- 填充速率(rate):单位时间补充的令牌数
- 剩余令牌(tokens):当前桶中的令牌数量
在分布式场景下,需要通过Redis存储每个用户的令牌状态。由于Redis的单线程特性,使用Lua脚本可以保证操作的原子性,避免竞态条件。
Lua脚本的执行流程如下:
- 获取当前用户令牌数量
- 计算需要扣除的令牌数
- 更新令牌数量(不超过容量)
- 返回是否允许请求的布尔值
三、环境准备
技术栈要求
- Java 17+
- Redis 6.2+
- Kafka 3.0+
- Spring Boot 3.x
- Lettuce Redis客户端
依赖配置
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.2.4</version> </dependency> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>3.3.1</version> </dependency> </dependencies>
四、核心实现
1. Redis Lua脚本实现
-- 令牌桶限流Lua脚本
local key = KEYS[1] -- 用户标识
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 填充速率
local now = tonumber(ARGV[3]) -- 当前时间戳
local last = tonumber(redis.call('get', key) or 0)
local tokens = (now - last) * rate / 1000 + capacity
tokens = math.min(tokens, capacity)
-- 计算当前可用令牌数
local current_tokens = tokens - 1
-- 更新最后更新时间
local next_update = math.floor(now + (capacity - current_tokens) / rate * 1000)
if current_tokens >= 0 then
-- 允许请求
return {1, current_tokens, next_update}
else
-- 拒绝请求
return {0, current_tokens, next_update}
end逐段解释:
KEYS[1]表示用户标识,可以是用户ID或请求IPARGV[1]是桶容量,单位是令牌数ARGV[2]是填充速率,单位是令牌/秒ARGV[3]是当前时间戳(毫秒)last是上一次更新时间戳tokens计算当前可用的令牌数量current_tokens是扣除当前请求后剩余的令牌数next_update是下次更新时间
2. Java客户端实现
public class RedisRateLimiter {
private static final String LUA_SCRIPT = "local key = KEYS[1] \n" +
"local capacity = tonumber(ARGV[1]) \n" +
"local rate = tonumber(ARGV[2]) \n" +
"local now = tonumber(ARGV[3]) \n" +
"local last = tonumber(redis.call('get', key) or 0) \n" +
"local tokens = (now - last) * rate / 1000 + capacity \n" +
"tokens = math.min(tokens, capacity) \n" +
"local current_tokens = tokens - 1 \n" +
"local next_update = math.floor(now + (capacity - current_tokens) / rate * 1000) \n" +
"if current_tokens >= 0 then \n" +
" return {1, current_tokens, next_update} \n" +
"else \n" +
" return {0, current_tokens, next_update} \n" +
"end";
public boolean isAllowed(String userId, int capacity, double rate, long now) {
RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommandChannel<String, String, String> channel = connection.channel();
RedisScript<Long> script = RedisScript.of(LUA_SCRIPT, Long.class);
RedisCommand<String> command = RedisCommands.associate("EVAL", script);
List<String> keys = Arrays.asList(userId);
List<String> args = Arrays.asList(String.valueOf(capacity), String.valueOf(rate), String.valueOf(now));
Long result = channel.writeAndRead(command, keys, args);
connection.close();
redisClient.shutdown();
if (result != null) {
return result > 0;
}
return false;
}
}关键点说明:
- 使用Redis的EVAL命令执行Lua脚本
- 通过RedisScript定义Lua脚本
- 使用RedisCommandChannel进行通信
- 响应结果大于0表示允许请求
3. 限流策略配置
@Configuration
public class RateLimitConfig {
@Bean
public RedisRateLimiter rateLimiter() {
return new RedisRateLimiter();
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1", 6379));
}
}五、完整案例
1. 系统架构设计
+-------------------+ +-------------------+
| 用户请求 | | 分布式限流 |
| (Web/APP) | | (Redis+Lua) |
+---------+--------+ +-------------------+
| |
v v
+-------------------+ +-------------------+
| 路由网关 | | 业务服务 |
| (Spring Cloud Gateway) | | (Spring Boot) |
+-------------------+ +-------------------+
| |
v v
+-------------------+ +-------------------+
| Kafka消息队列 | | 数据库 |
| (消息缓冲) | | (MySQL/Redis) |
+-------------------+ +-------------------+2. 限流控制流程
- 用户发起请求 -> 路由网关 -> 分布式限流模块
- 分布式限流模块执行Redis+Lua限流逻辑
- 允许通过则继续处理请求
- 拒绝则返回429 Too Many Requests
- 通过的请求发送到Kafka消息队列
- 业务服务从Kafka消费消息进行处理
3. 完整代码示例
@RestController
@RequestMapping("/api")
public class RateLimitController {
@Autowired
private RedisRateLimiter rateLimiter;
@GetMapping("/test")
public ResponseEntity<String> testLimit(@RequestParam String userId) {
long now = System.currentTimeMillis();
int capacity = 100; // 桶容量
double rate = 10.0; // 填充速率(令牌/秒)
if (rateLimiter.isAllowed(userId, capacity, rate, now)) {
// 允许请求,发送到Kafka
kafkaProducer.send("rate_limit_queue", userId);
return ResponseEntity.ok("请求成功");
} else {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body("请求过多,请稍后再试");
}
}
}六、源码解析
1. Redis Lua脚本逐行分析
local key = KEYS[1] -- 获取用户标识
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 填充速率
local now = tonumber(ARGV[3]) -- 当前时间戳
local last = tonumber(redis.call('get', key) or 0) -- 上次更新时间KEYS[1]是用户标识,可以是用户ID或IP地址ARGV是参数列表,包含容量、速率、当前时间redis.call('get', key)获取上次更新时间,若不存在则返回0
local tokens = (now - last) * rate / 1000 + capacity
tokens = math.min(tokens, capacity) -- 计算当前可用令牌数- 计算时间差乘以速率得到新生成的令牌数
- 确保不超过桶容量
local current_tokens = tokens - 1 -- 扣除当前请求
local next_update = math.floor(now + (capacity - current_tokens) / rate * 1000)current_tokens是扣除请求后的剩余令牌- 计算下次更新时间
if current_tokens >= 0 then
return {1, current_tokens, next_update} -- 允许请求
else
return {0, current_tokens, next_update} -- 拒绝请求
end- 返回值格式:{状态码, 剩余令牌, 下次更新时间}
七、进阶使用
1. 动态调整限流参数
public void updateRateLimit(String userId, int newCapacity, double newRate) {
RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommandChannel<String, String, String> channel = connection.channel();
RedisScript<Void> script = RedisScript.of("local key = KEYS[1]\n" +
"redis.call('set', key, ARGV[1])", Void.class);
RedisCommand<String> command = RedisCommands.associate("EVAL", script);
List<String> keys = Arrays.asList(userId);
List<String> args = Arrays.asList(String.valueOf(newCapacity), String.valueOf(newRate));
channel.writeAndRead(command, keys, args);
connection.close();
redisClient.shutdown();
}2. 结合Kafka实现异步处理
public class KafkaProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public KafkaProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void send(String topic, String message) {
kafkaTemplate.send(topic, message);
}
}3. 多线程处理优化
@Async
public void asyncProcess(String userId) {
// 异步处理业务逻辑
}八、性能与工程实践
1. 性能优化
- Redis集群部署:将限流数据分片存储,避免热点
- Lua脚本优化:避免复杂的计算,减少脚本执行时间
- 批处理:对批量请求进行合并处理
- 缓存预热:提前加载常用限流参数
2. 安全风险
- Redis未授权访问:可能导致数据篡改
- 限流参数配置错误:导致系统过载或服务不可用
- Lua脚本漏洞:可能引发DoS攻击
3. 异常处理
- Redis连接异常:使用重试机制和连接池
- Lua脚本执行异常:捕获异常并记录日志
- 时间戳问题:确保时间戳的准确性
九、常见问题与踩坑
1. 常见错误
错误示例:
// 错误:未处理Redis连接异常
public boolean isAllowed(String userId, int capacity, double rate, long now) {
RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommandChannel<String, String, String> channel = connection.channel();
// ... 省略其他代码
}错误原因: 未处理连接异常,可能导致资源泄露
改进方法:
public boolean isAllowed(String userId, int capacity, double rate, long now) {
RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379");
try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
RedisCommandChannel<String, String, String> channel = connection.channel();
// ... 省略其他代码
} catch (Exception e) {
log.error("Redis连接异常", e);
return false;
}
}2. 其他常见问题
- Lua脚本参数类型错误:确保参数类型与Lua脚本匹配
- Redis集群分片策略:确保用户标识的哈希分布均匀
- 限流策略配置错误:导致系统过载或服务不可用
十、最佳实践
限流参数配置
- 容量设置为系统处理能力的1.5倍
- 填充速率根据业务需求动态调整
- 设置合理的请求窗口时间
监控与告警
- 使用Prometheus监控限流指标
- 设置阈值告警,当拒绝请求率超过5%时触发告警
日志记录
- 记录限流决策日志,便于后续分析
- 对拒绝请求进行分类统计
缓存策略
- 对高频访问的用户标识进行缓存预热
- 设置合理的缓存失效时间
十一、总结
本文深入探讨了基于Redis和Lua实现分布式令牌桶限流的原理和实现方式,通过完整的代码示例展示了如何在实际项目中应用这一技术。在分布式系统中,限流是保障系统稳定性和服务质量的关键环节。通过Redis的原子操作和Lua脚本,我们能够实现跨节点的限流控制,避免资源耗尽等问题。
在实际开发中,需要根据业务场景选择合适的限流策略。对于高并发、分布式系统,建议使用基于Redis的分布式限流方案。同时,要注意处理常见的性能问题和安全风险,确保系统的稳定运行。
限流技术的选型需要综合考虑业务需求、系统架构、性能要求等多方面因素。在实际项目中,可以根据具体场景灵活选择单机限流、分布式限流或结合其他限流机制,最终实现系统的稳定运行。