针对外呼场景中的号码频次限制需求(如每3天只能呼出1000通电话),我可以提供一个基于Spring Boot和Redis的完整解决方案。

方案设计

核心思路

  1. 使用Redis的计数器+过期时间机制

  2. 采用滑动窗口算法实现精确控制

  3. 通过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通常已经足够
}

方案优化点

  1. 分布式锁:如果需要更精确的控制,可以在Lua脚本中加入分布式锁

  2. 多维度限制:可以扩展为基于号码+时间段的多维度限制

  3. 熔断机制:当达到限制阈值时,可以暂时熔断该号码的呼叫能力

  4. 动态配置:将限制参数配置在数据库或配置中心,实现动态调整

测试用例

@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、在获取号码阶段只去判断当前的访问次数是否超过了限制频次即可,这样的坏处时并不能精准的去控制频率(会有一小部分的时差),需要在性能和精确度上做综合的权衡。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/87555.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/87555.shtml
英文地址,请注明出处:http://en.pswp.cn/web/87555.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

下一代 2D 图像设计工具「GitHub 热点速览」

长期以来&#xff0c;2D 设计领域似乎已是 Adobe 与 Figma 的天下&#xff0c;层叠的图层、熟悉的工具栏&#xff0c;一切都显得那么顺理成章&#xff0c;却也让不少设计师在创意的边界上感到了些许乏力。当我们以为设计工具的革新只能是小修小补时&#xff0c;开源社区再次扮演…

L58.【LeetCode题解】模拟算法习题集1(Z 字形变换、外观数列)

目录 1.Z 字形变换 方法1: 模拟 代码 提交结果 方法2:优化后的模拟 代码 提交结果 2.外观数列 方法1:模拟 代码 提交结果 方法2:打表 知识回顾 代码 1.Z 字形变换 https://leetcode.cn/problems/zigzag-conversion/ 将一个给定字符串 s 根据给定的行数 numRows &…

Flink MySQL CDC 环境配置与验证

一、MySQL 服务器配置详解 1. 启用二进制日志&#xff08;Binlog&#xff09; MySQL CDC 依赖二进制日志获取增量数据&#xff0c;需在 MySQL 配置文件&#xff08;my.cnf 或 my.ini&#xff09;中添加以下配置&#xff1a; # 启用二进制日志 log-binmysql-bin # 二进制日志…

如何查看自己电脑的CUDA版本?

在搜索栏输入命令提示符 打开 输入 nvidia-smi图片中的两个是CUDA版本和显卡的信息

opencv使用 GStreamer 硬解码和 CUDA 加速的方案

在Conda环境中从源代码编译OpenCV&#xff08;支持CUDA和GStreamer&#xff09; 以下是完整的方案步骤&#xff0c;包括必要的依赖库安装过程&#xff1a; 1. 安装Miniconda&#xff08;如果尚未安装&#xff09; # 下载Miniconda安装脚本 wget https://repo.anaconda.com/m…

Java面试宝典:多线程一

