当前位置:首页 > 问答 > 正文

短信平台 发送频控 基于Redis实现短信发送频率限制,redis短信接口限流

用Redis给短信发送接口上把锁

场景引入:短信轰炸的烦恼

"叮咚~"王经理的手机又响了,这已经是今天第20条验证码短信。"我就注册个账号而已,怎么验证码发起来没完了?"他皱着眉头删掉了短信,运维小张正盯着监控大屏上火红的告警线发愁——短信接口又被刷爆了,这个月成本又超标了...

这种场景在互联网公司太常见了,短信作为重要的用户触达通道,既要保证正常业务需求,又要防止恶意刷量,今天我们就来聊聊,如何用Redis这把"瑞士军刀"实现短信发送频率控制。

为什么选择Redis做频控?

Redis简直是频控场景的"天选之子":

  • 快如闪电:10万+ QPS的吞吐量,应对高并发小菜一碟
  • 原子操作:INCR、EXPIRE等指令保证计数准确
  • 丰富数据结构:String、Hash、Sorted Set各显神通
  • 过期特性:自动清理过期数据,省心省力

基础版:计数器限流法

最朴素的实现方式——给每个手机号配个计数器:

def can_send_sms(phone):
    key = f"sms_limit:{phone}"
    # 60秒内最多发1条
    count = redis.incr(key)
    if count == 1:
        redis.expire(key, 60)
    return count <= 1

这个方法简单粗暴,但有个明显漏洞:如果在60秒的最后1秒和下一个60秒的第1秒各发1条,实际上2条短信间隔可能只有2秒,但系统却认为合规。

升级版:滑动窗口限流

更科学的实现应该用滑动窗口,Redis的Sorted Set大显身手的时候到了:

短信平台 发送频控 基于Redis实现短信发送频率限制,redis短信接口限流

def can_send_sms_v2(phone):
    now = time.time()
    key = f"sms_window:{phone}"
    # 移除1分钟前的记录
    redis.zremrangebyscore(key, 0, now - 60)
    # 当前请求加入集合
    redis.zadd(key, {str(now): now})
    # 设置key过期时间避免内存泄漏
    redis.expire(key, 60)
    # 检查1分钟内发送次数
    return redis.zcard(key) <= 5  # 假设每分钟限5条

这个方案精准控制了任意60秒窗口内的发送次数,但相对耗资源,我们实测发现,在QPS 5000+的场景下,Redis CPU会涨到30%左右。

生产级优化方案

结合多家大厂实战经验,推荐这个混合方案:

def can_send_sms_pro(phone, biz_type):
    # 分级控制键
    minute_key = f"sms:{biz_type}:{phone}:minute"
    hour_key = f"sms:{biz_type}:{phone}:hour"
    day_key = f"sms:{biz_type}:{phone}:day"
    # 多级检查(使用管道提升性能)
    with redis.pipeline() as pipe:
        pipe.incr(minute_key)
        pipe.incr(hour_key)
        pipe.incr(day_key)
        # 如果是首次设置,初始化过期时间
        pipe.expire(minute_key, 60)
        pipe.expire(hour_key, 3600)
        pipe.expire(day_key, 86400)
        m_cnt, h_cnt, d_cnt = pipe.execute()[:3]
    # 分级阈值检查(不同业务类型可配置不同阈值)
    limits = {
        'verify': {'minute':1, 'hour':5, 'day':10},  # 验证码类
        'notify': {'minute':3, 'hour':20, 'day':50}  # 通知类
    }
    return (m_cnt <= limits[biz_type]['minute'] and 
            h_cnt <= limits[biz_type]['hour'] and 
            d_cnt <= limits[biz_type]['day'])

这个方案有三大亮点:

  1. 业务分级:验证码、营销通知等不同业务配置不同阈值
  2. 多级控制:分钟、小时、天三级防护,兼顾用户体验和安全
  3. 管道操作:减少网络往返,性能提升40%+

突发流量处理技巧

遇到秒杀等场景时,可以引入"令牌桶"算法:

短信平台 发送频控 基于Redis实现短信发送频率限制,redis短信接口限流

def get_sms_token(phone):
    key = f"sms_token:{phone}"
    # 令牌桶参数
    capacity = 10  # 桶容量
    rate = 0.5     # 每秒补充0.5个令牌
    with redis.pipeline() as pipe:
        while True:
            try:
                pipe.watch(key)
                current = float(pipe.get(key) or capacity)
                last_time = float(pipe.hget(key, 'last_time') or time.time())
                # 计算新增令牌数
                now = time.time()
                new_tokens = (now - last_time) * rate
                current = min(capacity, current + new_tokens)
                if current >= 1:  # 有令牌可用
                    pipe.multi()
                    pipe.set(key, current - 1)
                    pipe.hset(key, 'last_time', now)
                    pipe.execute()
                    return True
                return False
            except WatchError:
                continue

监控与调优建议

  1. 监控指标

    • 限流拦截率(健康业务应<5%)
    • Redis内存增长趋势
    • 限流接口响应时长(建议<10ms)
  2. 参数调优

    • 根据业务特点调整时间窗口(验证码建议1-5分钟窗口)
    • 动态调整阈值(如夜间可适当放宽营销短信限制)
  3. 异常处理

    try:
        if not can_send_sms(phone):
            log.warning(f"频控拦截:{phone}")
            return {"code": 429, "msg": "发送太频繁"}
    except RedisError:
        # Redis故障时降级处理
        if config.allow_failover:
            log.error("Redis异常,降级放行")
            return True
        raise

踩坑实录

去年双十一我们遇到过这些坑:

短信平台 发送频控 基于Redis实现短信发送频率限制,redis短信接口限流

  • 坑1:没有区分业务类型,导致验证码短信被营销短信挤占
  • 坑2:使用DEL命令清除计数器,造成瞬间流量穿透
  • 坑3:未设置Redis连接超时,网络抖动时接口卡死

对应的解决方案:

  1. 业务键前缀分离
  2. 改用EXPIRE替代DEL
  3. 添加connect_timeout配置

用Redis实现短信频控就像给高速路口装ETC——既要保证合法车辆快速通行,又要拦截违规车辆,关键技术点:

  • 选择合适的数据结构(String计数器起步,Sorted Set更精准)
  • 多级时间窗口配置(分钟+小时+天)
  • 异常情况下的降级策略
  • 持续监控和动态调整

最后提醒:任何技术方案都要结合业务实际,比如教育类APP的课程提醒和电商的秒杀通知,频控策略就应该有所区别,现在就去检查你们的短信平台,别让"短信轰炸"毁了用户体验!

发表评论