目录

1. 概述

1.1 redis底层结构

1.2 为什么定义SDS数据类型

1.3 扩容机制

2. 数据类型总述

2.1 总述

2.2 详细

2.3 渐进式rehash

2.4 为什么要渐进式rehash

3.string数据结构

3.1 基本命令

3.2 应用场景  

3.3 底层数据结构

3.4 编码转换

3.5 bitmap类型

4. hash结构

4.1 Hash常用操作

4.2 应用场景 

4.3 优缺点

4.4 数据结构

4.5 hash对象的编码 

5. list数据结构 

5.1 常用操作  

5.2 应用场景  

5.3 数据结构

6. set数据结构

6.1 常用操作 

6.2 应用场景 

6.3 数据结构

7. zset数据结构

7.1 常用操作

7.2 应用场景 

7.3 数据结构

8. 其他命令 

8.1 keys:全量遍历键 

8.2 scan:渐进式遍历键 

8.3 其他命令 


1. 概述

1.1 redis底层结构

Redis底层用c语言实现,c语言字符串是用char[]数组表示,redis重新对字符串进行了定义,定义为SDS,simple dynamic string,简单动态字符串

1.2 为什么定义SDS数据类型

为什么用自定义这种类型呢,因为redis作为客户端,会接收各种语言传过来的数据,如果通通都转为c语言的话,c语言字符串的特点就是以\0进行结尾,如果传过来的数据经过序列化生成后,在中间有\0那么后面的数据就不会再进行读取了

SDS的话,举个例子,这么定义,len作为这个字符串长度,buf用来存储数据,这样的话就能体现redis是二进制安全的数据结构(就是没有丢失数据),free表示还有多少空间能放入数据,下面这个例子就是0,总长是8的话

sds:

    free:

    len:8

    char buf[]=“dongdong”

1.3 扩容机制

redis存储字符串过程:如果原来存入的字符数组长度为6,现在往里面加入3个字符,即addlen,扩容计算方法是(len + addlen) * 2进行扩容,然后就分配了18个字节,剩余空间是9个,好处就是下次修改的话就不需要重新分配内存空间了,直接复制进去,用空间换时间,当数据存储到1M的时候就不会再进行扩容了,提供了内存预分配机制,避免了频繁的内存分配,同时也兼容c语言函数库 

2. 数据类型总述

2.1 总述

redis 存储二进制块 redis保存二进制数据_数据

2.2 详细

redis 存储二进制块 redis保存二进制数据_redis 存储二进制块_02

2.3 渐进式rehash

redis 存储二进制块 redis保存二进制数据_开发语言_03

2.4 为什么要渐进式rehash

redis 存储二进制块 redis保存二进制数据_redis_04

3.string数据结构

3.1 基本命令

字符串常用操作

  • SET  key  value                               存入字符串键值对
  • MSET  key  value [key value ...]      批量存储字符串键值对
  • SETNX  key  value                          存入一个不存在的字符串键值对
  • GET  key                                          获取一个字符串键值
  • MGET  key  [key ...]                         批量获取字符串键值
  • DEL  key  [key ...]                            删除一个键
  • EXPIRE  key  seconds                    设置一个键的过期时间(秒)

原子加减

  • INCR  key             //将key中储存的数字值加1
  • DECR  key             //将key中储存的数字值减1
  • INCRBY  key  increment         //将key所储存的值加上increment
  • DECRBY  key  decrement     //将key所储存的值减去decrement 

3.2 应用场景  

单值缓存

SET  key  value     

GET  key 

对象缓存

1) SET  user:1  value(json格式数据)

2) MSET  user:1:name  zhuge   user:1:balance  1888     MGET  user:1:name   user:1:balance 

redis 存储二进制块 redis保存二进制数据_数据_05

总结:上面有两种对对象存储的方式,我们工作中一般用第一种比较多一些,如果对象字段数据很多,如果修改其中几个数据,第二种方式修改更直接一些,第一种的话,需要把json查出来,然后进行转换,然后修改,转换为json再储存回去,比较麻烦,缺点的话key会很多 

分布式锁

SETNX  product:10001  true         //返回1代表获取锁成功

