高并發(fā)下Redis精確計(jì)數(shù)與時(shí)間窗口過期的方法詳解
引言
在實(shí)時(shí)數(shù)據(jù)處理系統(tǒng)中,我們經(jīng)常需要統(tǒng)計(jì)某個(gè)事件在特定時(shí)間窗口內(nèi)的發(fā)生次數(shù),例如:
- 統(tǒng)計(jì)用戶每小時(shí)訪問次數(shù)
- 限制設(shè)備每分鐘請(qǐng)求頻率
- 廣告曝光按小時(shí)去重計(jì)數(shù)
這類需求通常面臨兩個(gè)核心挑戰(zhàn):
- 高并發(fā)計(jì)數(shù):多臺(tái)服務(wù)器同時(shí)讀寫同一個(gè)計(jì)數(shù)器
- 精確時(shí)間窗口:數(shù)據(jù)到點(diǎn)自動(dòng)過期,避免累積
本文將詳細(xì)介紹如何基于 Redis 實(shí)現(xiàn)高性能、高可用的計(jì)數(shù)方案,并提供完整的Java代碼實(shí)現(xiàn)。
一、Redis計(jì)數(shù)方案選型
1.1 為什么選擇Redis
| 方案 | QPS | 數(shù)據(jù)一致性 | 實(shí)現(xiàn)復(fù)雜度 |
|---|---|---|---|
| 數(shù)據(jù)庫+事務(wù) | ~1K | 強(qiáng)一致 | 高 |
| 本地緩存 | ~100K | 最終一致 | 中 |
| Redis原子操作 | 50K+ | 強(qiáng)一致 | 低 |
Redis的單線程模型天然適合計(jì)數(shù)場(chǎng)景,提供INCR/INCRBY等原子命令。
1.2 Key設(shè)計(jì)原則
// 格式:業(yè)務(wù)前綴:appId:deviceId:ip:時(shí)間窗口 String key = "flow:count:app123:device456:127.0.0.1:2023080117";
- 包含所有維度信息
- 時(shí)間窗口按小時(shí)切分(可調(diào)整)
- 添加業(yè)務(wù)前綴避免沖突
二、基礎(chǔ)實(shí)現(xiàn)方案
2.1 簡(jiǎn)單INCRBY實(shí)現(xiàn)
public void incrementCount(String key, int delta) {
redisTemplate.opsForValue().increment(key, delta);
}
問題:沒有過期時(shí)間,會(huì)導(dǎo)致數(shù)據(jù)無限堆積
2.2 增加過期時(shí)間
public void incrementWithExpire(String key, int delta, long ttlSeconds) {
redisTemplate.opsForValue().increment(key, delta);
redisTemplate.expire(key, ttlSeconds, TimeUnit.SECONDS);
}
新問題:每次操作都設(shè)置TTL,造成冗余Redis調(diào)用
三、優(yōu)化方案:精準(zhǔn)TTL控制
3.1 判斷Key是否首次寫入
我們需要確保TTL只在Key創(chuàng)建時(shí)設(shè)置一次,兩種實(shí)現(xiàn)方式:
方案A:Lua腳本(推薦)
private static final String LUA_SCRIPT =
"local current = redis.call('INCRBY', KEYS[1], ARGV[1])\n" +
"if current == tonumber(ARGV[1]) then\n" +
" redis.call('EXPIRE', KEYS[1], ARGV[2])\n" +
"end\n" +
"return current";
public Long incrementAtomically(String key, int delta, long ttl) {
return redisTemplate.execute(
new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
Collections.singletonList(key),
String.valueOf(delta), String.valueOf(ttl)
);
}
優(yōu)勢(shì):
- 完全原子性執(zhí)行
- 單次網(wǎng)絡(luò)往返
- 精準(zhǔn)判斷首次寫入
方案B:SETNX+INCRBY
public void incrementWithNX(String key, int delta, long ttl) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection conn = (StringRedisConnection) connection;
conn.setNX(key, "0"); // 嘗試初始化
conn.incrBy(key, delta);
if (conn.setNX(key + ":lock", "1")) { // 簡(jiǎn)易鎖判斷首次
conn.expire(key, ttl);
conn.expire(key + ":lock", 10);
}
return null;
});
}
適用場(chǎng)景:Redis版本<2.6(不支持Lua)
四、完整生產(chǎn)級(jí)實(shí)現(xiàn)
4.1 時(shí)間窗口計(jì)算
public long calculateTtlToNextHour() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime nextHour = now.plusHours(1).truncatedTo(ChronoUnit.HOURS);
return ChronoUnit.SECONDS.between(now, nextHour);
}
4.2 Kafka消費(fèi)者集成
@Component
@RequiredArgsConstructor
public class FlowCounter {
private final RedisTemplate<String, String> redisTemplate;
private static final String KEY_PREFIX = "flow:count:";
@KafkaListener(topics = "${kafka.topic}")
public void handleMessages(List<Message> messages) {
Map<String, Integer> countMap = messages.stream()
.collect(Collectors.toMap(
this::buildKey,
msg -> 1,
Integer::sum
));
countMap.forEach((k, v) ->
incrementAtomically(k, v, calculateTtlToNextHour())
);
}
??????? private String buildKey(Message msg) {
return String.format("%s%s:%s:%s:%s",
KEY_PREFIX,
msg.getAppId(),
msg.getDeviceId(),
msg.getIp(),
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHH"))
);
}
}4.3 查詢接口
public long getCurrentCount(String appId, String deviceId, String ip) {
String key = buildKey(appId, deviceId, ip);
String val = redisTemplate.opsForValue().get(key);
return val != null ? Long.parseLong(val) : 0L;
}
五、性能優(yōu)化技巧
5.1 Pipeline批量處理
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection conn = (StringRedisConnection) connection;
countMap.forEach((k, v) -> {
conn.incrBy(k, v);
// 可結(jié)合Lua腳本進(jìn)一步優(yōu)化
});
return null;
});
5.2 本地預(yù)聚合
// 在內(nèi)存中先合并相同Key的計(jì)數(shù)
Map<String, Integer> localCount = messages.stream()
.collect(Collectors.toMap(
this::buildKey,
m -> 1,
Integer::sum
));
5.3 集群部署注意事項(xiàng)
使用{}強(qiáng)制哈希標(biāo)簽,保證相同Key路由到同一節(jié)點(diǎn)
"{flow}:count:app123:..."
考慮分片策略避免熱點(diǎn)
六、異常處理與監(jiān)控
6.1 Redis重試機(jī)制
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 100))
public void safeIncrement(String key, int delta) {
// 業(yè)務(wù)邏輯
}
6.2 監(jiān)控指標(biāo)
# TYPE redis_operations_total counter
redis_operations_total{operation="incr"} 12345
redis_operations_total{operation="expire"} 678
6.3 數(shù)據(jù)補(bǔ)償
@Scheduled(fixedRate = 3600000)
public void checkDataConsistency() {
// 對(duì)比DB與Redis計(jì)數(shù)差異
}
七、方案對(duì)比總結(jié)
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|
| Lua腳本 | 原子性強(qiáng),性能最佳 | 需要Redis 2.6+ | 新項(xiàng)目首選 |
| SETNX+INCR | 兼容舊版 | 有競(jìng)態(tài)風(fēng)險(xiǎn) | 遺留系統(tǒng) |
| 純INCR+TTL | 實(shí)現(xiàn)簡(jiǎn)單 | TTL冗余 | 不推薦生產(chǎn) |
結(jié)語
通過本文的方案,我們實(shí)現(xiàn)了:
- 單機(jī)50K+ QPS的計(jì)數(shù)能力
- 精確到小時(shí)的時(shí)間窗口控制
- 分布式環(huán)境下的強(qiáng)一致性
最佳實(shí)踐建議:
- 生產(chǎn)環(huán)境優(yōu)先選擇Lua腳本方案
- 對(duì)于超高并發(fā)場(chǎng)景(如雙11),可增加本地緩存層
- 定期檢查Redis內(nèi)存使用情況
以上就是高并發(fā)下Redis精確計(jì)數(shù)與時(shí)間窗口過期的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Redis高并發(fā)精確計(jì)數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
redis使用zset實(shí)現(xiàn)延時(shí)隊(duì)列的示例代碼
本文主要介紹了redis使用zset實(shí)現(xiàn)延時(shí)隊(duì)列的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
redis實(shí)現(xiàn)存儲(chǔ)帖子的點(diǎn)贊狀態(tài)和數(shù)量的示例代碼
使用Redis來實(shí)現(xiàn)點(diǎn)贊功能是一種高效的選擇,因?yàn)镽edis是一個(gè)內(nèi)存數(shù)據(jù)庫,適用于處理高并發(fā)的數(shù)據(jù)操作,這篇文章主要介紹了redis實(shí)現(xiàn)存儲(chǔ)帖子的點(diǎn)贊狀態(tài)和數(shù)量的示例代碼,需要的朋友可以參考下2023-09-09
redis?lua腳本解決高并發(fā)下秒殺場(chǎng)景
這篇文章主要為大家介紹了redis?lua腳本解決高并發(fā)下秒殺場(chǎng)景,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
redis?zset實(shí)現(xiàn)滑動(dòng)窗口限流的代碼
這篇文章主要介紹了redis?zset實(shí)現(xiàn)滑動(dòng)窗口限流,滑動(dòng)窗口算法思想就是記錄一個(gè)滑動(dòng)的時(shí)間窗口內(nèi)的操作次數(shù),操作次數(shù)超過閾值則進(jìn)行限流,本文通過實(shí)例代碼給大家詳細(xì)介紹,需要的朋友參考下吧2022-03-03
gem install redis報(bào)錯(cuò)的解決方案
今天小編就為大家分享一篇關(guān)于gem install redis報(bào)錯(cuò)的解決方案,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01
Redis高級(jí)玩法之利用SortedSet實(shí)現(xiàn)多維度排序的方法
Redis的SortedSet是可以根據(jù)score進(jìn)行排序的,以手機(jī)應(yīng)用商店的熱門榜單排序?yàn)槔?,根?jù)下載量倒序排列。接下來通過本文給大家分享Redis高級(jí)玩法之利用SortedSet實(shí)現(xiàn)多維度排序的方法,一起看看吧2019-07-07

