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

高并发|数据唯一性 基于Redis缓存的数据去重方法,利用Redis实现高效缓存去重

用Redis打造一把"数据筛子"

场景:每秒万次请求下的重复数据困扰

"小王,咱们的优惠券发放系统又被刷爆了!"凌晨两点,运维同事的电话把刚入睡的小王惊醒,打开监控一看,系统正在经历一波恶意刷券攻击——同一个用户ID在短短1秒内发起了上百次领取请求,而系统竟然全都处理了。

这不是小王第一次遇到这种问题,在电商大促、秒杀活动等高并发场景下,重复请求就像野草一样疯长,如何在海量请求中快速识别并过滤重复数据?今天我们就来聊聊用Redis这把"瑞士军刀"解决数据去重的实战方案。

为什么Redis是去重的首选方案?

在传统方案中,我们可能会选择数据库唯一索引或者应用层缓存来判断数据是否重复,但在高并发场景下,这些方案都存在明显短板:

  • 数据库方案:频繁的IO操作会成为系统瓶颈,特别是在分布式环境下,唯一索引可能引发死锁
  • 本地缓存方案:无法在集群环境下共享状态,各节点间的去重可能不一致

Redis之所以成为去重利器,核心在于三点:

高并发|数据唯一性 基于Redis缓存的数据去重方法,利用Redis实现高效缓存去重

  1. 内存级速度:微秒级的读写响应
  2. 原子操作:保证高并发下的线程安全
  3. 丰富数据结构:提供了多种适合去重的数据模型

Redis去重的四种实战姿势

方案1:Set集合 - 简单直接的"黑名单"

import redis
r = redis.Redis(host='localhost', port=6379)
def check_duplicate(user_id, coupon_id):
    key = f"coupon:{coupon_id}:users"
    # 如果用户已存在集合中,返回True表示重复
    return r.sismember(key, user_id)
def add_user(user_id, coupon_id):
    key = f"coupon:{coupon_id}:users"
    r.sadd(key, user_id)
    # 设置24小时过期
    r.expire(key, 86400)

适用场景:中小规模去重(单个集合建议不超过1万元素) 优点:实现简单,内存占用较小 注意点:大数据量时考虑分片,比如按用户ID哈希分桶

方案2:Bloom Filter - 海量数据下的空间优化大师

当需要处理亿级数据去重时,布隆过滤器是更经济的选择:

from redisbloom.client import Client
rb = Client()
# 初始化过滤器(预计处理1亿条数据,误判率0.1%)
rb.bfCreate('coupon_users', 0.001, 100000000)
def check_duplicate(user_id):
    return rb.bfExists('coupon_users', user_id)

适用场景:允许极小概率误判的超大规模去重 优点:空间效率极高,1亿数据仅需约114MB内存 注意点:存在误判可能,且不支持删除操作

高并发|数据唯一性 基于Redis缓存的数据去重方法,利用Redis实现高效缓存去重

方案3:String+过期时间 - 轻量级的临时去重

def check_duplicate(request_id):
    key = f"req:{request_id}"
    # setnx原子操作:key不存在时设置并返回1,已存在返回0
    return not r.setnx(key, 1)
def set_expire(request_id, ttl=60):
    r.expire(f"req:{request_id}", ttl)

适用场景:短时效的请求去重(如60秒内防重) 优点:极其轻量,适合高频短生命周期场景 注意点:需要合理设置TTL避免内存堆积

方案4:HyperLogLog - 去重计数器的不二之选

当需要统计不重复元素数量时:

def count_unique_visitors(page_id, user_id):
    key = f"page:{page_id}:uv"
    r.pfadd(key, user_id)
def get_uv_count(page_id):
    return r.pfcount(f"page:{page_id}:uv")

适用场景:只需要计数不关心具体内容的场景 优点:12KB内存可统计2^64个不重复元素 注意点:有约0.81%的标准误差

高并发|数据唯一性 基于Redis缓存的数据去重方法,利用Redis实现高效缓存去重

生产环境中的进阶技巧

分布式锁防击穿

def safe_check_duplicate(request_id):
    lock_key = f"lock:{request_id}"
    try:
        # 获取分布式锁(10秒超时)
        if r.set(lock_key, 1, nx=True, ex=10):
            return _real_check_duplicate(request_id)
        else:
            time.sleep(0.1)
            return safe_check_duplicate(request_id)
    finally:
        r.delete(lock_key)

Lua脚本保证原子性

-- 检查并设置的原子操作
local key = KEYS[1]
local value = ARGV[1]
local ttl = tonumber(ARGV[2])
local exists = redis.call("EXISTS", key)
if exists == 0 then
    redis.call("SET", key, value)
    redis.call("EXPIRE", key, ttl)
    return 0
else
    return 1
end

内存优化配置

# redis.conf 关键配置
hash-max-ziplist-entries 512
set-max-intset-entries 512
activerehashing yes

避坑指南:这些雷区不要踩

  1. 大Key问题:单个Set存储超过1万元素会显著影响性能
  2. 雪崩效应:批量操作时给不同Key设置随机过期时间
  3. 持久化丢失:对准确性要求高的场景配合AOF持久化
  4. 网络开销:批量使用pipeline减少往返时延

性能实测对比(基于Redis 7.2)

方案 10万QPS延迟 内存占用(100万数据) 准确性
Set 3ms ~80MB 100%
Bloom Filter 5ms ~1.2MB 9%
String 2ms ~60MB 100%
HyperLogLog 4ms ~12KB ≈99.2%

选择哪种Redis去重方案,就像选择不同的筛子——Set是精密的金属滤网,Bloom Filter是高效的大孔渔网,HyperLogLog则是聪明的统计学家,根据你的业务场景(数据规模、准确性要求、时效需求)选择合适的工具,才能在高并发洪流中游刃有余。

没有最好的方案,只有最适合的方案,下次当你面对海量重复数据时,不妨先问问自己:这次该用Redis的哪种"筛法"?

发表评论