SpringBoot+Redis實(shí)現(xiàn)外呼頻次限制功能的項(xiàng)目實(shí)踐
針對(duì)外呼場(chǎng)景中的號(hào)碼頻次限制需求(如每3天只能呼出1000通電話),我可以提供一個(gè)基于Spring Boot和Redis的完整解決方案。
方案設(shè)計(jì)
核心思路
- 使用Redis的計(jì)數(shù)器+過(guò)期時(shí)間機(jī)制
- 采用滑動(dòng)窗口算法實(shí)現(xiàn)精確控制
- 通過(guò)Lua腳本保證原子性操作
實(shí)現(xiàn)步驟
1. 添加依賴(lài)
<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency>
2. 配置Redis
# application.yml spring: redis: host: localhost port: 6379 password: database: 0
3. 實(shí)現(xiàn)頻次限制服務(wù)
@Service public class CallFrequencyService { private final StringRedisTemplate redisTemplate; private static final String CALL_COUNT_PREFIX = "call:count:"; private static final String CALL_TIMESTAMP_PREFIX = "call:timestamp:"; @Autowired public CallFrequencyService(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 檢查并增加呼叫計(jì)數(shù) * @param callerNumber 主叫號(hào)碼 * @param limit 限制次數(shù) * @param period 限制周期(秒) * @return 是否允許呼叫 */ public boolean checkAndIncrement(String callerNumber, int limit, long period) { String countKey = CALL_COUNT_PREFIX + callerNumber; String timestampKey = CALL_TIMESTAMP_PREFIX + callerNumber; // 使用Lua腳本保證原子性 String luaScript = """ local count = redis.call('get', KEYS[1]) local timestamp = redis.call('get', KEYS[2]) local now = tonumber(ARGV[3]) if count and timestamp then if now - tonumber(timestamp) < tonumber(ARGV[2]) then if tonumber(count) >= tonumber(ARGV[1]) then return 0 else redis.call('incr', KEYS[1]) return 1 end else redis.call('set', KEYS[1], 1) redis.call('set', KEYS[2], ARGV[3]) redis.call('expire', KEYS[1], ARGV[2]) redis.call('expire', KEYS[2], ARGV[2]) return 1 end else redis.call('set', KEYS[1], 1) redis.call('set', KEYS[2], ARGV[3]) redis.call('expire', KEYS[1], ARGV[2]) redis.call('expire', KEYS[2], ARGV[2]) return 1 end """; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(luaScript); redisScript.setResultType(Long.class); Long result = redisTemplate.execute(redisScript, Arrays.asList(countKey, timestampKey), String.valueOf(limit), String.valueOf(period), String.valueOf(System.currentTimeMillis() / 1000)); return result != null && result == 1; } /** * 獲取剩余可呼叫次數(shù) * @param callerNumber 主叫號(hào)碼 * @param limit 限制次數(shù) * @return 剩余次數(shù) */ public int getRemainingCount(String callerNumber, int limit) { String countKey = CALL_COUNT_PREFIX + callerNumber; String countStr = redisTemplate.opsForValue().get(countKey); if (StringUtils.isBlank(countStr)) { return limit; } int used = Integer.parseInt(countStr); return Math.max(0, limit - used); } }
4. 實(shí)現(xiàn)REST接口
@RestController @RequestMapping("/api/call") public class CallController { private static final int DEFAULT_LIMIT = 1000; private static final long DEFAULT_PERIOD = 3 * 24 * 60 * 60; // 3天(秒) @Autowired private CallFrequencyService callFrequencyService; @PostMapping("/check") public ResponseEntity<?> checkCallPermission(@RequestParam String callerNumber) { boolean allowed = callFrequencyService.checkAndIncrement( callerNumber, DEFAULT_LIMIT, DEFAULT_PERIOD); if (allowed) { return ResponseEntity.ok().body(Map.of( "allowed", true, "remaining", callFrequencyService.getRemainingCount(callerNumber, DEFAULT_LIMIT) )); } else { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(Map.of( "allowed", false, "message", "呼叫次數(shù)超過(guò)限制" )); } } @GetMapping("/remaining") public ResponseEntity<?> getRemainingCount(@RequestParam String callerNumber) { int remaining = callFrequencyService.getRemainingCount(callerNumber, DEFAULT_LIMIT); return ResponseEntity.ok().body(Map.of( "remaining", remaining, "limit", DEFAULT_LIMIT )); } }
5. 添加定時(shí)任務(wù)重置計(jì)數(shù)器(可選)
@Scheduled(cron = "0 0 0 * * ?") // 每天凌晨執(zhí)行 public void resetExpiredCounters() { // 可以定期清理過(guò)期的key,避免Redis積累太多無(wú)用key // 實(shí)際應(yīng)用中,依賴(lài)expire通常已經(jīng)足夠 }
方案優(yōu)化點(diǎn)
- 分布式鎖:如果需要更精確的控制,可以在Lua腳本中加入分布式鎖
- 多維度限制:可以擴(kuò)展為基于號(hào)碼+時(shí)間段的多維度限制
- 熔斷機(jī)制:當(dāng)達(dá)到限制閾值時(shí),可以暫時(shí)熔斷該號(hào)碼的呼叫能力
- 動(dòng)態(tài)配置:將限制參數(shù)配置在數(shù)據(jù)庫(kù)或配置中心,實(shí)現(xiàn)動(dòng)態(tài)調(diào)整
測(cè)試用例
@SpringBootTest public class CallFrequencyServiceTest { @Autowired private CallFrequencyService callFrequencyService; @Test public void testCallFrequencyLimit() { String testNumber = "13800138000"; int limit = 5; long period = 60; // 60秒 // 前5次應(yīng)該成功 for (int i = 0; i < limit; i++) { assertTrue(callFrequencyService.checkAndIncrement(testNumber, limit, period)); } // 第6次應(yīng)該失敗 assertFalse(callFrequencyService.checkAndIncrement(testNumber, limit, period)); // 等待周期結(jié)束 try { Thread.sleep(period * 1000); } catch (InterruptedException e) { e.printStackTrace(); } // 新周期應(yīng)該重新計(jì)數(shù) assertTrue(callFrequencyService.checkAndIncrement(testNumber, limit, period)); } }
這個(gè)方案能夠高效、準(zhǔn)確地實(shí)現(xiàn)外呼頻次限制功能,通過(guò)Redis的高性能和原子性操作保證系統(tǒng)的可靠性,適合在生產(chǎn)環(huán)境中使用。
備注:
1、什么時(shí)間來(lái)統(tǒng)計(jì)使用次數(shù),真正呼叫出去才應(yīng)該是使用了呼叫次數(shù),所以需要異步在話單里來(lái)進(jìn)行處理,且需要判斷話單的具體狀態(tài)是否認(rèn)為是這個(gè)號(hào)碼被使用了。
2、在獲取號(hào)碼階段只去判斷當(dāng)前的訪問(wèn)次數(shù)是否超過(guò)了限制頻次即可,這樣的壞處時(shí)并不能精準(zhǔn)的去控制頻率(會(huì)有一小部分的時(shí)差),需要在性能和精確度上做綜合的權(quán)衡。
到此這篇關(guān)于SpringBoot+Redis實(shí)現(xiàn)外呼頻次限制功能的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot+Redis外呼頻次限制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中數(shù)組如何轉(zhuǎn)為字符串的幾種方法
數(shù)組是java中一個(gè)重要的類(lèi)型,小伙伴們知道如何將數(shù)組轉(zhuǎn)為字符串嗎,這篇文章主要給大家介紹了關(guān)于Java中數(shù)組如何轉(zhuǎn)為字符串的幾種方法,需要的朋友可以參考下2024-03-03SpringBoot如何使用MyBatis-Plus實(shí)現(xiàn)高效的數(shù)據(jù)訪問(wèn)層
在開(kāi)發(fā) Spring Boot 應(yīng)用時(shí),數(shù)據(jù)訪問(wèn)是不可或缺的部分,本文將詳細(xì)介紹如何在 Spring Boot 中使用 MyBatis-Plus,并結(jié)合具體代碼示例來(lái)講解它的使用方法和常見(jiàn)配置,希望對(duì)大家有一定的幫助2025-04-04java中靜態(tài)變量和實(shí)例變量的區(qū)別詳細(xì)介紹
本篇文章介紹了,java中靜態(tài)變量和實(shí)例變量的區(qū)別。需要的朋友參考下2013-05-05詳解SpringBoot實(shí)現(xiàn)ApplicationEvent事件的監(jiān)聽(tīng)與發(fā)布
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何實(shí)現(xiàn)ApplicationEvent事件的監(jiān)聽(tīng)與發(fā)布,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-03-03Java實(shí)現(xiàn)DelayQueue延遲隊(duì)列示例代碼
Java中的DelayQueue是一個(gè)特殊的隊(duì)列,它只允許在指定的延遲時(shí)間之后才能從隊(duì)列中取出元素,這篇文章主要給大家介紹了關(guān)于Java實(shí)現(xiàn)DelayQueue延遲隊(duì)列的相關(guān)資料,需要的朋友可以參考下2025-07-07Mybatis-plus 批量插入太慢的問(wèn)題解決(提升插入性能)
公司使用的Mybatis-Plus操作SQL,用過(guò)Mybatis-Plus的小伙伴一定知道他有很多API提供給我們使用,但是批量插入大數(shù)據(jù)太慢應(yīng)該怎么解決,本文就詳細(xì)的介紹一下,感興趣的可以了解一下2021-11-11詳解Java的Spring框架下bean的自動(dòng)裝載方式
這篇文章主要介紹了Java的Spring框架下bean的自動(dòng)裝載方式,Spring是Java的SSH三大web開(kāi)發(fā)框架之一,需要的朋友可以參考下2015-12-12淺談java 字符串,字符數(shù)組,list間的轉(zhuǎn)化
下面小編就為大家?guī)?lái)一篇淺談java 字符串,字符數(shù)組,list間的轉(zhuǎn)化。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-11-11