Redis中Big Key该如何解决?

目录

1、Big Key的产生

2、BigKey场景分析

3、Big Key的危害

4、检测 BigKey 

5、解决 BigKey 问题

Big Key拆分

(1)按时间/业务拆分

(2)按哈希(Hash)拆分

(3)按前缀树拆分

Big Key定期清理

Big key压缩

Big Key批处理

优化持久化配置

6、总结


 Big Key 问题是指某个键(key)的值(value)过大,这会导致 Redis 的性能下降

1、Big Key的产生

  • 业务设计不合理

在设计应用时,如果对数据结构的设计不够精细,可能会导致单个 key 存储的数据量过大,没有合理地拆分成多个较小的 key

如:存储了大量数据的字符串、哈希表(hash)、列表(list)等,这会导致在执行涉及该键的操作时消耗更多的时间和资源。

  • 未及时清理无用数据

如:忘记设置过期时间,没有定期删除过期或不再需要的数据,List结构中数据持续增加而没有弹出数据的机制,那么数据会越来越多

  • 数据类型选择不当

不同的数据类型有不同的内存使用特点。例如,使用字符串类型存储大量数据可能不如使用哈希(HASH)类型来得高效。
选择不适合的数据类型可能导致内存使用效率低下,进而产生 Big Key

如:文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)

  • 动态增长管理不当

某些数据结构可能随着时间和用户行为的变化而不断增长,如果没有适当的管理机制来控制其增长,就可能导致 Big Key 的出现。

如:日志记录、历史数据存储、某个明星热点粉丝列表或者评论的列表等功能如果没有合理的清理策略,可能会导致 key 的数据量逐渐增大

2、BigKey场景分析

以以下常见的场景分析Big Key的问题以及解决方案:

1. 用户行为日志记录

场景描述:应用程序记录用户的行为日志,如点击事件、浏览历史等,通常会将这些数据存储在一个用户的键中。
问题产生:随着用户行为的增多,单一用户的日志数据量会逐渐增大,形成 BigKey。
解决方案:分割日志数据到多个键中,例如按照日期分割。定期归档或清理旧的日志数据

2. 产品目录或商品信息

场景描述:电商平台或零售系统中,每个商品可能包含大量的属性信息,如规格、价格、库存等。
问题产生:如果将所有商品信息都存储在一个键中,随着商品数量的增加,键的大小也会增加。
解决方案:将商品信息拆分为多个键,例如按类别或品牌存储。使用更高效的数据结构,如使用哈希表存储商品的属性

3. 社交网络的好友关系

场景描述:社交应用中,每个用户都有自己的好友列表。
问题产生:当用户的好友数量非常多时,存储好友列表的键可能会变得非常大。
解决方案:将好友列表拆分为多个键,例如按照字母顺序或好友活跃度进行分组。使用有序集合(Sorted Set)存储好友列表,便于快速查找和排序

4. 会话状态存储

场景描述:Web 应用程序中,会话状态通常需要在服务器端存储。
问题产生:如果每个用户的会话状态包含了大量数据,如购物车、登录信息等,会话状态键可能会变得非常大。
解决方案:将会话状态拆分为多个键,例如将购物车和其他状态分开。定期清理过期的会话状态。

3、Big Key的危害

  • 性能下降

对 BigKey 执行批量操作,如 HGETALLHSETALL,这些命令的时间复杂度通常是 O(N),其中 N 是键中元素的数量。当 N 很大时,这些操作会变得非常耗时,尤其是在 Redis 的单线程模型下

  • 内存占用

BigKey 占用过多的内存,可能导致 Redis 的整体内存使用过高,从而触发内存淘汰策略,影响其他键的性能

  • 网络拥塞

BigKey 在网络上传输时会占用较多带宽,特别是当客户端频繁读取或写入 BigKey 时,可能会导致网络拥塞。

  • 超时阻塞

Redis 的主要命令执行是在单线程中完成的,这意味着在执行 BigKey 相关操作时,其他客户端的请求会被阻塞,直到该操作完成

  • 备份和恢复时间增加

Big Key 会使 RDB 快照文件或 AOF 日志文件增大,从而增加备份和恢复所需的时间。

大量的 Big Key 存在时,可能会导致备份和恢复过程变得非常缓慢

  • 复制延迟/删除异常

在主从复制场景中,Big Key 的复制会消耗更多的时间,导致从节点的延迟增加

当Big Key 过期需要删除时,由于数据量过大,可能发生主库较响应时间过长,主从数据同步异常(删除掉的数据,从库还在使用)

4、检测 BigKey 

  • 使用 redis-cli 工具:可以通过 redis-cli 工具来检测 BigKey
  • 使用 Redis 自带的命令:Redis 提供了 BIGKEYS 命令来查询当前 Redis 中所有 key 的信息,帮助统计分析键值对的大小情况

 使用以下命令扫描Redis中所有的键,并返回前1000个Big Key

redis-cli --scan --pattern '*' --count 1000
redis-cli -a "password" -- bigkeys

5、解决 BigKey 问题

  • Big Key拆分

将大键拆分成多个小键,使用更合适的数据结构来存储数据

拆分方式:

(1)按时间/业务拆分

如果 Big Key 包含的是按时间顺序排列的数据,可以考虑按时间范围拆分

(2)按哈希(Hash)拆分

使用哈希函数将大 Key 拆分成多个小 Key,并将其存储在不同的 Redis 实例中

(3)按前缀树拆分

数据结构与算法——前缀树-CSDN博客

使用前缀树将大 Key 拆分成多个层级结构,每个层级的 Key 都更小

