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

 更新时间:2025年07月07日 09:03:11   作者:sean_it  
本文提出基于SpringBoot和Redis的外呼频次限制解决方案,采用Redis计数器与滑动窗口算法实现精准控制,具有一定的参考价值,感兴趣的可以了解一下

针对外呼场景中的号码频次限制需求(如每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 主叫号码
     * @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
                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")
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", "呼叫次数超过限制"
            ));
        }
    }
    
    @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外呼频次限制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot对接阿里云OSS的详细步骤和流程

    SpringBoot对接阿里云OSS的详细步骤和流程

    阿里云对象存储服务(Object Storage Service,简称OSS)是一种海量、安全、低成本、高可靠的云存储服务,它适合存储任意类型的文件,适用于海量数据存储、图片/视频存储、静态网站托管等场景,本文给大家介绍了SpringBoot对接阿里云OSS的详细步骤和流程
    2025-08-08
  • Java List排序实例代码详解

    Java List排序实例代码详解

    这篇文章主要介绍了Java List排序的相关资料,Java排序方法包括自然排序、自定义排序、Lambda简化及多条件排序,实现灵活且代码简洁,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-05-05
  • SpringBoot中间件ORM框架实现案例详解(Mybatis)

    SpringBoot中间件ORM框架实现案例详解(Mybatis)

    这篇文章主要介绍了SpringBoot中间件ORM框架实现案例详解(Mybatis),本篇文章提炼出mybatis最经典、最精简、最核心的代码设计,来实现一个mini-mybatis,从而熟悉并掌握ORM框架的涉及实现,需要的朋友可以参考下
    2023-07-07
  • IDEA+GIT使用入门图文详解

    IDEA+GIT使用入门图文详解

    这篇文章主要介绍了IDEA+GIT使用入门详解,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • idea启动多个服务不显示Services或者RunDashboard窗口的处理方法

    idea启动多个服务不显示Services或者RunDashboard窗口的处理方法

    这篇文章主要介绍了idea启动多个服务不显示Services或者RunDashboard窗口,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • 使用Apache POI和SpringBoot实现Excel文件上传和解析功能

    使用Apache POI和SpringBoot实现Excel文件上传和解析功能

    在现代企业应用开发中,数据的导入和导出是一项常见且重要的功能需求,Excel 作为一种广泛使用的电子表格工具,常常被用来存储和展示数据,下面我们来看看如何使用Apache POI和SpringBoot实现Excel文件上传和解析功能吧
    2025-01-01
  • Intellij IDEA导入eclipse web项目的操作步骤详解

    Intellij IDEA导入eclipse web项目的操作步骤详解

    Eclipse当中的web项目都会有这两个文件,但是idea当中应该是没有的,所以导入会出现兼容问题,但是本篇文章会教大家如何导入,并且导入过后还能使用tomcat运行,需要的朋友可以参考下
    2023-08-08
  • Maven发布封装到中央仓库时候报错:no default secret key

    Maven发布封装到中央仓库时候报错:no default secret key

    这篇文章主要介绍了Maven发布封装到中央仓库时候报错:no default secret key,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • Java调用打印机的2种方式举例(无驱/有驱)

    Java调用打印机的2种方式举例(无驱/有驱)

    我们平时使用某些软件或者在超市购物的时候都会发现可以使用打印机进行打印,这篇文章主要给大家介绍了关于Java调用打印机的2种方式,分别是无驱/有驱的相关资料,需要的朋友可以参考下
    2023-11-11
  • Java使用JSqlParser解析SQL语句应用场景

    Java使用JSqlParser解析SQL语句应用场景

    JSqlParser是一个功能全面的Java库,用于解析SQL语句,支持多种SQL方言,它可以轻松集成到Java项目中,并提供灵活的操作方式,本文介绍Java使用JSqlParser解析SQL语句总结,感兴趣的朋友一起看看吧
    2024-09-09

最新评论