Redis基础

前言

        Redis(Remote Dictionary Server)是一个开源的高性能键值对(key-value)数据库,也称为远程字典服务。它通常被用作数据结构服务器,因为它支持各种类型的数据结构。Redis 以其卓越的性能和低延迟特性而闻名,非常适合用作数据库、缓存系统和消息代理。 Redis作为一种服务,使用TCP连接通信,每个命令都是TCP请求,然后Redis再进行TCP回应。

        Redis特点:

  • 内存数据库:Redis 将所有数据存储在内存中,这使得它能够提供极快的读写速度。
  • 键值对数据库:使用key操作value,所有对Redis的数据操作都要用到与之对应的key。
  • 数据结构数据库:Redis的value又可以是多种数据结构,如string、list、hash、zset、set等。

数据结构与应用

        命令不需要刻意去记,而是要多使用,干记是没有用的,可以参考命令手册或者问AI,因为我如果写命令,这篇博客就是操作手册了,画蛇添足罢了。五种常用数据结构如下:

  • string:与c++中的std::string类似的二进制安全字符串,不同于c字符串的以\0作为分隔符,而是以长度作为分隔符,这样就能存储二进制字符串,图片视频等。
  • hash:哈希表,最外层是哈希表,value本身也是哈希表,也就是Key-<key,value>,但是value都是string类型,存int也是string。
  • list:双向循环列表,本身有序,按照插入先后顺序存储,不去重。
  • set:集合,去重,无序。
  • zset:有序集合,用score字段进行排序。

        大家可以直接搜索redis源码,redis是开源的,而且设计非常巧妙,值得去细品。我们接下来讨论redis常用的几种数据结构的底层及应用。

string

        string底层是动态数组。对于字符串内存空间申请,redis会选择性使用几种不同长度的结构体(pow(2,16),pow(2,32),pow(2,64)),以便合理分配内存,每个结构内部有一个柔性数组,这样只会free一次,如果是char*就需要free外部的结构体,并且free char*数组。

/* 针对不同的字符串设置了不同的结构体,主要差别在于len和alloc的数据类型,不同长度使用
 * 不同的数据类型,以达到节省内存的目的。  
 *  
 * 注意:sdshdr5从未被使用过,我们只是直接访问flag。但是,这里记录下sdshdr5的结构。 */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 已使用空间大小 */
    uint8_t alloc; /* 总共可用的字符空间大小,应该是实际buf的大小减1(因为c字符串末尾必须是\0,不计算在内) */
    unsigned char flags; /* 标志位,主要是识别这是sdshdr几,目前只用了3位,还有5位空余 */
    char buf[];  /* 真正存储字符串的地方 */
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

        string虽然功能单一,但是玩法却sao的很。

  • 存储对象:string可以用于存储对象,比如MySQL的row、JSON等等,但是如果value经常会改变,不适合用string存储。
  • 累加器:可以用incr或incrby命令对value进行累加操作。
  • 分布式锁:用redis实现非公平锁,setnx lock 1占用锁,不存在才能设置,返回1加锁成功,还可以设置过期时间。
  • 位运算:使用setbit可进行位运算,比如实现签到功能,按日历每个月签到,可以使用位运算标记。用bitcount可统计签到次数。

list

        list用双向链表实现,首尾操作时间复杂度为O(1),查询中间元素时间复杂度为O(n)。内部可能会进行压缩操作,元素长度小于48则不压缩,否则会压缩,但是压缩后长度与压缩前长度差不超过8就不会压缩。

/* quicklistNode 中用了32个字节存储一个节点,三个指针总计24字节,加sz和后面按位用的几个int,总计32字节
 * 用了32个bit来保存ziplist的信息。 
 * count: 16位,最大65536(最大ziplist的大小是65k,所以实际上count小于32k)
 * encoding: 2位,RAW=1, LZF=2. 
 * container: 2位,NONE=1, ZIPLIST=2. 
 * recompress: 1位,bool类型,如果是true表示当前节点是临时使用的解压后的节点. 
 * attempted_compress: 1位, boolean类型,基本是在测试中使用  
 * extra: 用剩下的10位,暂时没有用到,留给之后的feature */
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;           /* quicklist节点对应的ziplist */
    unsigned int sz;             /* ziplist的字节数 */
    unsigned int count : 16;     /* ziplist的item数*/
    unsigned int encoding : 2;   /* 数据类型,RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* 这个节点以前压缩过吗? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* 未使用到的10位 */
} quicklistNode;

