一、基本特性
- 速度快
- 支持多种数据类型
- 支持多种编程语言
- 持久化,内存淘汰
- 支持事务,发布订阅,pipeline,lua
- 集群,分布式
二、基本命令
- 增加/修改:set key value
- 查询:get key
- 查询全部的key:keys *
- 查看key的数量:dbsize
- 查询key是否存在:exists key
- 删除:del key key……可一次删除多个
- 更改key的名字:rename oldKey newKey
- 查看key存储的value的数据类型:type key
- 清空当前数据库内容:flushdb
- 清空全部内容:flushall
接下来,将介绍redis支持存储的value类型
三、string类型
1、存储类型
- Int整形
- Float 单精度浮点型
- String 字符串
2、常见命令
- 查看固定长度的值:getrange key int int,示例:getrange redis 0 1 即查询key为redis的value的0至1位的数值
- 查看value长度:strlen key
- 追加字符串:append key value
- 如果key不存在,增加成功:setnx key value
- 同时增加多个key和value:mset key value key value
- 同时查看多个key的value:mget key key……
- 对key的value增加1:incr key
- 对key的value增加任意数值:incrby key int
- 对key的value减少1:decr key
- 对key的value减少任意数值:decr key int
- 对key的value增加任意浮点数:incrbyfloat key float
3、存储原理
key的存储都采用了redis特有的SDS类型,所有的value都是redisObject,然后由redisObject中的指针指向一个SDS存储的数值。
一)SDS
在redis中,key的存储都采用了redis特有的SDS类型
- sdshdr5:2^5=32byte(不用)
- sdshdr8:2^8=256byte
- sdshdr516:2^16=64KB
- sdshdr532:2^32=4GB
组成内容如下
- len:当前字符数组的长度
- alloc:当前字符数组总共分配的内存大小
- flags:当前字符数组的属性,用来表示SDS的类型
- buf []:字符串真正的值
对比优势
1)C字符数组本身存在的问题
- 使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
- 如果要获取字符长度,必须遍历字符数组,时间复杂度是O(n)。
- C字符串长度的变更会对字符数组做内存重分配。
- 通过从字符串开始到结尾碰到的第一个’\0’来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。
2)SDS的优势
- 不用担心内存溢出问题,如果需要会对SDS进行扩容
- 获取字符串长度时间复杂度为O(1),因为定义了len属性。
- 修改字符串长度N次最多需要执行N次内存重分配
- 判断是否结束的标志是len属性,可以包含’\O’(它同样以’\0’结尾是因为这样就可以使用C语言中函数库操作字符串的函数了)。
3)对比表格
C字符数组 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有<string.h>库中的函数 | 可以使用一部分<string.h>库中的函数 |
二)redisObject
所有的value都是redisObject,然后由redisObject中的指针指向不同的数据地址,redisObject由五部分组成
- type:对象类型
- encoding:具体的数据结构(编码)
- lru:LRU_BITS:24位,对象最后一次被命令程序访问的时间,与内存回收有关
- refcount:引用计数,当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了
- *ptr:指向对象实际的数据结构
4、编码类型
string共有三种编码类型
- int:存储8个字节的长整型(long,2^63-1)
- embstr:embstr格式的SDS,存储小于44个字节的字符串
- raw:raw格式的SDS,存储大于44个字节的字符串
embstr和raw的区别在于,embstr只分配一次内存空间,因为RedisObject和SDS是连续的,好处是这样创建时会少分配一次内存空间,删除时少释放一次内存空间,所有数据连在一起,方便查找,坏处是如果字符串长度改变,需要整个重分配内存空间,因此embstr只读。raw需要分配两次内存空间。
5、类型转换
类型在某些情况下会发生转换,编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换,但不包括重新set的情况
- int数据不再是整数——raw
- int大小超过了long的范围(2^64-1)–embstr
- embstr的长度超过了44个字节——raw
- 修改了embstr——raw,因为embstr是只读的,因此在对embstr进行修改时,都会转化为raw再进行修改
6、应用场景
- 缓存:缓存热点数据提高访问速度
- 分布式session:因为是分布式的独立服务,数据可以在多个应用间共享,例如可以缓存token
- 分布式锁:setnx方法,只有不存在时才能添加成功,返回true,存在竞争性
- 分布式全局ID:int类型,利用incr或incrby增加数值操作的原子性来实现一次增加一段计数,例如分库分表的场景,一次拿一段数据
- 计数器:int类型,incr方法,暂时将计数结果加入到redis中
- 限流:int类型,incr方法,访问者每做某种操作就增加计数,超过一定数值就禁止操作。
四、hash类型
如果想要在redis中存储一张表的数据,要怎么做呢?我们可以采用序列化的方式,将一张表的数据压缩成一个字符串,或者,在redis有一种方法是key分层,可以将多个key用冒号:分开,例如
mset key0:key1:key2 value0 key3:key4:key5 value1 key6:key7:key8 value2
取值时一次取出,用list接收
mget key0:key1:key2 key3:key4:key5 key6:key7:key8
但这样做显然也有缺点,就是key太长,很不方便,除此之外,还有一个方法,就是hash类型
1、用法与优缺点
hash类型用来存储多个无序的键值对,最大存储量为2^32-1(40亿),hash的value只能是string类型,不能是其他嵌套类型如hash或list
hash存储的优点是
- 节省内存空间
- 减少key冲突
- 取值减少性能消耗
hash存储的缺点是
- field不能单独设置过期时间
- 需要考虑数据量分布的问题,内部数据无法被拆分
2、常见命令
- 以hash形式保存数据:hset key field value
- 以hash形式保存多对数据:hmset key field0 value0 field1 value1 field2 value2
- 以hash形式获取数据:hget key field
- 以hash形式获取多对数据:hmget key field0 field1 field2
- 获取某个hash值的全部field:hkeys key
- 获取某个hash值的全部value:hvals key
- 获取某个hash值的全部内容:hgetall key
- 删除某个hash值的某个field:hdel key field
- 获取某个hash值的field数量:hlen key
- 删除某个hash值:del key
3、存储结构
一)ziplist(压缩列表)
ziplist(压缩列表)OJB_ENCODING_ZIPLIST,是一个经过特殊编码的,由连续内存块组成的双向链表。它不存储指向上一个链表节点和指向下一个链表节点的指针,而是储存上一个节点长度和当前节点长度。
组成内容
- prevrawlensize:int 存储上一个链表节点的长度数值所需要的字节数
- prevrawlen:int 上一个链表节点占用的长度
- lensize:int 储存当前链表节点的长度数值所需要的字节数
- len:int 当前链表节点占用的长度
- headersize:int 当前链表节点的头部大小(prevrawlensize+lensize)即非数据域的大小
- encoding:char 编码方式
- *p:char 压缩链表以字符串的形式保存,该指针指向当前节点起始位置
应用情况
- 一个hash对象保存的field数量<512个
- 一个hash对象中所有的field和value的字符串长度都<64byte
如果范围超过上面任意一个限制,就会被转化为hashtable
二)hashtable(哈希表)
1)dictEntry
redis是key-value的数据库,它采用hashtable来存储最外层的key-value结构数据,每个键值对都是一个dictEntry,通过指针指向key的存储结构和value的存储结构,而且next存储了指向下一个键值对的指针,具体内容如下
- *key:关键字定义
- *val:值定义
- *next:dictEntry 指向下一个键值对节点
2)dictht(hashtable)
dictEntry放到了dictht里面,内容如下:
- **table: dictEntry 哈希表数组
- size:long 哈希表大小
- sizemask:long 掩码大小,用于计算索引值。总是等于size-1
- used:long 已有节点数
3)dict
dictht放到dict里,内容如下:
- *type:dictType 字典数据
- *privdata:私有数据
- ht[2]:dictht 一个字典有两个hash表
- rehashidx:long rehash索引
- iterators:long 当前正在使用的迭代器数量
4)redisDb
最外层的结构实际上是redisDb,redisDb里放置的是dict,内容如下
- *dict:所有的键值对
- *expires:设置了过期时间的键值对
5) 整体结构
在dict的ht[1]指向的dictht的table一直为空,为扩容做准备,ht[0]指向一个dictEntry数组,每一个数组内容又指向一个链表,也就是说,hashtable本质上就是一个数组加链表构成的。dictEntry的位置将根据field的hash计算得到位数,如果为空则说明当前没有任何field进行hash计算后到这个地址,多于一个则证明出现hash冲突。如果dictEntry总数除以数组长度得到的负载因子,也就是平均每一个ht保存了5个entry时,证明这个table已经存满了,需要扩容。
6) 扩容rehash
rehash的步骤
- 为字符ht[1]哈希表分配空间。ht[1]的大小为第一个大于等于ht[O].used*2的2的N次方幂。比如已经使用了10000,那就是16384。
- 将所有的ht[0]上的节点rehash到 ht[1]上,重新计算hash 值和索引,然后放入指定的位置。
- 当ht[0]全部迁移到了ht[1]之后,释放 ht[0]的空间,将ht[1]设置为ht[0]表,并创建新的 ht[1],为下次rehash做准备。
4、应用场景
购物车使用场景
- key:用户ID(一个用户一个购物车)
- field:商品ID(购物车有很多商品)
- value:商品数量(每种商品数量不同)
- 商品数量增加:hincr
- 商品数量减少:hincrby key field -1
- 删除商品:hdel
- 全选商品:hgetall
- 购物车商品种类数:hlen
五、list类型
1、用法与优缺点
存储有序的字符串(从左到右,因此左边是header,右边是tail),元素可以重复,最大存储数量2^32-1(40亿左右)
2、常见命令
- 向某个队列的左边放入一个元素:lpush key value
- 向某个队列的左边放入数个元素:lpush key value0 value1
- 向某个队列的右边放入数个元素:rpush key value0 value1
- 某个队列的最左边元素弹出,这个队列将失去这个元素:lpop key
- 某个队列的最右边元素弹出,这个队列将失去这个元素:rpop key
- 取出队列的第几位元素:lindex key 0
- 取出队列的第几位到第几位元素:lrange key 0 -1(0代表开始,-1代表末尾)
3、存储结构
在较早的版本中,list采用ziplist和linkedlist两种结构,后来被替换为quicklist
一)整体结构
二)结构内容
1)quicklist
quicklist的内容如下:
- *head:quicklistNode 指向双向列表的表头
- *tail:quicklistNode 指向双向列表的表尾
- count:long 所有的ziplist中一共存了多少个元素
- len:long 双向链表的长度,node的数量
- fill:int ziplist最大大小,对应list-max-ziplist-size
- compress:压缩深度,对应list-compress-depth
- bookmark_count:4位,bookmarks数组的大小
- bookmarks[]:quicklistBookmark 是一个可选字段,quicklist重新分配内存空间时使用,不使用时不占用空间。
2)list-max-ziplist-size(fill)
- 正数表示单个ziplist最多所包含的entry个数
- 负数表示单个ziplist的大小,默认8K
- 数字代表:-1:4KB;-2:8KB;-3:16KB;-4:32KB;-5:64KB;
3)list-compress-depth(compress)
压缩深度,默认是0
- 首尾的ziplist不压缩
- 首尾第一第二个ziplist不压缩,以此类推。
4)quicklistNode
- *prev:quicklistNode 指向前一个节点
- *next:quicklistNode 指向后一个节点
- *zl:char 指向实际的ziplist
- sz:int 当前ziplist占用多少字节
- count:int 当前ziplist中存储了多少个元素,占16bit(下同),最大65536个
- encoding:int 是否采用了LZF压缩算法压缩节点(RAW1,LZF0)
- container:int 存储结构为ziplist,未来可能支持其他结构存储(NONE1,ZIPLIST2)
- recompress:int 当前ziplist是不是已经被解压出来作临时使用
- attempted_compress:int 测试用
- extra:int 预留给未来使用
4、应用场景
一)列表
用户的消息列表,网站的公告列表,活动列表,博客的文章列表,评论列表。
存储所有字段,lrange取出一页
二)队列/栈
当做分布式环境下的队列/栈使用
list提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间(单位:秒)
- BLPOP:BLPOP key timeout 移出并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
- BRPOP:BRPOP key timeout 移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
- 队列:先进先出,因此使用rpush和blpop,左头右尾,右边进入队列,左边出队列。
- 栈:先进后出,因此使用rpush和brpop。
六、set类型
1、用法与优缺点
set存储string类型的无序集合,最大存储数量为2^32-1(40亿左右)
2、常见命令
- 添加一个或者多个元素:sadd key value0 value1 value2
- 获取所有元素:smembers key
- 统计元素个数:scard key
- 随机获取一个元素:srandmember key
- 随机弹出一个元素:spop key
- 移除一个或者多个元素:srem key value0 value1 value2
- 查看元素是否存在:sismember key value
3、存储结构
因为可以存储两种类型,int和string类型,所以采用两种存储方式,intset或hashtable
一)inset
如果元素都是整数类型,就用intset存储
内容如下:
- encoding:编码类型
- length:长度,最大长度
- contents[]:用来存储成员的动态数组
二)hashtable
如果元素不全为整数类型,或者都是整数类型但是超过512个的时候,就会使用hashtable。
512是一个设置值,可以改变
set类型没有value,但是可以将value置为空来解决hashtable存储set数据的问题。
4、应用场景
一)抽奖
利用随机获取元素的方法,spop setkey
二)点赞,签到,打卡
利用set集合不可重复的特性,可以对内容进行维护和筛选
- 打卡:sadd
- 取消打卡:srem
- 是否打卡:sismember
- 所有打卡者:smembers
- 打卡人数:scard
三)商品标签
用来维护商品的不同标签
四)商品筛选
不同的set之间可以取得交集,并集和差集
- 获取差集:sdiff set1 set2
- 获取交集:sinter set1 set2
- 获取并集:sunion set1 set2
五)用户关注,互相关注
利用set存储用户关注的账号,利用并集获取互相都关注的账号
六、zset类型
1、用法与优缺点
zset存储有序的元素,每一个元素都有一个score,按照score从小到大排序,score相同的时候,按照key的ASCII吗排序
2、常见命令
- 添加元素:zadd zsetkey score0 value0 score1 value1 score2 value2
- 获取所有元素和分值,从小到大排列:zrange zsetkey 0 -l withscores
- 获取所有元素和分值,从大到小排列:zrevrange zsetkey 0 -1 withscores
- 根据分值区间获取元素:zrangebyscore zsetkey score0 score1
- 移除元素,也可以根据score或rank删除:zrem zsetkey value0 value1
- 统计元素个数:zcard zsetkey
- 分值递增:zincrby zsetkey score value
- 根据分值统计个数:zcount zsetkey score0 score1
- 获取元素rank:zrank zsetkey value
- 获取元素score:zscore zsetkey value
- 也有倒序的rev操作(reverse)
3、存储结构
采用两种存储方式,ziplist和skiplist+dict
一)ziplist
如果元素数量小于128,并且所有元素长度小于64bytes,使用ziplist,如果这两个条件任意一个不满足,则转换为skiplist+dict结构。
ziplist会根据score进行排序,插入的时候会移动后面的数据
内容如下:
- encoding:编码类型
- length:长度,最大长度
- contents[]:用来存储成员的动态数组
二)skiplist+dict
skiplist,跳表,和普通链表不同的是,它还有跳跃指针。
1)有序链表
在一个有序链表中,想要找到一个数据,就必须从头开始进行逐个比较,直到找到包含数据的那个节点,或是比给定数据大的节点为止,时间复杂度为O(N),当需要插入数据时,同理。二分查找只适合有序数组,有序链表无法二分查找。
2)第二层链表
假如每相邻两个节点增加一个指针,让该指针指向下下个节点,这样所有新增加的指针就组成了一个新的链表,但它包含的节点个数只有原来的一半,现在再查找数据时,就可以先沿着新的链表进行查找,找到比它大的节点时就可以进入下一层链表继续查找。
3)跳表
实际在skiplist中,节点的链表层级(level)被算法控制决定,在某种层度上来说是随机的,因此链表不止一层。
算法如下
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
4)具体实现
zskiplist的内容组成
- *header:zskiplistNode 指向跳跃表的头结点
- *tail:zskiplistNode 指向跳跃表的尾结点
- length:long 跳跃表的节点数
- level:int 最大层数
zskiplistNode的内容组成
- ele:sds zset的元素
- score:double 分值score
- *backwark:zskiplistNode 后退指针
- zskiplistLevel
- *forward:zskiplistNode 前进指针,对应level的下一个节点
- span:long 从当前节点到下一节点的跨度(跨越的节点数)
- level[]:层
4、应用场景
排行榜,通过点击来进行计数排名
- zincrby:对某条新闻进行点击数加一
- zrevrange:倒序排列新闻热度,即从大到小排列
七、其他类型
1、bitmaps
bitmaps是在字符串类型上定义的位操作,一个字节由八位二进制组成,可以进行多种位运算
2、hyperloglogs
hyperloglogs提供了一种不太精确的基数统计方法,用来统计一个集合中不重复的元素个数。
3、geo
geo主要用于存储地理位置信息,并对存储的信息进行操作,如增加,获取地址位置信息,计算两个位置间的距离,指定范围内的迪狸合集
4、streams
streams是 Redis 5.0 版本新增加的数据结构,主要用于支持多播可持久化消息队列,用于实现发布订阅功能。