Redis

数据结构:
  1. 字符串(String)

    • 应用场景:适用于简单的键值对存储,如缓存网页内容、计数器等。
    • 底层实现:基于简单动态字符串(SDS, Simple Dynamic String)。SDS允许高效地追加和修改字符串,并且在内部维护了长度信息,使得获取字符串长度的操作时间复杂度为O(1)。
  2. 哈希表(Hash)

    • 应用场景:用于存储对象,例如用户信息(包含字段如姓名、年龄等),或作为小型数据库表的映射。
    • 底层实现:使用字典(hash table)实现。对于小数量的键值对,可能会使用ziplist(压缩列表)来节省空间;当元素数量超过配置阈值时,会转换为更通用但可能占用更多内存的hash table表示形式。
  3. 列表(List)

    • 应用场景:适用于构建队列、栈,或是需要保持插入顺序的数据集合,如评论系统中的最新评论列表。
    • 底层实现:可以使用双端链表(linked list)或者ziplist。对于较小的列表,Redis倾向于使用ziplist以减少内存消耗;随着列表的增长,它将转换为更高效的双端链表形式,以便于快速地从两端进行插入和删除操作。
  4. 集合(Set)

    • 应用场景:适合用于保证数据唯一性、成员关系测试以及执行集合运算(如并集、交集和差集)的场景,比如社交网络中的好友推荐功能。
    • 底层实现:通常使用整数集合(intset)或哈希表(hash table)。如果集合内的所有元素都是整数且数量不多,Redis会优先使用intset;否则,将采用hash table实现。
  5. 有序集合(Sorted Set/ZSet)

    • 应用场景:适用于需要按照某个评分排序的数据,如排行榜、带权重的消息队列等。
    • 底层实现:通过跳表(skip list)和哈希表共同实现。跳表支持快速查找和范围查询,而哈希表则用来确保元素的唯一性。
大Key:

问题原因:

  • Redis进行大key数据操作耗时会增加
  • 占用内存高
  • redis单线程处理客户端请求,大key导致主线程占用过久,阻塞其他请求
  • 大key增加RDB快照创建与AOF重写的时间,也可能导致主从复制变慢
  • 网络IO需要更多时间

解决方案:

  • 识别:redis提供redis-cli --bigkeys命令扫描数据库以发现大key,或利用第三方工具如redis-rdb-tools分析RDB文件来识别大key。
  • 设计:设计尽量避免大key,可以将key拆分,对于集合类型也可以考虑分片。
  • 现行:对于已有大key,可以逐步迁移到新结构;使用批量操作而不是一次性扫描全部数据;设置合理的TTL,定期检查并清除
热Key:

一般指某个指定节点的Key,由于流量高导致节点负载过高,能提前预期规避就规避,否则需要进行热点探测进行相应处理

  1. 使用本地缓存存储热点
  2. 将热Key拆分为多个子Key
  3. 对热Key进行限流
  4. 热点探测
事务:

常用命令:

  • multi:开启一个事务,multi 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中。
  • exec:执行队列中所有的命令
  • discard:中断当前事务,然后清空事务队列并放弃执行事务
  • watch key1 key2 ... :监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

Redis支持事务,但是和传统关系型数据库的事务不同,区别:

  • 原子性:Redis中事务保证的是一个队列中的命令作为一个整体,要么全部执行,要么全部不执行,即使有命令执行失败,不会影响执行成功的命令(可以使用lua脚本或单个命令保证原子性)。
  • 无隔离级别:Redis中事务的命令在提交前不会被实际执行,所以不存在事务隔离级别问题(客户端发送muti命令后,命令会放入队列中,只有exec命令发送后才开始执行)。
  • 乐观锁:Redis使用watch命令实现乐观锁,如果检测某些键在exec前被修改,则事务不会被执行并报错给客户端。
Redis变慢:
  1. 缓存批量过期,Redis进行主动删除时,其他任务必须等待(不会在慢日志记录)【随机过期时间+可以使用lazy-free后台删除】
  2. 内存上限:redis内存淘汰策略如下,淘汰也会阻塞其他操作

    • 【有过期时间/全量key】LRU淘汰Key
    • 【有过期时间/全量key】随机淘汰Key
    • 【有过期时间/全量key】LFU淘汰Key
    • 【有过期时间/全量key】淘汰即将过期的Key
    • 不淘汰,新写入返回错误
  3. 持久化导致:Redis进行RDB或AOF rewrite后,主进程会fork子进程去拷贝自己的页表【由于是操作页表,所以获取数据会快,可以使用INFO 查看latest_fork_usec】;刷盘时,如果磁盘IO过高,也会阻塞主线程的write调用
  4. 内存大页:常规页大小4KB,开启大页后会增加,但是耗时也会增加;Redis使用写时复制,fork期间即使少量数据修改也会申请一个大页
  5. Swap分区:OS的Swap分区,会用磁盘缓存部分内存
  6. redis内存碎片整理:也在主线程执行
  7. 网络IO:网络带宽等导致
使用scan获取所有key:
import redis.clients.jedis.Jedis;  
import redis.clients.jedis.ScanParams;  
import redis.clients.jedis.ScanResult;  

import java.util.List;  

public class RedisKeyScanner {  

    public static void main(String[] args) {  
        // 建立 Redis 连接  
        Jedis jedis = new Jedis("localhost", 6379);  

        // 初始化 SCAN 游标  
        String cursor = ScanParams.SCAN_POINTER_START;  

        // 扫描参数  
        ScanParams scanParams = new ScanParams();  
        scanParams.match("*"); // 匹配所有模式,可以指定模式  
        scanParams.count(10); // 每次扫描返回的个数,这个值可调  

        do {  
            // 使用 SCAN 命令遍历键  
            ScanResult<String> scanResult = jedis.scan(cursor, scanParams);  
            List<String> keys = scanResult.getResult();  
            cursor = scanResult.getCursor();  

            // 处理每次返回的键  
            for (String key : keys) {  
                System.out.println("Found key: " + key);  
            }  

        } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); // SCAN 命令从头到尾遍历  

        // 关闭 Redis 连接  
        jedis.close();  
    }  
}
最后修改于:2025年02月22日 15:35

添加新评论