Redis学习系列之——高并发应用的缓存问题(二)

发布于:2025-07-16 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、布隆过滤器

        布隆过滤器由一个 BitMap 和若干 Hash 函数组成,可以用来快速判断一个值是否存在后端存储中。它是解决 Redis 缓存穿透问题的一个不错的解决方案。

工作原理

步骤1:当 key-value 键值对存储到 Redis 后,向布隆过滤器添加 key

步骤2:布隆过滤器会对 key 进行多个独立的哈希运算,假设是3个Hash,则得到 hash1、hash2、hash3

步骤3:假设布隆过滤器的 BitMap 长度为 L,那么 BitMap 中的 hash1 % L、hash2 % L、hash3 % L 这三个位会被设置为1

步骤4:若要查询某个 key 是否存在于 Redis 中,可以先查询布隆过滤器。布隆过滤器会检查 hash1 % L、hash2 % L、hash3 % L 这三个位置是否都为1

步骤4.1:若否,则 key 一定不存在于 Redis 中

步骤4.2:若是,由于可能出现 Hash 冲突,因此无法判断 key 是否存在于 Redis 中

特点

1、布隆过滤器无法删除数据,若想要删除只能全量初始化

2、布隆过滤器采用 Bitmap 结构,十分节省空间(假如1亿的key,一般需要10亿长度的Bitmap,这样的 Bitmap 只需要 120MB 左右的空间即可)

3、布隆过滤器适用于数据命中率不高、数据相对固定、数据量大的场景。配合“缓存空值”的策略来对抗缓存穿透时,可以大幅减少 Redis 中空值对应的 key 的数量,从而节约 Redis 空间

示例代码

void init(){
    // 初始化布隆过滤器, 预计元素10万, 误差率 3%,
    // 根据这两个参数会计算出底层 bitmap 的大小及 Hash 函数的数量
    RBloomFilter<String> bloomFilter = redisson.getBloomFilter("bloom1");
    bloomFilter.tryInit(100000, 0.03);
    // 将现有 key 全部初始化
    for(String key : allKeys){
        bloomFilter.put(key);
    }
}

String get(String key){
    // 由布隆过滤器判断是否存在
    if(!bloomFilter.contains(key)){
        return "";
    }
    // 布隆过滤器不能确定, 尝试从缓存中获取
    String value = cache.get(key);
    if(value != null){
        return value;
    }
    // 缓存中不存在, 尝试从数据库获取, 并更新缓存
    value = db.get(key);
    if(value != null){
        cache.set(key, value, 600s)
    } else {
        // 缓存空值
        value = "";
        cache.set(key, value, 30s);
    }
    return value;
}

void add(String key, String value){
    // 新增元素时,除了写入数据库,还需要将 key 写入布隆过滤器
    db.add(key, value);
    bloomFilter.put(key);
}

二、bigkey 问题

定义

        bigkey 是指单值类型(string)大小很大,二级数据结构的大小很大或元素过多。

        一般地,超过 10KB 的 string,或者超过 5000 个元素的 hash/list/set/zset,可以认定为 bigkey。

危害

1、高并发请求大 key 时,容易导致 Redis 阻塞

2、高并发请求大 key 时,容易导致网络拥塞

3、过期删除大 key 时,存在阻塞 Redis 的可能性。(Redis 4.0 之后默认采用过期异步删除,可以一定程度缓解,但是大 key 删除仍有较大的性能开销)

产生原因

        程序设计不当,或对数据规模预估不足。比如:

1、粉丝列表,若没有精心设计,大 V 的粉丝列表容易成为大 key;

2、为了方便,把关联的数据全部放到一个 key 中存储;

优化方法

1、数据分段存储,比如一个大的 list,分成10个小的 list 存储

2、如果 bigkey 不可避免,每次请求尽量取出少量数据,比如用 hmget 代替 hgetall

3、使用合适的数据结构,比如一个大的 JSON 存储为 string 类型,容易成为大 key,使用 hash 可以将数据分摊到多个 field 中。

4、设置 key 的过期时间,避免 Redis 内存不断膨胀

三、命令使用

1、O(N) 操作注意性能问题

        对于 hgetall、smembers、zrange 等等 O(N) 操作,要关注 N 的值。有遍历元素的需求时,可以使用 hscan、sccan、zscan 等代替,这些 scan 命令使用游标进行遍历,一次只返回有限数量的元素以及下一次 scan 的游标,而不是一次性返回全量数据。

