[每天进步一点点]Redis笔记:常用的基本数据类型

Redis常用的基本数据类型

激励:人人都有一个大厂的心,坚持自己的梦想,你就是世界。

乏味:笔记很无聊,需要去品味。

坚持:每天进步一点点,当知道的越多,才发现不知道的也越多。

String

最基本也是最常用的数据类型,也被叫做Binary-safe strings

  • 可以用来存储字符串、正数、浮点数。

操作命令

  • 批量操作(原子性

    mset key1 val1 key2 val2

  • 设置值,如果key存在,则不成功

    setnx key

    说明:基于该操作可以实现分布式锁,然后用del key来释放锁。

    存在问题:如果del key失败了,会导致其它节点永远获取不到锁。

    解决方法:给key加上过期时间,可以使用expire命令,单是这样不是原子性操作。

    最好的办法就是使用setnx key value [expriation EX seconds | PX milliseconds] [NX|XX]。

    用例:

    set lock 1 EX 10 NX

  • 整数值递增/减

    递增:

    • incr key
    • incrby key num

    递减:

    • decr key
    • decrby key num

    浮点数:

    • Incrbyfloat key num

    备注:这里的num是要增加/减少的值。

  • 批量获取

    mget key1 key2

  • 获取长度

    strlen key

  • 字符串追加

    append key val

  • 获取指定范围的字符

    getrange key start end

    备注:start:开始位置, end:结束位置。

实现原理

结构图

备注:SDS:Simple Dynamic String,redis中字符串的实现。

  • SDS结构,下边源码是sdshdr8, SDS又分为:sdshdr5(2^5 = 32byte)、sdshdr8(2^8 = 256byte)、sdshdr16(2^16 = 65536byte = 64KB)、sdshdr32、sdshdr64。

    struct __attribute__ ((__packed__)) sdshdr8 {
      uint8_t len; /* 当前字符数组的长度 */
      uint8_t alloc; /*当前字符数组总共分配的内存大小 */
      unsigned char flags; /* 当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等 */
      char buf[]; /* 字符串真正的值 */
    };
    
  • redis为什么要用SDS来实现字符串?

    原因:c语言本身是没有字符串类型的,只能用字符数组char[]来实现。

    1. 使用字符数组必须先给目标变量分配足够的空间,否则会有溢出的情况;
    2. 如果要获取字符数据的长度,必须遍历整个字符数组,比较耗时(时间复杂度是O(n));
    3. 字符串长度如果发生变更,需求重新分配内存空间;
    4. c语言中字符串的尾部是以‘\0’来标示的,因此不能存储图片、音视频、压缩文件等二进制保存的内容,二进制不安全,这里也解释了redis为什么是Binary-safe strings

    SDS特点:

    1. 不用担心内存溢出问题,会自动扩容;
    2. 内部结构存储了字符串长度(定义了len属性),获取字符串长度时间复杂度是O(1)的;
    3. 通过空间预分配和惰性空间释放来防止多次重复分配内存;
    4. 判断是否结束的标示是len属性,它同样以‘\0’结尾,是因为这样可以使用c语言中的函数库;
数据模型

Redis是KV形式的数据库,它是通过hashtable实现的,所以每个键值对都有一个dictEntry(源码参考:dict.h)。

typedef struct dictEntry {
    void *key; /* key 关键字定义 */
    union {
        void *val;
        uint64_t u64; /* value 定义 */
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; /* 指向下一个键值对节点 */
} dictEntry;    
  • key是字符串,但是redis没有直接使用C语言中国呢的字符数组,而是存储在自定定义的SDS中。

  • value既不是直接作为字符串存储,也不是直接使用SDS,而是存储在redisObject中。实际上五种常用的数据类型的任何一种都是通过redisObject来存储的。

  • redisObject定义在src/server.h文件中。

    typedef struct redisObject {
      unsigned type:4; /* 对象的类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
      unsigned encoding:4; /* 具体的数据结构 */
      unsigned lru:LRU_BITS; /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关 */
      int refcount; /* 引用计数。当 refcount 为 0 的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
      void *ptr; /* 指向对象实际的数据结构 */
    } robj;
    
内部编码
  • int:存储8个字节的长整型(long,2^63 - 1);

  • Embstr:代表embstr格式的SDS,存储小于44个字节的字符串。

  • raw:存储大于44个字节的字符串(3.2版本以前是39个字节)。

    embstr和raw的区别:

    • embstr只分配一次内存空间(因为redisObject和SDS是连续的),raw需要分配两次内存空间(为redisObject和SDS分别分配空间);
    • embstr相对于raw的好处是在于创建时少分配一次空间,删除的时候少释放一次空间,所有的数据都是连在一起的,寻找方便;
    • embstr的缺点也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和SDS都需要重新分配空间,因此redis中的embstr实现为只读。

    int和embstr与raw之间的转换:

    • 当int数据不再是整数时,或者大小超过了long的范围时会将int自动转化为embstr;
    • 对于embstr来说,由于实现时只读的,因此在对embstr对象进行修改时,会先转化成raw后再进行修改。只要是对embstr类型的对象进行修改,修改后的对象类型一定是raw,无论是否超过了44个字节的长度限制;
    • int和embstr与raw之间转换的过程是不可逆的,只能从小内存编码转换成大内存编码(重新set的情况除外);

    备注long的范围为:2^63 - 1

    redis通过这种封装,可以根据对象的类型动态的选择存储的结构和可使用的命令,实现节省空间和优化查询。

应用场景

  • string类型:热点数据的缓存(例如:新闻内容、报表数据)、对象缓存、全页缓存,可以提升热点数据的访问速度。

  • 分布式系统中的共享数据:分布式的session、分布式锁、全局的ID、计数器、限流操作、位统计等。

Hash哈希

操作命令

  • 常用的操作:

    • hset key field val
    • hmset key field1 val1 field2 val2 field3 val3
    • hget key field
    • hmget key field1 field2
    • hkeys key
    • hvals key
    • hgetall key
  • key操作:

    • hget exists key
    • hdel key
    • hlen key

实现原理

结构示例图
存储类型

包含键值的无序散列表,val只能是字符串且不能嵌套其他类型。

  • hash与string的区别

    1. 把所有相关的值聚集到一个key中,节省内存空间;
    2. 只使用一个key,减少key之间的冲突;
    3. 存储的是一个完整的对象信息,减少了将对象信息分开存储的I/O和cpu的消耗;
  • hash不适合的场景

    1. Field不能单独设置过期时间;
    2. 需要考虑数据量分布问题,value值非常大的时候,无法分布到多个节点;
    3. 没有bit操作;
存储原理

redis的hash本身也是一个kv的结构,类似于java中的HashMap。

外层的哈希(redis kv的实现)只用到了hashtable,当存储hash数据类型时,一般叫做内层的哈希。

内层的哈希底层可以使用两种数据结构实现:ziplist:OBJ_ENCODING_ZIPLIST(压缩列表),hashtable:OBJ_ENCODING_HT(哈希表)。

  • ziplist压缩列表: 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少、字段值小的场景。当hash对象同时满足以下两个条件的时候,使用ziplist编码:
  1. 所有的键值对的键和值的字符串长度都小于等于64byte(一个英文字母一个字节);
  2. 哈希对象保存的键值对数量小于512个;
// src/redis.conf配置
hash-max-ziplist-value 64  //ziplist中最大能存放的值长度
hash-max-ziplist-entries 512 //ziplist中最多能存放的entry节点数量

一个哈希对象超过配置的阈值(键和值的长度大于64byte,键值对个数大于512个)时,会转换成哈希表hashtable。

  • hashtable(dict):hashtable被称为dictionary,它是一个数组+链表的结构。

redis的hash默认使用的是ht[0],ht[1]不会初始化和分配空间。

哈希表dictht是用链地址法来解决碰撞的问题,在这种情下,哈希表的性能取决于它的大小(size属性)和它所保存的节点的数量(used属性)之间的比率;

  • 比率在1:1时(一个哈希表ht只存储一个节点entry),哈希表的性能最好;
  • 如果节点数量比哈希表的大小要大很多的话(比例用ratio表示,5表示平均一个ht存储5个entry),那么哈希表就会退化成个多个链表,哈希表本身的性能优势就不存在了。在这个情况下需要扩容。redis里面的这种操作叫做rehash。
  • rehash的步骤:
    1. 为字符ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作和ht[0]当前包含的键值对的数量。(ht[1]的大小为第一个大于等于ht[0].used*2);
    2. 将所有的ht[0]上的节点rehash到ht[1]上,重新计算hash值和索引,然后放到指定的位置;
    3. 当ht[0]全部迁移到ht[1]后,释放ht[0]的空间,将ht[1]设置为ht[0]表,并创建新的ht[1]为下次rehash做准备。

应用场景

  • String:String 类型的hash都可以做。
  • 存储对象:一个对象或一张表的数据(比string节省更多的key空间,集中管理)。

List列表

操作命令

  • 元素增减

    lpush key val

    lpush key val1 val2

    rpush key val1

    lpop key

    rpop key

    blpop key

    brpop key

  • 取值

    lindex key index

    lrange key start stop

存储类型

存储有序的字符串(从左到右),元素可以重复。可以当简单的队列和栈使用.

结构图
存储原理

3.2版本之前,数据量较小的时候用ziplist存储,达到阈值时转换为linkedlist进行存储,分别对应OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_LINKEDLIST。

3.2版本之后统一使用了quicklist来存储。quicklist存储了一个双向链表,每个节点都是一个ziplist。

  • quicklist

    • quicklist(快速列表)是ziplist和linkedlist的结合体。
    • quicklist.h,head和tail指向双向链表的表头和表尾。
    typedef struct quicklist {
        quicklistNode *head; /* 指向双向列表的表头 */
        quicklistNode *tail; /* 指向双向列表的表尾 */
        unsigned long count;
        /* 所有的 ziplist 中一共存了多少个元素 */
        unsigned long len;
        /* 双向链表的长度,node 的数量 */
        int fill : 16;
        /* fill factor for individual nodes */
        unsigned int compress : 16; /* 压缩深度,0:不压缩; */
    } quicklist;
    

    配置参数(redis.conf):

    • List-max-ziplist-size(fill):正数表示单个ziplist最多包含的entry个数。负数代表单个ziplist的大小,默认8k。(-1:4KB;-2:8KB;-3:16KB;-4:32KB;-5:64KB)
    • List-compress-depth(compress):压缩深度,默认是0。(1:首尾的ziplist不压缩;2:首尾第一第二个ziplist不压缩,以此类推)

应用场景

  • 用户消息的时间线(timeline)

  • 消息队列:list提供两个阻塞的弹出操作:blpop/brpop,可以设置超时时间。

    • blpop:blpop key1 timeout移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出的元素为止;

    • brpop:brpop key1 timeout移除并获取列表的最后一个元素,超时机制同blpop;

    • 队列:先进先出:rpush、blpop,左头右尾,右边进入队列,左边出队列;

    • 栈:先进后出:rpsuh和brpop

Set集合

操作命令

  • 添加一个/多个元素

    sadd key member

    sadd key member1 member2

  • 获取所有元素

    smembers key

  • 统计元素个数

    scard key

  • 随机获取一个元素

    srandmember key

  • 随机弹出一个元素

    spop key

  • 移除一个/多个

    srem key member

    srem key member1 member2

  • 查看元素是否存在

    sismember key member

存储类型

String类型的无序集合,最大存储数量为2^32 - 1。

结构图
存储原理

redis用intset或hashtable来存储set。如果元素都是整数类型,就用intset存储,如果不是整数类型,就用hashtable存储,如果元素个数超过512个,也会用hashtable存储。

应用场景

  • 抽奖:随机获取元素
  • 点赞、签到、打卡
  • 数据的标签,数据的筛选
  • 用户关注、推荐模型:可以用set来取并集,差集,交集。

ZSet有序集合

操作命令

  • 添加元素

    zadd key [NX|XX] [CH] [INCR] score member [score member ...]

  • 获取全部元素

    zrange key start stop [WITHSCORES]

    zrevrange key start stop [WITHSCORES]

  • 根据分值区间获取元素

    zrangebyscore key min max [WITHSCORES] [LIMIT offset count]

  • 移除元素

    zrem key member [member ...]

  • 统计元素个数

    zcard key

  • 分值递增

    zincrby key increment member

  • 根据分值统计个数

    zcount key min max

  • 获取元素rank

    zrank key member

  • 获取元素score

    zscore key member

存储类型

sorted set:有序的set,每个元素都有一个score。如果score相同,按照key的ASCII码排序。

结构图
存储原理

同时满足以下条件使用ziplist:

  1. 元素个数小于128;
  2. 所有member的长度都小于64字节;

在ziplist的内部,按照score排序递增来存储,插入的时候要移动之后的数据。超过阈值之后使用skiplist+dict存储。

  • skiplist(跳跃表)

假设我们要查找22这个值,

  1. 22首先和5比较,然后再和17比较,22比它们都大,所以继续向后比较;
  2. 当22和27比较时,发现22小于27,则会进入下面的链表,开始比较;
  3. 22正好时下一个链表的值,这样就顺利找到了。

假如要查找25,根据上边流程第三步发现25比22大,会继续向后查询,然后又比27小,说明要查询的25在原链表中不存在。

在整个查询过程中,由于新增加了指针,查询的时候就不需要遍历整个链表的节点,这样查询的次数大概减少了一半,这个就叫做跳跃表。

应用场景

  • 热搜排行榜:例如视频网站需要用户上传的视频做排行榜,榜单维护可能是多方面,按时间、按照播放量、按照获得的点赞次数等。
  • 带权重的队列:比如普通消息的sorce为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务,让重要的任务优先执行。

BitMaps

BitMaps时在字符串类型上面定义的位操作,一个字节又8个二进制位组成。

操作命令

  • 获取value在offset处的值

    getbit key offset

  • 修改二进制数据

    setbit key offset value

  • 统计二进制位中1的个数

    bitcount key

    bitcount key [start end] //获取start到end的位置的1

应用场景

  • 用户访问统计
  • 在线用户统计
  • 布隆过滤器

其他数据类型

  • Hyperloglogs:提供了一种不精准的基数统计方法,比较适合用来做大规模数据的去重统计。例如:网站的UV。

  • Geospatial:可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等,例如:用redis来实现附近的人或者计算最优地图路径。

  • Streams:5.0版本以后推出的数据类型,支持多播的可持久化的消息队列,用于实现发布订阅功能(借鉴了kafka的设计)。


本文由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容

  • String 字符串 存储类型 可以用来存储字符串、整数、浮点数。 操作命令 设置多个值(批量操作,原子性) 设置...
    WEIJAVA阅读 1,295评论 0 4
  • 转载:可能是目前最详细的Redis内存模型及应用解读 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据...
    jwnba24阅读 620评论 0 4
  • 前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站...
    小陈阿飞阅读 803评论 0 1
  • 【大势】大盘跳空杀跌,盈亏效应扩散。 【节奏】石油,汽配 杀跌体育,医药 活跃。 第 2 天 - 跳杀三维丝跳杀...
    天涯别院阅读 129评论 0 0
  • 妖精,不是贬义。“妖”者,有能力蛊惑别人。“精”者,有一定道行和修行的人,境界达到一定层次的人。写作营里的人,有积...
    桃花太红李太白呀阅读 354评论 12 1