SETNX  product:10001  true         //返回0代表获取锁失败 

。。。执行业务操作 DEL  

product:10001            //执行完业务释放锁

SET product:10001 true  ex  10  nx    //防止程序意外终止导致死锁

计数器

INCR article:readcount:{文章id}      

GET article:readcount:{文章id} 

文章,或者博客的阅读量  

Web集群session共享 

spring session + redis实现session共享 (了解)

分布式系统全局序列号 

INCRBY  orderId  1000        //redis批量生成序列号提升性能  

  • 你进行分库分表,如果用数据库自增id也可以,如果你把你的库分成多个,一张表分为多张表,存储在多个库里面去,就不能使用自增id了,雪花算法也可以,如果用incr id的话,数据量大,每次都进行+1操作的话,会很占资源(我理解啊,就是频繁与redis建立连接,消耗资源),光这个操作占资源不是很合适
  • 他这里面应该是能获取到1000个订单之后,然后执行在订单号加1000,就不需要每次进行+1曹组了

3.3 底层数据结构

1)这边先讲key

redis 存储二进制块 redis保存二进制数据_java_06

3.2之前就我们开篇说的这种类型,3.2之后又出现了很多类型,通过buf里面的数据就在100字节以内,int能表示的数字很大,用来表示buf里面数据的长度和剩余空间有点浪费

看一下3.2版本后面的定义,uint8_t表示8位,8bit表示长度和分配空间,如果超过这个数字,依次类推使用其他数据结构

flags字段解释,占一个字节,flags和buf是紧挨着的,buf向前移动一个字节就是flag,flag里面包含的是sds类型,和len长度,sdshdr5,就是前面3个是type,后面5个是长度,因为是5bit,type占3bit共一个字节

redis 存储二进制块 redis保存二进制数据_数据_07

redis 存储二进制块 redis保存二进制数据_redis_08

sdshdr看一下,001对应的就是这个数据结构类型,后面5bit是未使用的,在上面len和alloc各栈一个字节 

2)再说value 

说到k-v就会想到hashmap,在redis里面指的是dict,存储方式一般有数组(O(1))、链表(O(n))和树(O(logN)),redis用的是数组和链表,通过hash函数将任意一个数据转换为自然数,快速的完成了数据的索引,hash函数得到的值非常大,如果拿这个数去做索引的话,就很浪费空间了,通过我我们会对这个值进行求模,一般这个值就是数组的大小,求模优化就是&长度-1,和求模结果是一样的,性能更好,进行一次位运算就可以

redis 存储二进制块 redis保存二进制数据_java_09

如果key的hash值是一样的话,就涉及到了hash碰撞(因为hash函数相同的概率特别小,但是求模之后概率就变大了),采用链地址法或者开放地址法,redis采用的链地址法的头插法进行解决, 

3)先看一下redis数据库

typedef struct redisDb{

        dict *dict;// key-value就存在这里面        

        dict *expires;// 过期时间

        dict *blocking_keys; //阻塞队列的处理

        dict *ready_keys; //维护key和客户端的关系

        dict *watched_keys;// 事务处理

        int id;//id

        long long avg_ttl;

        unsigned long expires_cursor;

        list *defrag_later;

}redisDb;

4)hashtable

redis 存储二进制块 redis保存二进制数据_开发语言_10

这个就是hashtable,上面table字段就是指向实际hashtable数组,其他了解一下就行 

5)扩容机制

redis里面扩容数组数据复制,不是直接创建一个2倍容积的数组,数据全部往里复制,redis数据量很大,复制进来太耗时间和性能,它会访问到某个key的时候,选一部分key-value复制到新建的扩容数组,然后快点结束去执行其他的命令,依次类推就是渐进式的rehash,即使没有执行命令也会不断地去搬数据直到搬空为止,然后把老的数组释放掉,所以每个dict都有两个dictht 

6)数组里存放的数据

redis 存储二进制块 redis保存二进制数据_redis 存储二进制块_11

  

这个就是存放的数据key-value,key就是sds数据类型,value具体也是引用,实际上用redisObject进行封装存放数据的

redis 存储二进制块 redis保存二进制数据_java_12