1. run() vs start() 陷阱题 下面程序的运行结果 public static void main(String[] args) {Thread t = new Thread(

【CSS-14-基础样式表Base.css】如何编写高质量的Base.css:前端样式重置与基础规范指南

在前端开发中&#xff0c;Base.css&#xff08;也称为重置样式表或基础样式表&#xff09;是整个项目样式的基石。它负责消除浏览器默认样式的差异&#xff0c;建立统一的样式基准&#xff0c;为后续开发提供一致的起点。一个精心设计的Base.css能够显著提高开发效率&#xff0…

探索Python数据科学工具链NumPyPandas与Scikit-learn

NumPy&#xff1a;数值计算的基石 NumPy是Python中用于科学计算的核心库&#xff0c;它提供了一个强大的N维数组对象&#xff0c;以及大量的数学函数库&#xff0c;能够高效地进行向量和矩阵运算。对于数据科学家而言&#xff0c;掌握NumPy是进行数据处理和算法实现的基础。 创…

八股学习(三)---MySQL

一、MySQL中的回表是什么&#xff1f;我的回答&#xff1a;MySQL回表指的是在查询使用非聚簇索引也就是二级索引时&#xff0c;叶子节点只存储了索引列的值和主键Id&#xff0c;若要查询其他字段&#xff0c;就要根据主键去聚簇索引查询完整的数据。这个过程就是回表。比如用na…

NeighborGeo:基于邻居的IP地理定位(一)

NeighborGeo:基于neighbors的IP地理定位 X. Wang, D. Zhao, X. Liu, Z. Zhang, T. Zhao, NeighborGeo: IP geolocation based on neighbors, Comput. Netw. 257 (2025) 110896, Abstract IP地址定位在网络安全、电子商务、社交媒体等领域至关重要。当前主流的图神经网络方法…

MySQL 8.0:窗口函数

一、基础知识 定义 窗口函数&#xff08;Window Function&#xff09;对查询结果集的子集&#xff08;“窗口”&#xff09;进行计算&#xff0c;保留原始行而非聚合为单行&#xff0c;适合复杂分析&#xff08;如排名、累积和&#xff09;。 基本语法&#xff1a; 函数名() OV…

AI 深度学习面试题学习

1.神经网络 1.1各个激活函数的优缺点? 1.2为什么ReLU常用于神经网络的激活函数? 1.在前向传播和反向传播过程中,ReLU相比于Sigmoid等激活函数计算量小; 2.避免梯度消失问题。对于深层网络,Sigmoid函数反向传播时,很容易就会出现梯度消失问题(在Sigmoid接近饱和区时,变换…

遇到该问题:kex_exchange_identification: read: Connection reset`的解决办法

kex_exchange_identification: read: Connection reset 是一个非常常见的 SSH 连接错误。它表明在 SSH 客户端和服务器建立安全连接的初始阶段&#xff08;密钥交换&#xff0c;Key Exchange&#xff09;&#xff0c;连接就被对方&#xff08;服务器&#xff09;强制关闭了。 …

(论文蒸馏)语言模型中的多模态思维链推理

&#xff08;论文总结&#xff09;语言模型中的多模态思维链推理 论文名称研究背景动机主要贡献研究细节两阶段框架实验结果促进收敛性摆脱人工标注错误分析与未来前景 论文名称 Multimodal Chain-of-Thought Reasoning in Language Models http://arxiv.org/abs/2302.00923 …

React Native 接入 eCharts

React Native 图表接入指南 概述 本文档详细介绍了在React Native项目中接入ECharts图表的完整步骤&#xff0c;包括依赖安装、组件配置、数据获取、图表渲染等各个环节。 目录 1. 环境准备2. 依赖安装3. 图表组件创建4. 数据获取Hook5. 图表配置6. 组件集成7. 国际化支持8…

基于C#的OPCServer应用开发,引用WtOPCSvr.dll

操作流程&#xff1a; 1.引入WtOPCSvr.dll文件 2.注册服务&#xff1a;使用UpdateRegistry方法注册&#xff0c;注意关闭应用时使用UnregisterServer取消注册。 3.初始化服务&#xff1a;使用InitWTOPCsvr初始化 4.使用CreateTag方法&#xff0c;创建标签 5.读写参数使用下面三…

Java类加载器getResource行为简单分析

今天尝试集成一个第三方SDK&#xff0c;在IDE里运行正常&#xff0c;放到服务器上却遇到了NPE&#xff0c;反编译一看&#xff0c;原来在这一行&#xff1a;String path Test.class.getClassLoader().getResource("").getPath(); // Test.class.getClassLoader().ge…

【CodeTop】每日练习 2025.7.4

Leetcode 1143. 最长公共子序列 动态规划解决&#xff0c;比较当前位置目标和实际字符串的字母&#xff0c;再根据不同情况计算接下来的情形。 class Solution {public int longestCommonSubsequence(String text1, String text2) {char[] t1 text1.toCharArray();char[] t2…

ES6从入门到精通:Promise与异步

Promise 基础概念Promise 是 JavaScript 中处理异步操作的一种对象&#xff0c;代表一个异步操作的最终完成或失败及其结果值。它有三种状态&#xff1a;Pending&#xff08;进行中&#xff09;、Fulfilled&#xff08;已成功&#xff09;、Rejected&#xff08;已失败&#xf…

数据结构:二维数组(2D Arrays)

目录 什么是二维数组&#xff1f; 二维数组的声明方式 方式 1&#xff1a;静态二维数组 方式 2&#xff1a;数组指针数组&#xff08;数组中存放的是指针&#xff09; 方式 3&#xff1a;双指针 二级堆分配 &#x1f4a1; 补充建议 如何用“第一性原理”去推导出 C 中…