2、通过 rename 禁用 keys、flushall、flushdb 等危险命令

3、合理使用 select

        Redis 多数据库的功能比较弱,使用数字区分。多个业务分别使用同一个 Redis 实例的不同数据库,Redis 实际上还是单线程处理。

4、使用批量操作提高效率

        原生命令:例如用 mget/mset 代替多次 get/set;非原生命令:使用 pipeline 打包发送多个命令。

        但也要注意批量操作规模不能太大。

四、客户端连接池

        应用程序使用 Redis 连接池,可以避免连接频繁建立、释放造成的性能损耗。

        使用连接池后,由连接池维护若干连接,负责这些连接的建立、保活、销毁。业务层需要使用连接的时候,从连接池取出直接使用,使用完毕后归还给连接池。

        连接池的关键参数:

1、maxTotal 最大连接数

        maxTotal 的估算要考虑多方因素,比如:

        (1)一次“借出连接 -> 执行命令 -> 归还连接"大约耗时 1ms,那么单个连接的 QPS 就是1000,如果业务要求的 QPS 是 50000,那么至少需要 50 个连接,此时 maxTotal 可以比 50 大一些

        (2)Redis 服务端的最大连接数是 maxClient,则应用节点数量 * maxTotal 不能超过 maxClient

        (3)从性能最优考虑,一般设置为和 maxIdle 相等,可以避免缓存池伸缩带来性能开销。如果考虑连接占用

        (4)maxTotal 不是越大越好,设置合理的值可以限制应用程序消耗 Redis 的资源,而且由于 Redis 工作线程为单线程,一个大命令阻塞的发生时,即使 maxTotal 设置再大也没用

2、maxIdle 最大空闲连接数

        超出 maxIdle 的连接在被归还后会被缓慢释放。

        从性能最优考虑,一般设置为 maxIdle = maxIdle,可以避免缓存池伸缩带来性能开销。如果考虑连接占用问题,maxIdle 一般设为业务预期的最高并发数,maxTotal 再放大一倍。

3、minIdle 最少空闲连接数

        注意,Redis 连接池是懒加载的,因此应用程序启动时,可以用的连接数不会直接到达 minIdle,如果应用启动后会有很多请求过来,那么我们提前执行代码进行预热,使得初始的可用连接数到达 minIdle 个。

4、blockWhenExhausted  连接池没有空闲连接时,调用方是否等待

5、maxWaitMillis  blockWhenExhausted=true 时,调用方等待的最长时间

6、testOnBorrow  连接池是否在借出连接时,检测连接的有效性

7、testOnReturn  连接池是否在归还连接时,检测连接的有效性

五、数据清除策略

        在 Redis 中,maxmemory 参数规定了能够使用的最大内存。

1、已使用内存 < maxmemory

        此时,Redis 会对过期数据进行清除,方式如下:

(1)被动删除:惰性删除,当一个 key 到期时,Redis 不会立刻删除。而是当客户端读/写一个已经过期的 key 时,Redis 进行删除

(2)主动删除:惰性删除无法确保冷数据被及时删除,所以 Redis 会定期主动删除一批已经过期的 key

2、已使用内存 > maxmemory

        此时,Redis 会触发主动清理策略。主动清理策略有 8 种:

(1)针对设置了有效期的 key 进行删除

        a. volatile-ttl:越早过期的数据越先被删除

        b. volatile-random:随机删除

        c. volatile-lru:使用 LRU 算法删除

        d. volatile-lfu:使用 LFU 算法删除

(2)针对所有的 key 进行删除

        e. allkeys-random:随机删除

        f. allkeys-lru:使用 LRU 算法删除

        g. allkeys-lfu:使用 LFU 算法删除

(3)不删除

        h. noeviction:缺省值。不会删除任何数据,并拒绝客户端的所有写操作。

3、配置建议

(1)Redis 默认不限制 maxmemory。生产环境建议配置,并且不要让 maxmemory 超过物理内存大小,否则会触发磁盘 swap 影响性能。

(2)一般推荐使用 volatile-lru;如果热点数据比较多,则用 volatile-lfu


网站公告

今日签到

点亮在社区的每一天
去签到