第 196 章:Redis 分布式 Session 与限流
学习目标
- 实现分布式 Session 共享
- 掌握 4 种限流算法
- 用 Redis + Lua 实现限流
- Spring Security Session 集成
一、为什么需要分布式 Session
多机部署时,默认 Session 存在单机内存,负载均衡到不同机器会出现登录态丢失。
二、Spring Session + Redis
2.1 引入
xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>2.2 启用
java
@SpringBootApplication
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class App { }2.3 配置
yaml
spring:
session:
store-type: redis
timeout: 3600s
redis:
namespace: myapp:session
flush-mode: on_save
data:
redis:
host: localhost
port: 63792.4 使用(无侵入)
所有 HttpSession 自动存到 Redis:
java
@GetMapping("/user/info")
public User getUserInfo(HttpSession session) {
Long userId = (Long) session.getAttribute("userId");
return userService.findById(userId);
}Session 数据结构:
Key: myapp:session:abc-uuid
Value: hash结构(attributes)
TTL: 3600s 自动续期三、Token 替代 Session
移动端 / 跨域场景,常用 JWT 或 Redis Token:
java
@Service
public class TokenService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public String create(Long userId) {
String token = UUID.randomUUID().toString().replace("-", "");
String key = "token:" + token;
// 存 userId,7 天过期
redisTemplate.opsForValue().set(key, userId, 7, TimeUnit.DAYS);
return token;
}
public Long getUserId(String token) {
String key = "token:" + token;
Long userId = (Long) redisTemplate.opsForValue().get(key);
if (userId == null) {
throw new UnauthorizedException("登录已过期");
}
// 续期
redisTemplate.expire(key, 7, TimeUnit.DAYS);
return userId;
}
public void destroy(String token) {
redisTemplate.delete("token:" + token);
}
}四、限流算法
4.1 固定窗口(最简单)
lua
-- 固定窗口
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current <= tonumber(ARGV[2])缺点:临界突发(窗口切换时可能 2 倍流量)
4.2 滑动窗口
lua
-- 滑动窗口
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
-- 移除过期
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 查当前数
local current = redis.call('ZCARD', key)
if current >= limit then
return 0
end
-- 记录
redis.call('ZADD', key, now, now .. ':' .. math.random())
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 14.3 令牌桶(Token Bucket)
以固定速率往桶里放令牌,请求拿走令牌才能通过。
lua
-- 令牌桶
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 每秒生成 N 个
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间 ms
local requested = tonumber(ARGV[4]) -- 需要的令牌数
local data = redis.call('HMGET', key, 'tokens', 'last')
local tokens = tonumber(data[1]) or capacity
local last = tonumber(data[2]) or now
-- 计算这段时间新增的令牌
local delta = math.max(0, now - last)
local newTokens = math.min(capacity, tokens + delta * rate / 1000)
if newTokens >= requested then
newTokens = newTokens - requested
redis.call('HMSET', key, 'tokens', newTokens, 'last', now)
redis.call('PEXPIRE', key, 60000)
return 1
end
return 04.4 漏桶(Leaky Bucket)
请求进入桶,桶以固定速率流出,溢出则拒绝。
特点:强制匀速,常用于流量整形。
五、Redis 实战限流
5.1 Lua 脚本
lua
-- rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local period = tonumber(ARGV[2]) -- 秒
local current = redis.call('GET', key)
if current == false then
redis.call('SET', key, 1, 'EX', period)
return 1
end
current = tonumber(current)
if current >= limit then
return 0
end
redis.call('INCR', key)
return 15.2 Spring 注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int limit() default 60;
int period() default 60; // 秒
String key() default "";
}5.3 AOP 实现
java
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
String key = rateLimit.key().isEmpty()
? "rate:" + pjp.getSignature().toShortString()
: "rate:" + rateLimit.key();
Long count = redisTemplate.execute(rateLimitScript,
Collections.singletonList(key),
String.valueOf(rateLimit.limit()),
String.valueOf(rateLimit.period()));
if (count == null || count == 0) {
throw new RateLimitException("访问过于频繁");
}
return pjp.proceed();
}
}5.4 使用
java
@RateLimit(limit = 60, period = 60) // 每分钟 60 次
@GetMapping("/api/data")
public Result data() {
return Result.ok(...);
}
@RateLimit(limit = 5, period = 1, key = "login:ip:" + ip) // 登录防爆破
@PostMapping("/login")
public Result login(@RequestBody LoginDto dto) {
return Result.ok(...);
}六、Gateway 限流
yaml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒令牌数
redis-rate-limiter.burstCapacity: 20 # 桶容量
key-resolver: "#{@ipKeyResolver}" # 按 IPjava
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}七、滑动窗口实战
java
@Service
public class SlidingWindowLimiter {
private static final String LUA = """
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count >= limit then
return 0
end
redis.call('ZADD', key, now, now .. ':' .. ARGV[4])
redis.call('PEXPIRE', key, window + 1000)
return 1
""";
@Autowired
private StringRedisTemplate redisTemplate;
public boolean tryAcquire(String key, int limit, long windowMs) {
Long now = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString();
Long result = redisTemplate.execute(
new DefaultRedisScript<>(LUA, Long.class),
Collections.singletonList("limit:" + key),
String.valueOf(now),
String.valueOf(windowMs),
String.valueOf(limit),
nonce
);
return result != null && result == 1;
}
}八、本章小结
| 限流算法 | 精度 | 适用 |
|---|---|---|
| 固定窗口 | 低 | 简单限流 |
| 滑动窗口 | 中 | 一般场景 |
| 令牌桶 | 高 | 突发流量 |
| 漏桶 | 高 | 流量整形 |
| Session 方案 | 特点 |
|---|---|
| 默认 Session | 单机内存 |
| Spring Session + Redis | 无侵入、自动同步 |
| Token + Redis | 跨域、移动端 |
动手练习
- 用 Spring Session + Redis 实现 Session 共享
- 实现 Lua 版固定窗口限流
- 用 ZSet 实现滑动窗口限流
- 用令牌桶保护登录接口(防爆破)
推荐阅读
下一章:第 197 章:Redis 应用场景与最佳实践 →