开发者

SpringBoot+Redis实现外呼频次限制功能的项目实践

目录
  • 方案设计
    • 核心思路
  • 实现步骤
    • 1. 添加依赖
    • 2. 配置Redis
    • 3. 实现频次限制服务
    • 4. 实现REST接口
    • 5. 添加定时任务重置计数器(可选)
  • 方案优化点
    • 测试用例
      • 备注:

        针对外呼场景中的号码频次限制需求(如每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: localhost
            port: 6379
            password: 
            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:";
            
            @Autowired
            public CallFrequencyService(StringRedisTemplate redisTemplate) {
                this.redisTemplate = redisTemplate;
            }
            
            /**
             * 检查并增加呼叫计数
             * @param callerNumber 主叫www.devze.com号码
             * @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 then
                    python    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;
            }
            
            /**
             * 获取剩余可呼叫次数
             * @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")
        pub编程客栈lic class CallController {
            
            private static final int DEFAULT_LIMIT = 1000;
            private static final long D编程客栈EFAULT_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", "呼叫次数超过限制"
                    ));
                }
            }
            
            @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 {
            
            @Autowired
            private CallFrequencyService callFrequencyService;
            
            @Test
            public 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、在获取号码阶段只去判断当前的访问次数是否超过了限制频次即可,这样的坏处时并不能精准的去控制频率(会有一小部分的时差),需要在性能和精确度上做综合的权衡。

        到此这篇关于SpringBoot+Redis实现外呼频次限制功能的项目实践的文章就介绍到这了,更多相关SpringBoot+Redis外呼频次限制内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