前缀树是一种树形数据结构,用于高效地存储和检索字符串集合

🌰:假设有一个 Big Key 用于存储每天的用户登录记录按照Hash拆分

按照Hash拆分:
HashExample中- 使用 HashMap 来存储键值对。
                           - insert 方法用于插入以 user: 开头的键值对。
                           - get 方法用于查找特定键的值。
                           - printKeysWithPrefix 方法用于打印所有以特定前缀开头的键

public class HashExample {
    private Map<String, String> hashTable;

    public HashExample() {
        hashTable = new HashMap<>();
    }

    // 插入键值对
    public void insert(String key, String value) {
        if (key.startsWith("user:")) {
            hashTable.put(key, value);
        } else {
            System.out.println("只允许以 'user:' 开头的键。");
        }
    }

    // 查找键
    public String get(String key) {
        return hashTable.get(key);
    }

    // 查找以特定前缀开头的所有键
    public void printKeysWithPrefix(String prefix) {
        System.out.println("以 '" + prefix + "' 开头的键:");
        for (String key : hashTable.keySet()) {
            if (key.startsWith(prefix)) {
                System.out.println(key + ": " + hashTable.get(key));
            }
        }
    }

    public static void main(String[] args) {
        HashExample hashExample = new HashExample();
        hashExample.insert("user:123", "User 123 Data");
        hashExample.insert("user:456", "User 456 Data");
        hashExample.insert("admin:001", "Admin Data"); // 不会插入

        // 查找以 "user:" 开头的所有键
        hashExample.printKeysWithPrefix("user:");
    }
}

按照前缀树拆分

那么按照前缀树实现来存储和查询以 "user:" 开头的键,该如何实现呢?

首先,需要定义一个前缀树节点的类

TrieNode类中每个节点包含一个字符的子节点映射( children )和一个布尔值( isEndOfKey ),指示该节点是否为一个完整的键

class TrieNode {
    Map<Character, TrieNode> children;
    boolean isEndOfKey;

    public TrieNode() {
        children = new HashMap<>();
        isEndOfKey = false;
    }
}

接下来,定义一个前缀树类:
Trie 类:  - insert 方法用于插入一个键
                - collectKeysWithPrefix 方法用于查找以特定前缀开头的所有键
                - collectKeys 方法使用深度优先搜索(DFS)遍历节点,收集所有完整的键

class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // 插入键
    public void insert(String key) {
        TrieNode node = root;
        for (char ch : key.toCharArray()) {
            node.children.putIfAbsent(ch, new TrieNode());
            node = node.children.get(ch);
        }
        node.isEndOfKey = true;
    }

    // 查找以特定前缀开头的所有键
    public List<String> collectKeysWithPrefix(String prefix) {
        TrieNode node = root;
        for (char ch : prefix.toCharArray()) {
            if (!node.children.containsKey(ch)) {
                return new ArrayList<>(); // 如果前缀不存在,返回空列表
            }
            node = node.children.get(ch);
        }

        List<String> keys = new ArrayList<>();
        collectKeys(node, prefix, keys);
        return keys;
    }

    // 深度优先搜索,收集所有以特定前缀开头的键
    private void collectKeys(TrieNode node, String currentPrefix, List<String> keys) {
        if (node.isEndOfKey) {
            keys.add(currentPrefix);
        }
        for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
            collectKeys(entry.getValue(), currentPrefix + entry.getKey(), keys);
        }
    }
}

最后,使用前缀树来存储和查询以 "user:" 开头的键:

public class Main {
    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insert("user:1", "value1");
        trie.insert("user:2", "value2");
        trie.insert("user:3", "value3");
        trie.insert("admin:1", "value4");

        // 查找以 "user:" 开头的所有键
        List<String> userKeys = trie.collectKeysWithPrefix("user:");
        System.out.println("以 'user:' 开头的键: " + userKeys);
    }
}

Big Key定期清理

异步删除:定期清理不再使用的 BigKey,以释放内存空间

Redis 4.0+ 可以使用 UNLINK 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 SCAN 命令结合 DEL 命令来分批次删除

设置TTL:对于经常访问的大键,可以考虑使用缓存策略LRU(最近最少使用)来减轻 Redis 的负担

关于缓存策略可以看这篇文章:Redis的过期策略以及内存淘汰机制-CSDN博客

Big key压缩

对于文本数据,可以考虑使用压缩算法来减小存储空间

Redis 支持使用 LZFQUICKLZGZIP 算法进行压缩

Big Key批处理

如果 Big Key 不能避免,可以考虑使用分批处理的方式来读取和更新数据,比如使用 SCAN 命令

优化持久化配置

RDB:调整 RDB 的备份频率和条件,减少 Big Key 对 RDB 文件大小的影响。
AOF:启用 AOF 的压缩功能,减少 AOF 文件的大小

🔴建议:

  • 避免使用非常长的 Key:理想情况下,Key 的长度应小于 100 字节。
  • 使用复合键:使用多个字段(例如,用户 ID 和帖子 ID)组合成一个键,可以减少单个键的长度。
  • 使用子列表:将大列表拆分成多个子列表,每个子列表的元素较少。
  • 使用 HyperLogLog:HyperLogLog 是一种概率数据结构,可以近似计算大集合中的唯一元素数量,它占用的空间非常小,例如统计页面 UV、网站访问者的唯一 IP 地址、用户的唯一 ID等

6、总结

Redis的Big Key会给Redis带来的意想不到的危害,需要监控、及时发现和处理Big Key。在开发过程中需要选用适当的Redis数据结构开发业务,尽量避免big key

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值