SpringBoot整合Redis實(shí)現(xiàn)分布式限流功能
引言
線上服務(wù)最怕被惡意刷接口或者突發(fā)流量把系統(tǒng)打垮。單機(jī)限流用 Guava RateLimiter 就行,但微服務(wù)多實(shí)例部署時(shí)必須用分布式限流——所有實(shí)例共享同一個(gè)限流狀態(tài)。
一、限流的常見算法
| 算法 | 原理 | 特點(diǎn) |
|---|---|---|
| 固定窗口 | 1 分鐘內(nèi)最多 N 個(gè)請(qǐng)求 | 簡(jiǎn)單但有突刺問題 |
| 滑動(dòng)窗口 | 把窗口切小段,更平滑 | 推薦,較常用 |
| 漏桶 | 請(qǐng)求進(jìn)桶,勻速流出 | 平滑流量,但突發(fā)處理慢 |
| 令牌桶 | 勻速放令牌,拿到的才通過 | 允許一定突發(fā),最均衡 |
Redis 實(shí)現(xiàn)限流的本質(zhì): 用 Redis 計(jì)數(shù)器/有序集合記錄請(qǐng)求次數(shù),加上過期時(shí)間自動(dòng)清理。
二、固定窗口限流
最簡(jiǎn)單的方案:一個(gè) key 記錄 1 分鐘內(nèi)的請(qǐng)求數(shù)。
@Component
public class FixedWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 固定窗口限流
* @param key 限流 key(如 "rate:user:1001")
* @param max 窗口內(nèi)最大請(qǐng)求數(shù)
* @param window 窗口大?。耄?
* @return true 通過,false 被限
*/
public boolean tryAcquire(String key, long max, long window) {
// INCR 原子自增
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
// 第一次訪問,設(shè)置過期時(shí)間
redisTemplate.expire(key, window, TimeUnit.SECONDS);
}
return count <= max;
}
}
@RestController
public class TestController {
@Autowired
private FixedWindowRateLimiter rateLimiter;
@GetMapping("/api/test")
public ResultVO<?> test() {
// 限制每個(gè)用戶每秒最多 5 次
if (!rateLimiter.tryAcquire("rate:user:1001", 5, 1)) {
return ResultVO.error(429, "請(qǐng)求太頻繁,請(qǐng)稍后");
}
return ResultVO.success("成功");
}
}
問題: 窗口邊界會(huì)有突刺——比如第 59 秒和第 61 秒各發(fā) 5 個(gè)請(qǐng)求,理論上通過 10 個(gè),但看起來像 1 秒內(nèi) 10 個(gè)。
三、滑動(dòng)窗口限流(推薦)
用 Redis ZSet(有序集合)記錄每個(gè)請(qǐng)求的時(shí)間戳,滑動(dòng)窗口更精確。
@Component
public class SlidingWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 滑動(dòng)窗口限流
* @param key 限流 key
* @param max 窗口內(nèi)最大請(qǐng)求數(shù)
* @param window 窗口大?。ê撩耄?
* @return true 通過,false 被限
*/
public boolean tryAcquire(String key, long max, long windowMs) {
long now = System.currentTimeMillis();
long windowStart = now - windowMs;
// 使用 Lua 腳本保證原子性
String luaScript =
"redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]) " + // 移除窗口外的記錄
"local count = redis.call('ZCARD', KEYS[1]) " + // 統(tǒng)計(jì)窗口內(nèi)請(qǐng)求數(shù)
"if count < tonumber(ARGV[2]) then " +
" redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3]) " + // 加入當(dāng)前請(qǐng)求
" redis.call('EXPIRE', KEYS[1], ARGV[4]) " + // 設(shè)置過期(秒)
" return 1 " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(windowStart),
String.valueOf(max),
String.valueOf(now),
String.valueOf(windowMs / 1000 + 1)
);
return Long.valueOf(1).equals(result);
}
}
四、注解式限流
把限流邏輯封裝成注解,用在需要的接口上,代碼更干凈。
1. 定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/** 限流 key(支持 SpEL) */
String key() default "";
/** 限流維度:用戶、IP、接口 */
String type() default "user"; // user、ip、api
/** 最大請(qǐng)求數(shù) */
long max() default 10;
/** 窗口大小(秒) */
long window() default 1;
}
2. 實(shí)現(xiàn) AOP 切面
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private SlidingWindowRateLimiter rateLimiter;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
// 構(gòu)建 key
String key = buildKey(rateLimit, pjp);
// 執(zhí)行限流
boolean acquired = rateLimiter.tryAcquire(
key, rateLimit.max(), rateLimit.window() * 1000);
if (!acquired) {
throw new RuntimeException("請(qǐng)求太頻繁,請(qǐng)稍后重試");
}
return pjp.proceed();
}
private String buildKey(RateLimit rateLimit, ProceedingJoinPoint pjp) {
String prefix = "rate:";
switch (rateLimit.type()) {
case "ip":
// 從請(qǐng)求中獲取 IP(需要注入 RequestContextHolder)
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getRemoteAddr();
return prefix + "ip:" + ip;
case "api":
// 按接口限流
String methodName = pjp.getSignature().toShortString();
return prefix + "api:" + methodName;
default:
// 按用戶限流(需要從 Token 獲取用戶 ID)
String userId = StpUtil.getLoginIdAsString();
return prefix + "user:" + userId;
}
}
}
// 全局異常處理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResultVO<?> handle(RuntimeException e) {
if (e.getMessage().contains("請(qǐng)求太頻繁")) {
return ResultVO.error(429, "請(qǐng)求太頻繁,請(qǐng)稍后重試");
}
return ResultVO.error(500, e.getMessage());
}
}
3. 使用
@RestController
@RequestMapping("/api")
public class OrderController {
@PostMapping("/order")
@RateLimit(key = "createOrder", type = "user", max = 5, window = 10)
public ResultVO<?> createOrder(@RequestBody OrderDTO dto) {
// 每個(gè)用戶 10 秒內(nèi)最多 5 次下單
return ResultVO.success("下單成功");
}
@GetMapping("/product/list")
@RateLimit(type = "api", max = 100, window = 1)
public ResultVO<?> list() {
// 接口級(jí)別限流:每秒最多 100 次
return ResultVO.success("商品列表");
}
}
五、秒殺系統(tǒng)的限流
在秒殺系統(tǒng)中,限流是保護(hù)系統(tǒng)的第一道防線:
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@PostMapping("/do/{productId}")
@RateLimit(type = "user", max = 1, window = 10) // 每個(gè)用戶10秒只能秒殺1次
public ResultVO<?> doSeckill(@PathVariable Long productId) {
long userId = StpUtil.getLoginIdAsLong();
return seckillService.doSeckill(productId, userId, "用戶");
}
}
秒殺系統(tǒng)的多層防護(hù):
用戶請(qǐng)求 ↓ ① 限流(Redis 滑動(dòng)窗口)→ 被限返回"操作頻繁" ↓ ② 參數(shù)校驗(yàn) → 不合法直接返回 ↓ ③ 時(shí)間校驗(yàn) → 沒開始/已結(jié)束直接返回 ↓ ④ 重復(fù)提交校驗(yàn)(SETNX)→ 已提交直接返回 ↓ ⑤ Redis 預(yù)減庫(kù)存 → 庫(kù)存不足直接返回 ↓ ⑥ 數(shù)據(jù)庫(kù)扣庫(kù)存(樂觀鎖)→ 失敗回滾 ↓ ⑦ 生成訂單
限流在最外層,把大部分無效請(qǐng)求擋在門外。
六、IP 限流
@GetMapping("/api/public")
@RateLimit(type = "ip", max = 30, window = 60)
public ResultVO<?> publicApi() {
// 同一 IP 每分鐘最多 30 次
return ResultVO.success("公開接口");
}
獲取真實(shí) IP(經(jīng)過 Nginx 代理時(shí)):
public static String getRealIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多個(gè)代理時(shí)取第一個(gè)
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
七、限流配置管理
把限流參數(shù)放在配置中心或 yml 中,不用改代碼:
rate-limit:
defaults:
max: 10
window: 1
rules:
- key: "createOrder"
max: 5
window: 10
- key: "seckill"
max: 1
window: 10
- key: "publicApi"
max: 30
window: 60@ConfigurationProperties(prefix = "rate-limit")
@Data
public class RateLimitProperties {
private Rule defaults = new Rule();
private List<Rule> rules = new ArrayList<>();
@Data
public static class Rule {
private String key;
private long max;
private long window;
}
}
八、限流效果驗(yàn)證
# 快速請(qǐng)求測(cè)試限流效果
for i in {1..20}; do
curl -X POST http://localhost:9090/api/order
echo ""
done
# 正常返回:{"code":200,"message":"下單成功"}
# 被限返回:{"code":429,"message":"請(qǐng)求太頻繁,請(qǐng)稍后重試"}
九、限流 vs 熔斷 vs 降級(jí)
| 限流 Ratelimit | 熔斷 CircuitBreaker | 降級(jí) Degrade | |
|---|---|---|---|
| 作用 | 控制請(qǐng)求速率 | 防止故障擴(kuò)散 | 舍棄非核心功能 |
| 時(shí)機(jī) | 請(qǐng)求進(jìn)來時(shí) | 錯(cuò)誤率超標(biāo)時(shí) | 資源不足時(shí) |
| 工具 | Redis + Lua | Sentinel、Resilience4j | Sentinel、Hystrix |
建議: 限流是必須的,熔斷和降級(jí)在大流量的核心系統(tǒng)上建議加上。
以上就是SpringBoot整合Redis實(shí)現(xiàn)分布式限流功能的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Redis分布式限流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mybatis實(shí)現(xiàn)批量刪除和多條件查詢方式
本文介紹了批量刪除功能的實(shí)現(xiàn)步驟,包括后臺(tái)代碼的修改,具體涉及到mapper層、Service層、Controller接口等層面的修改,同時(shí)也介紹了多條件查詢功能的實(shí)現(xiàn),具體包括創(chuàng)建封裝查詢條件的類,mapper層、Service層、Controller接口的修改等2026-05-05
Java8如何優(yōu)雅的記錄代碼運(yùn)行時(shí)間
這篇文章主要為大家詳細(xì)介紹了 Java 8 中幾種記錄代碼運(yùn)行時(shí)間的優(yōu)雅方式,并附上實(shí)用工具類與建議,希望可以幫助大家提高大家的代碼可讀性與復(fù)用性2025-04-04
Java高級(jí)用法中的JNA類型映射注意細(xì)節(jié)及使用問題
本文介紹了在使用JNA方法映射中應(yīng)該注意的一些細(xì)節(jié)和具體的使用問題,對(duì)java??JNA類型映射注意細(xì)節(jié)感興趣的朋友一起看看吧2022-04-04
Java數(shù)組動(dòng)態(tài)擴(kuò)容的實(shí)現(xiàn)示例
本文主要介紹了Java數(shù)組動(dòng)態(tài)擴(kuò)容的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-12-12
Java之idea @NotNull @Nullable 注解使用
這篇文章主要介紹了Java之idea @NotNull @Nullable 注解使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
Java實(shí)現(xiàn)雙色球抽獎(jiǎng)隨機(jī)算法示例
本篇文章主要介紹了Java實(shí)現(xiàn)雙色球抽獎(jiǎng)隨機(jī)算法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06
jdk中動(dòng)態(tài)代理異常處理分析:UndeclaredThrowableException
最近在工作中遇到了報(bào)UndeclaredThrowableException的錯(cuò)誤,通過查找相關(guān)的資料,終于解決了,所以這篇文章主要給大家介紹了關(guān)于jdk中動(dòng)態(tài)代理異常處理分析:UndeclaredThrowableException的相關(guān)資料,需要的朋友可以參考下2018-04-04

