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[]来实现。
- 使用字符数组必须先给目标变量分配足够的空间,否则会有溢出的情况;
- 如果要获取字符数据的长度,必须遍历整个字符数组,比较耗时(时间复杂度是O(n));
- 字符串长度如果发生变更,需求重新分配内存空间;
- c语言中字符串的尾部是以‘\0’来标示的,因此不能存储图片、音视频、压缩文件等二进制保存的内容,二进制不安全,这里也解释了redis为什么是Binary-safe strings。
SDS特点:
- 不用担心内存溢出问题,会自动扩容;
- 内部结构存储了字符串长度(定义了len属性),获取字符串长度时间复杂度是O(1)的;
- 通过空间预分配和惰性空间释放来防止多次重复分配内存;
- 判断是否结束的标示是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的区别
- 把所有相关的值聚集到一个key中,节省内存空间;
- 只使用一个key,减少key之间的冲突;
- 存储的是一个完整的对象信息,减少了将对象信息分开存储的I/O和cpu的消耗;
-
hash不适合的场景
- Field不能单独设置过期时间;
- 需要考虑数据量分布问题,value值非常大的时候,无法分布到多个节点;
- 没有bit操作;
存储原理
redis的hash本身也是一个kv的结构,类似于java中的HashMap。
外层的哈希(redis kv的实现)只用到了hashtable,当存储hash数据类型时,一般叫做内层的哈希。
内层的哈希底层可以使用两种数据结构实现:ziplist:OBJ_ENCODING_ZIPLIST(压缩列表),hashtable:OBJ_ENCODING_HT(哈希表)。
- ziplist压缩列表: 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少、字段值小的场景。当hash对象同时满足以下两个条件的时候,使用ziplist编码:
- 所有的键值对的键和值的字符串长度都小于等于64byte(一个英文字母一个字节);
- 哈希对象保存的键值对数量小于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的步骤:
- 为字符ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作和ht[0]当前包含的键值对的数量。(ht[1]的大小为第一个大于等于ht[0].used*2);
- 将所有的ht[0]上的节点rehash到ht[1]上,重新计算hash值和索引,然后放到指定的位置;
- 当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:
- 元素个数小于128;
- 所有member的长度都小于64字节;
在ziplist的内部,按照score排序递增来存储,插入的时候要移动之后的数据。超过阈值之后使用skiplist+dict存储。
- skiplist(跳跃表)
假设我们要查找22这个值,
- 22首先和5比较,然后再和17比较,22比它们都大,所以继续向后比较;
- 当22和27比较时,发现22小于27,则会进入下面的链表,开始比较;
- 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 发布!