/* quicklist是一个40字节的结构体(在64位系统中),具体字段如下: 
 * count: quicklist中所有数据项的个数
 * len:quicklist中的节点数  
 * compress: quicklist的压缩深度,0表示不压缩,否则就表示从两端开始有多少个节点不压缩
 * bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间 */
typedef struct quicklist {
    quicklistNode *head;        /* 头结点 */
    quicklistNode *tail;        /* 尾结点 */
    unsigned long count;        /* 在所有的ziplist中的entry总数 */
    unsigned long len;          /* quicklist节点总数 */
    int fill : QL_FILL_BITS;              /* 16位,每个节点的最大容量*/
    unsigned int compress : QL_COMP_BITS; /* 16位,quicklist的压缩深度,0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩 */
    unsigned int bookmark_count: QL_BM_BITS;  /*4位,bookmarks数组的大小,bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间*/
    quicklistBookmark bookmarks[];
} quicklist;

        list的用法:

  • 命令组合:可以通过命令组合实现栈、队列等数据结构,比如LPUSH+LPOP实现栈,LPUSH+RPOP实现队列,LPUSH+BRPOP实现阻塞队列(队列为空时则阻塞到有元素可用,用于生产者消费者场景)。
  • 获取固定窗口记录:使用lpush+ltrim可确保list只保留指定长度的最近记录,可用于实现最近评论,最近战绩等。

hash

        在redis中最多存在两层hash,也就是外部的key接hash数据结构,内部hash的value就不能是hash了。如果值经常会改变就需要使用hash,因为string会将值加密解密。Redis 的 Hash 数据类型用于存储键值对集合,它可以被视为包含多个字段的单一值。在 Redis 中,Hash 数据类型是一个非常灵活的结构,因为它允许你存储和管理对象属性,而不需要将每个属性作为独立的键存储。

  • ziplist(压缩列表):这是早期 Redis 版本中 Hash 的默认内部编码方式。ziplist 是一种紧凑的列表编码方式,它将所有元素存储在一块连续的内存空间中。这种方式适合存储小的 Hash 结构,因为它可以极大地节省内存。但是,当 Hash 中的字段或值较大时,ziplist 的性能会下降,因为它需要在内存中移动元素。

  • hashtable(哈希表):随着 Redis 版本的更新,当 Hash 中的元素数量或元素大小超过一定阈值时,Redis 会将 ziplist 转换为 hashtable。这种结构由数组和链表组成,提供了更好的性能,尤其是在处理大量数据时。hashtable 内部使用多个散列表来减少哈希冲突,并动态调整大小以优化性能。

  • quicklist:在 Redis 3.0 及以后的版本中,List 数据类型开始使用 quicklist 作为内部编码,这是一种结合了压缩列表(ziplist)和普通链表(linkedlist)的数据结构。虽然这是 List 数据类型的内部编码,但值得注意的是,Hash 数据类型在超过一定阈值后,也会从 ziplist 转换为基于 quicklist 的结构,以提高性能和内存利用率。

typedef struct {
    /* When string is used, it is provided with the length (slen). */
    unsigned char *sval;
    unsigned int slen;
    /* When integer is used, 'sval' is NULL, and lval holds the value. */
    long long lval;
} ziplistEntry;

        hash一般与其他结构组合使用:

  • 购物车:物品信息是会经常改变的,就不能用string,物品按加入的先后顺序排序,就需要使用list,这时候可以list+hash组合使用。
  • 游戏玩家信息:在线玩家可以使用set存储在线的玩家,而要知道用户属性,可以用hash+set。

