针对外呼场景中的号码频次限制需求(如每3天只能呼出1000通电话),我可以提供一个基于Spring Boot和Redis的完整解决方案。
方案设计
核心思路
-
使用Redis的计数器+过期时间机制
-
采用滑动窗口算法实现精确控制
-
通过Lua脚本保证原子性操作
实现步骤
1. 添加依赖
<!-- 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: localhostport: 6379password: database: 0
3. 实现频次限制服务
@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:";@Autowiredpublic CallFrequencyService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}/*** 检查并增加呼叫计数* @param callerNumber 主叫号码* @param limit 限制次数* @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 thenif now - tonumber(timestamp) < tonumber(ARGV[2]) thenif tonumber(count) >= tonumber(ARGV[1]) thenreturn 0elseredis.call('incr', KEYS[1])return 1endelseredis.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 1endelseredis.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 1end""";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;}/*** 获取剩余可呼叫次数* @param callerNumber 主叫号码* @param limit 限制次数* @return 剩余次数*/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. 实现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天(秒)@Autowiredprivate 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", "呼叫次数超过限制"));}}@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. 添加定时任务重置计数器(可选)
@Scheduled(cron = "0 0 0 * * ?") // 每天凌晨执行
public void resetExpiredCounters() {// 可以定期清理过期的key,避免Redis积累太多无用key// 实际应用中,依赖expire通常已经足够
}
方案优化点
-
分布式锁:如果需要更精确的控制,可以在Lua脚本中加入分布式锁
-
多维度限制:可以扩展为基于号码+时间段的多维度限制
-
熔断机制:当达到限制阈值时,可以暂时熔断该号码的呼叫能力
-
动态配置:将限制参数配置在数据库或配置中心,实现动态调整
测试用例
@SpringBootTest
public class CallFrequencyServiceTest {@Autowiredprivate CallFrequencyService callFrequencyService;@Testpublic void testCallFrequencyLimit() {String testNumber = "13800138000";int limit = 5;long period = 60; // 60秒// 前5次应该成功for (int i = 0; i < limit; i++) {assertTrue(callFrequencyService.checkAndIncrement(testNumber, limit, period));}// 第6次应该失败assertFalse(callFrequencyService.checkAndIncrement(testNumber, limit, period));// 等待周期结束try {Thread.sleep(period * 1000);} catch (InterruptedException e) {e.printStackTrace();}// 新周期应该重新计数assertTrue(callFrequencyService.checkAndIncrement(testNumber, limit, period));}
}
这个方案能够高效、准确地实现外呼频次限制功能,通过Redis的高性能和原子性操作保证系统的可靠性,适合在生产环境中使用。
备注:
1、什么时间来统计使用次数,真正呼叫出去才应该是使用了呼叫次数,所以需要异步在话单里来进行处理,且需要判断话单的具体状态是否认为是这个号码被使用了。
2、在获取号码阶段只去判断当前的访问次数是否超过了限制频次即可,这样的坏处时并不能精准的去控制频率(会有一小部分的时差),需要在性能和精确度上做综合的权衡。