这个type是用来约束客户端的命令的,encoding是实际的数据类型或者说底层编码,refcount是用来管理内存的,ptr是真实存储的位置 

整体结构图

redis 存储二进制块 redis保存二进制数据_java_13

过程讲解: 

  1. 先选择数据库,数据具体存储在dict *dict字典里面
  2. 字典里面数据结构,有两个hashtable,一个存储数据,一个实现渐进式rehash扩容
  3. dicht里面决定了什么时间进行扩容,used(使用的个数)/size=1的时候,按2倍进行扩容
  4. hashtable里面存储的是dicEntry,key如果发生冲突的话,采用链地址法以链表的形式存在
  5. key就是sds,里面包含了长度和允许空间以及buf数组,用来存放数据的
  6. value指向的redisObject对象,redisObject里面存储着type、encoding编码方式、最后面那个是真实数据的存储

注意:就是你用什么命令,他会把这个value封装为对应的type,但是string类型下面包括3种编码类型,raw、int、embstr,raw就是sds,redis怎么判断到底用哪种编码格式存储呢,就是当你用命令设置值的时候,他会去判断这个值的长度(刚开始都是sds类型数据,能获取到长度),如果长度小于20,就转换为int,主要为了节省cpu内存io,也节省了内存空间,另外就是redis缓存行会存64个字节数据,去掉必须要占用的空间(比如像存储长度、存储类型等),还有44个字节,如果长度小于等于44字节的话,会用raw类型存储,直接用缓存行来存储这个数据,为了不浪费这个空间,如果超出这个范围的话就用embstr类型存储

3.4 编码转换

redis 存储二进制块 redis保存二进制数据_java_14

3.5 bitmap类型

高效,省空间

  • 实际上bitmap type也是string类型的,你用set get命令也是可以操作,只不过获取到的数据看不明白,全是二进制数据
  • 命令:setbit key offset 0|1,具体设置哪个位置的数据为0或者1,getbit key offset获取这个位置上的数据,可以进行与、或操作
  • 为了统计日活量,可以将userId设置为offset,它这个默认长度是根据你设置的key来定义的,加入你设置key为100,除以8位12.5个字节,需要分配13个字节的空间,他这里面的长度指的就是字节数,索引的话可以更大,最大是可以512M,它底层也是string,索引值最大是2^32-1,就是求一下多少bit位

4. hash结构

4.1 Hash常用操作

HSET  key  field  value             //存储一个哈希表key的键值

HSETNX  key  field  value         //存储一个不存在的哈希表key的键值

HMSET  key  field  value [field value ...]     //在一个哈希表key中存储多个键值对

HGET  key  field                 //获取哈希表key对应的field键值

HMGET  key  field  [field ...]         //批量获取哈希表key中多个field键值

HDEL  key  field  [field ...]         //删除哈希表key中的field键值

HLEN  key                //返回哈希表key中field的数量

HGETALL  key                //返回哈希表key中所有的键值

HINCRBY  key  field  increment         //为哈希表key中field键的值加上增量increment 

4.2 应用场景 

对象缓存 

HMSET  user  {userId}:name  zhuge  {userId}:balance  1888

HMSET  user  1:name  zhuge  1:balance  1888

HMGET  user  1:name  1:balance   

redis 存储二进制块 redis保存二进制数据_数据_15

如果user里面有上亿条数据,这时候这个user是bigkey,bigkey的意思应该是这个key下面数量很大,当我们对这个key获取所有值的话,会执行很长时间,他会阻塞redis其他命令 

电商购物车 

1)以用户id为key 2)商品id为field 3)商品数量为value 

购物车操作

添加商品 hset cart:1001 10088 1

增加数量 hincrby cart:1001 10088 1

商品总数 hlen cart:1001

删除商品 hdel cart:1001 10088

获取购物车所有商品 hgetall cart:1001 

redis 存储二进制块 redis保存二进制数据_redis_16

4.3 优缺点

优点:

1)同类数据归类整合储存,方便数据管理

2)相比string操作消耗内存与cpu更小

3)相比string储存更节省空间

缺点:

1)过期功能不能使用在field上,只能用在key上