set

        set本身是无序集合,但底层确是有序的,这样集合做交并差操作更方便。它类似于数学中的集合,可以存储一组不重复的值。Set 数据类型在 Redis 中的内部实现也采用了不同的数据结构,以优化内存使用和操作性能。

  • intset(整数集合):这是 Set 数据类型的早期内部编码方式,适用于存储整数元素的集合。intset 是一个有序数组,它可以存储整数值,并且保持有序,这使得范围查询变得非常高效。intset 适合存储小的、包含整数值的集合,因为它在内存使用上非常高效。但是,当集合中的元素数量增加时,intset 的性能会下降,因为它需要在数组中移动元素。

  • hashtable(哈希表):当 Set 集合中的元素数量或元素的类型超过一定阈值时,Redis 会将 intset 转换为 hashtable。这种结构使用散列表来存储元素,提供了更好的性能,尤其是在处理大量数据或非整数值时。hashtable 内部使用多个散列表来减少哈希冲突,并动态调整大小以优化性能。

  • quicklist:虽然 quicklist 主要是 List 数据类型的内部编码,但在某些情况下,Redis 的 Set 数据类型也可能使用类似 quicklist 的结构,尤其是当集合中的元素数量非常大时。这种结构结合了压缩列表(ziplist)和普通链表(linkedlist)的优点,以提高性能和内存利用率。

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

        set用法:

  • 抽奖:set表面是无序唯一的,就可以用来作抽奖。
  • 共同关注:使用集合计算操作选出共同关注。

zset

         Zset(有序集合)是一个复杂的数据结构,它结合了 Set(集合)的特性,并为每个元素增加了一个排序属性 score(分值)。Zset 允许你存储不重复的成员,并且可以根据 score 进行排序。Zset 的内部实现使用了两种不同的数据结构:ziplist(压缩列表)和 skiplist(跳跃表)。

  • ziplist(压缩列表):当 Zset 满足以下条件时,Redis 使用 ziplist 作为底层实现:

    • 有序集合保存的元素个数要小于 128 个;
    • 有序集合保存的所有元素成员的长度都必须小于 64 字节。 ziplist 是一种紧凑的存储方式,它将所有的元素和分数紧密地存储在一起,形成了一个有序的列表。这种方式在元素数量较少且元素长度较短时非常高效,因为它减少了内存的使用。
  • skiplist(跳跃表):当 Zset 不满足使用 ziplist 的条件时,Redis 会使用 skiplist 作为底层结构。Skiplist 是一种随机化的数据结构,它通过在有序链表上增加多级索引来提高查找效率。Skiplist 允许快速的查找、插入和删除操作,时间复杂度为 O(logN)。在 Redis 的实现中,skiplist 通常由多层链表组成,每一层都是一个有序的链表,上层的链表是对下层的索引。这样可以在高层进行快速查找,然后迅速定位到下层的具体元素。

        zset用法:

  • 热搜:有序集合唯一有序的特性可以实现热搜榜。
  • 延时队列:可以将消息队列当做一个字符串作为zset的member,消息的到期处理时间作为score,多线程轮询zset获取到期任务进行处理。
  • 20
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于学习Redis基础知识,可以按照以下思路进行学习: 1. 了解Redis的概念和特点:首先需要了解Redis是什么,它的主要特点是什么,它为什么被广泛应用于缓存、消息队列、会话管理等场景。 2. 安装和配置Redis:根据你的操作系统,安装Redis并进行相关配置。可以参考Redis官方文档或其他教程来完成这一步。 3. 学习Redis的数据结构:Redis支持多种数据结构,如字符串、哈希、列表、集合和有序集合等。了解每种数据结构的特点、用途和操作命令,并通过实际操作来加深理解。 4. 掌握Redis的常用命令:学习Redis的常用命令,如get、set、hget、hset、lpush、lrange、sadd、smembers等,了解每个命令的具体用法和参数含义。 5. 理解Redis的持久化机制:了解Redis的RDB和AOF两种持久化方式,以及它们的优缺点。学习如何进行备份和恢复数据。 6. 学习Redis的事务和Lua脚本:了解Redis事务的基本概念和使用方法,以及如何使用Lua脚本来进行复杂的操作。 7. 深入了解Redis的性能优化和高可用方案:学习如何优化Redis的性能,包括配置调优、使用合适的数据结构、合理地使用缓存等。同时了解Redis的高可用方案,如主从复制、哨兵模式和集群模式。 8. 学习Redis与其他技术的结合:了解Redis如何与其他技术进行结合,如与Python、Java等编程语言的配合使用,以及与Spring、Django等框架的整合。 以上是学习Redis基础知识的一个思路,你可以根据自己的实际情况和需求进行学习和拓展。推荐参考一些经典的Redis教程和实战案例,通过实际操作和项目实践来提升自己的技能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值