2)需要考虑数据量分布的问题(value 值非常大的时候,无法分布到多个节点) 

4.4 数据结构

Hash数据结构底层实现为一个字典(dict),也是RedisDb用来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储,数据大小和元素数量阈值可以通过如下参数设置

hash-max-ziplist-entries 512  //ziplist元素个数超过512,将改为hashtable编码

hash-max-ziplist-value  64  //单个元素超过64byte时,将改为hashtable编码 

redis 存储二进制块 redis保存二进制数据_java_17

hash和string再比较,一个是hash不支持里层field的key过期时间的设置,从底层来说的话,string相比于hash字段更多,需要更多的rehash进行比较,而hash只需要比较外层的key的hash值即可 

4.5 hash对象的编码 

redis 存储二进制块 redis保存二进制数据_数据_18

5. list数据结构 

5.1 常用操作  

 LPUSH  key  value [value ...]         //将一个或多个值value插入到key列表的表头(最左边) RPUSH  key  value [value ...]         //将一个或多个值value插入到key列表的表尾(最右边) LPOP  key            //移除并返回key列表的头元素

RPOP  key            //移除并返回key列表的尾元素

LRANGE  key  start  stop        //返回列表key中指定区间内的元素,区间以偏移量start和stop指定

BLPOP  key  [key ...]  timeout    //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待                    timeout秒,如果timeout=0,一直阻塞等待

BRPOP  key  [key ...]  timeout     //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待                    timeout秒,如果timeout=0,一直阻塞等待 

 

redis 存储二进制块 redis保存二进制数据_开发语言_19

5.2 应用场景  

常用数据结构 

Stack(栈) = LPUSH + LPOP  先进后出

Queue(队列)= LPUSH + RPOP 先进先出

Blocking MQ(阻塞队列)= LPUSH + BRPOP 你如果没有数据的话,就阻塞,也可以加上过期时间,时间过了也不要一直阻塞着

你用stack或者queque本地的数据结构在分布式里面肯定不行

微博和微信公号消息流

redis 存储二进制块 redis保存二进制数据_java_20

诸葛老师关注了MacTalk,备胎说车等大V

1)MacTalk发微博,消息ID为10018 LPUSH  msg:{诸葛老师-ID}  10018

2)备胎说车发微博,消息ID为10086 LPUSH  msg:{诸葛老师-ID} 10086

3)查看最新微博消息 LRANGE  msg:{诸葛老师-ID}  0  4

就是你每个人关注的消息生成一个key,value值为list类型,存放消息id,每次关注的人发出消息,左边压入一个消息id,等到用户查询消息的时候,直接从左边获取就可以了 

如果一个大v有很多粉丝关注,那么就需要往很多用户发送消息id,优化的话可以分批去发,或者优先给在线的用户发,不在线的慢慢发,如果有上千万粉丝的话,就需要push/pull操作,就发到队列里面去,其他用户去队列里面拉取消息

5.3 数据结构

如果自己想一下redis的List结构用什么去实现,可能你会想到用链表会好一点,方便前后添加数据,但是查询效率比较低,而且如果List里面数据量很大的话,每个元素都很小,那么很多空间都会被指针所占据,即胖指针,链表的数据结构在物理逻辑上不连续,会在内存中产生大量的内存碎片

内存碎片解释:

内存碎片即“碎片的内存”描述一个系统中所有不可用的空闲内存,这些碎片之所以不能被使用,是因为负责动态分配内存的分配算法使得这些空闲的内存无法使用,这一问题的发生,原因在于这些空闲内存以小且不连续方式出现在不同的位置。因此这个问题的或大或小取决于内存管理算法的实现上,内存碎片会使得很难读取连续的有用内容,过多的内存碎片也会占据着有用的空间,降低内存的使用效率

实际上redis使用quicklist(双端链表)和ziplist 

redis 存储二进制块 redis保存二进制数据_开发语言_21

ziplist包含这几部分:

zipbytes占4个字节,指的是当前ziplist里面存多少数据

zltail占4个字节,指示尾结点索引,就是节点的位置,方便快速找到尾结点,往前面遍历

zlen占2个字节,当前ziplist有多少个元素

zlend占1个字节,恒等于255,用来标识数据结尾

每个entry数据结构:

prerawlen是前一个元素的信息,就是为了方便从前往后,也为了从后往前遍历,图上也有解释

len data是当前元素的信息,详细见上图

问题:

如果每次你去增加或者删除数据的时候,都需要重新分配一部分空间,然后重新赋值,这样的话非常消耗性能,这时候就用到了双端链表,就是分块,分成很多的ziplist,就用一个ziplist后面数据量大的话就不方便移动复制了,下面这种就将数据分块存储

redis 存储二进制块 redis保存二进制数据_开发语言_22

quicklist是双端链表,包含头结点和尾结点,quicklistNode是每个节点的信息,里面zl指向的是ziplist,节点之间通过链表进行关联,如果ziplist里面不断加入数据到一定程度的话,会进行分裂两个ziplist重新建立关系

list-max-ziplist-size -2   单个ziplist节点最大能存储8kb,超过则进行分裂,将数据存储在新的ziplist节点中

list-compress-depth  1   0代表所有的节点,都不进行压缩,1代表从头结点往后走一个,尾结点往前走一个不用压缩,其他全部压缩,其他数字以此类推

6. set数据结构

6.1 常用操作 

SADD  key  member  [member ...]            //往集合key中存入元素,元素存在则忽略,若key不存在则新建

SREM  key  member  [member ...]            //从集合key中删除元素

SMEMBERS  key                    //获取集合key中所有元素

SCARD  key                    //获取集合key的元素个数

SISMEMBER  key  member            //判断member元素是否存在于集合key中 SRANDMEMBER  key  [count]            //从集合key中选出count个元素,元素不从key中删除 SPOP  key  [count]                //从集合key中选出count个元素,元素从key中删除 

Set运算操作 

SINTER  key  [key ...]                 //交集运算

SINTERSTORE  destination  key  [key ..]        //将交集结果存入新集合destination中 SUNION  key  [key ..]                 //并集运算

SUNIONSTORE  destination  key  [key ...]        //将并集结果存入新集合destination中

SDIFF  key  [key ...]                 //差集运算

SDIFFSTORE  destination  key  [key ...]        //将差集结果存入新集合destination中 


集合操作

SINTER set1 set2 set3      { c }

SUNION set1 set2 set3     { a,b,c,d,e }

SDIFF set1 set2 set3        { a }

redis 存储二进制块 redis保存二进制数据_redis_23

6.2 应用场景 

微信抽奖小程序

1)点击参与抽奖加入集合 SADD key {userlD}

2)查看参与抽奖所有用户 SMEMBERS key      

3)抽取count名中奖者 SRANDMEMBER key [count] / SPOP key [count] 

微信微博点赞,收藏,标签 

1) 点赞 SADD  like:{消息ID}  {用户ID}

2) 取消点赞 SREM like:{消息ID}  {用户ID}

3) 检查用户是否点过赞 SISMEMBER  like:{消息ID}  {用户ID}

4) 获取点赞的用户列表 SMEMBERS like:{消息ID}

5) 获取点赞用户数 SCARD like:{消息ID} 

 集合操作实现微博微信关注模型

1) 诸葛老师关注的人: zhugeSet-> {guojia, xushu}

2) 杨过老师关注的人:  yangguoSet--> {zhuge, baiqi, guojia, xushu}

3) 郭嘉老师关注的人: guojiaSet-> {zhuge, yangguo, baiqi, xushu, xunyu)

4) 我和杨过老师共同关注: SINTER zhugeSet yangguoSet--> {guojia, xushu}

5) 我关注的人也关注他(杨过老师): SISMEMBER guojiaSet yangguo SISMEMBER xushuSet yangguo

6) 我可能认识的人: SDIFF yangguoSet zhugeSet->(zhuge, baiqi} 

集合操作实现电商商品筛选 

SADD  brand:huawei  P40 SADD  brand:xiaomi  mi-10

SADD  brand:iPhone iphone12 SADD os:android  P40  mi-10

SADD cpu:brand:intel  P40  mi-10

SADD ram:8G  P40  mi-10  iphone12 SINTER  os:android  cpu:brand:intel  ram:8G   {P40,mi-10} 

redis 存储二进制块 redis保存二进制数据_java_24

6.3 数据结构

set为无序的,自动去重的集合数据类型,set数据结构底层实现为一个value为null的字典(dict),当数据可以用整型表示时,set集合将被编码为intset数据结构。两个条件任意满足时set将用hashtable存储数据

1. 元素个数大于set-max-intset-entries 512 //intset 能存储的最大元素个数

2. 元素无法用整型表示

7. zset数据结构

带分值的集合

7.1 常用操作

ZADD key score member [[score member]…]    //往有序集合key中加入带分值元素

ZREM key member [member …]        //从有序集合key中删除元素

ZSCORE key member             //返回有序集合key中元素member的分值

ZINCRBY key increment member        //为有序集合key中元素member的分值加上increment ZCARD key                //返回有序集合key中元素个数

ZRANGE key start stop [WITHSCORES]    //正序获取有序集合key从start下标到stop下标的元素

ZREVRANGE key start stop [WITHSCORES]    //倒序获取有序集合key从start下标到stop下标的元素 

 Zset集合操作

ZUNIONSTORE destkey numkeys key [key ...]     //并集计算

ZINTERSTORE destkey numkeys key [key …]    //交集计算 

redis 存储二进制块 redis保存二进制数据_java_25

7.2 应用场景 

1)点击新闻 ZINCRBY  hotNews:20190819  1  守护香港 每次打开这个新闻,分值

2)展示当日排行前十 ZREVRANGE  hotNews:20190819  0  9  WITHSCORES

3)七日搜索榜单计算 ZUNIONSTORE  hotNews:20190813-20190819  7 hotNews:20190813  hotNews:20190814... hotNews:20190819

4)展示七日排行前十 ZREVRANGE hotNews:20190813-20190819  0  9  WITHSCORES 

7.3 数据结构

zset为有序的,自动去重的集合数据类型,zset数据结构底层实现为字典(dict)+跳表(skiplist),当数据比较少时,用ziplist编码结构存储

使用跳表的时间复杂度为logN,实际上就是空间换时间的设计

可以从前往后遍历,也可以从后往前遍历

跳表增加或修改数据过程:
首先先判断你加的这个元素的key是否已经含有,来判断是新增还是修改,如果是新增的话,然后拿这个分值去判断,根据level层高往下去找具体放在哪个位置,如果判断在哪个区间的话,就向下一层继续判断找出具体存放位置,层高是通过随机函数生成的,然后根据元素和分值创建好节点,与前后节点,层高等关联关系需要重新建立,一些数值也要重新计算

redis 存储二进制块 redis保存二进制数据_数据_26

redis 存储二进制块 redis保存二进制数据_java_27

8. 其他命令 

8.1 keys:全量遍历键 

keys:全量遍历键,用来列出所有满足特定正则字符串规则的key,当redis数据量比较大时, 性能比较差,要避免使用,优化的话就是分批查询,或者下面的这个方法 

8.2 scan:渐进式遍历键 

SCAN cursor [MATCH pattern] [COUNT count] 
scan 参数提供了三个参数,第一个是 cursor 整数值(hash桶的索引值),第二个是 key 的正则模式, 第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不是符合条件的结果数量。第 一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历 到返回的 cursor 值为 0 时结束。

注意:但是scan并非完美无瑕, 如果在scan的过程中如果有键的变化(增加、 删除、 修改) ,那 么遍历效果可能会碰到如下问题: 新增的键可能没有遍历到, 遍历出了重复的键等情况, 也就是说 scan并不能保证完整的遍历出来所有的键, 这些是我们在开发时需要考虑的。 

8.3 其他命令 

Info:查看redis服务运行信息,分为 9 大块,每个块都有非常多的参数,这 9 个块分别是:

Server 服务器运行的环境参数

Clients 客户端相关信息

Memory 服务器运行内存统计数据

Persistence 持久化信息

Stats 通用统计数据

Replication 主从复制相关信息

CPU CPU 使用情况

Cluster 集群信息

KeySpace 键值对统计数量信息