Redis常见对象有8种:
- 字符串对象 -string
- 列表对象 - list
- 哈希对象-hash
- 集合对象-set
- 有序集合对象-zset
- Bitmap
- Geo
- Hyperloglog
0、Redis对象介绍
Redis中的键值都是用对象表示的,新建键值时,每次都会至少创建两个对象:键对象、值对象。
Redis中的对象redisObject定义如下:
typedef struct redisObject {
unsigned type:4; // 对象类型, string、list、hash、set、zset
unsigned encoding:4; // 编码方式
unsigned lru:LRU_BITS;
int refcount; //引用次数
void *ptr; // 数据指针
} robj;
- 类型type:使用
TYPE key
,会分别输出: string、list、hash、set、zset,对应5种类型的对象。 - encoding:有字符串(int、embstr、raw)、字典(hashtable)、双端链表(linkedlist)、压缩列表(ziplist)、整数集合(intset)、跳跃表和字典(skiplist)。
一、字符串对象(stirng)
字符串对象的编码可以是int、raw、embstr 3种。
1、int:当字符串对象保存整数值时,使用int编码方式,值会保存到ptr属性中(省去了ptr指针占用的空间):
2、raw:当字符串对象保存字符串值,且字符串 > 39字节,使用SDS简单动态字符串来保存,编码方式为raw:
3、embstr:用于保存短字符串(<=39字节)。
与raw编码区别:raw编码会调用两次内存分配函数创建redisObject和sds。embstr只需要调用一次内存分配函数来分配一块连续的空间(减少内存碎片)。
常用操作命令:
set key value
get key
mset key1 value1 keys2 value2
mget key1 key2...
incrby key integer :将key的值 + integer值
dncrby key integer :将key的值 - integer值
二、列表对象(list)
列表对象编码是 ziplist 或 linkedlist。
当列表对象保存的是字符串元素,且:
- 字符串长度 < 64字节
- 字符串个数 < 512个
两者必须满足才会使用ziplist,否则用linkedlist。
1、ziplist (压缩列表)
RPUSH numbers 1 "three" 5
2、linkedlist (双端链表)
list对象常用命令:
l - left r - right
lpush key value1 [value2]
rpush key value1 [value2]
三、哈希对象(hash)
哈希对象编码可以是 ziplist 或 hashtable。
当哈希对象保存的键值对 键值字符串长度均 < 64字节;键值对个数都小于 < 512个,使用ziplist;否则会使用hashtable。
1、ziplist
使用压缩列表保存哈希对象时,先保存键对象,再保存值对象。新添加的元素放到表尾。
如 集合key为player,集合value中的key、value可以是 basketball - James; baskterball - Jordan; football - belly
即:hset player baskterball James
hset player baskterball Jordan
hset player football belly
=> hset key k v
2、hashtable
键值都是用字符串对象进行保存:
常用命令:
存储:hset book name "Java"
hset book price "100"
获取:hget book name ==> Java
hget book price ==> 100
四、集合对象(set)
集合对象编码使用intset 和 hashtable。
当集合对象保存的元素都是整数值 && 元素个数不超过512个时采用intset,否则使用hashtable。
1、intset:都是存储整数时,使用该编码方式;
2、hashtable:只使用字典键保存,每个键都是字符串对象,值设置为null。
常用命令:
sadd numbers 1 2 3
获取集合所有元素:smembers numbers
删除元素:srem numbers 2
获取集合个数:scard numbers
五、有序集合对象(zset)
有序集合编码使用ziplist和 skiplist。
元素个数< 128 && 长度小于64字节时,使用skiplist编码。
1、ziplist
压缩列表将元素值和分数一起保存。压缩列表中的元素按分值从小到大排序,分值较小的元素放在靠近表头的位置。
如:zadd price 8.5 apple 7.2 banana 2 cherry
,key为price, value为[8.5-apple]、[7.2-banana]、[2-chery]。
2、skiplist
zset同时使用 字典+跳跃表的方式实现有序集合:zsl + dict。
(1)zsl:跳跃表节点object属性保存元素,score保存分值。通过跳跃表可以对有序集合进行范围查询,便于范围查找。
(2)dict:保存了对象 与 分值的映射,目的:便于查找,查找O(1)
注:为什么有序集合同时使用跳跃表和字典?
=》 如果只使用zsl,在查找某个成员时时间复杂度会从O(1)变为O(logn)。如果只使用dict,在进行范围查找时,需要先遍历所有元素分值排序,最好时间复杂度是O(nlogn),同时需要额外的空间复杂度O(k)进行保存。
特殊对象类型
- BitMap
- Geo
- HyperLogLog
1、BitMap - 省空间、适用于2值状态统计,即0或1统计如签到,要么签了,要么没签到
位图不是一个真实的数据类型,而是定义在字符串类型上的面向位的操作的集合。由于字符串类型是二进制安全的二进制大对象,并且最大长度是 512MB,适合于设置 2^32个不同的位。
位图操作是用来操作比特位的,其优点是节省内存空间。为什么可以节省内存空间呢?假如我们需要存储100万个用户的登录状态,使用位图的话最少只需要100万个比特位(比特位1表示登录,比特位0表示未登录)就可以存储了,而如果以字符串的形式存储,比如说以userId为key,是否登录(字符串“1”表示登录,字符串“0”表示未登录)为value进行存储的话,就需要存储100万个字符串了,相比之下使用位图存储占用的空间要小得多,这就是位图存储的优势。
BitMap底层通过对字符串的操作来实现。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。你可以把 Bitmap 看作是一个 bit 数组。
语法:SETBIT key offset value
比如统计某用户一年打卡的天数。key为该用户id,value为365位的Bitmap,每一个bit对应该用户当天的签到情况,1-代表已签到,11000011代表用户号,0 1 2…360代偏移量,分别代表第1、2、3、360天。
比如第一天:setbit user:sign:11000011:2020 0 1
第2天:setbit user:sign:11000011:2020 1 1
,
第3天:setbit,
最后统计该用户一年的打卡次数:
BITCOUNT uid:sign:11000011:2020
再比如,统计1亿个用户10月份的签到情况。key为每一天的日期,31个key;value为1亿位的Bitmap,1代表已签到。
10.01:
// 3个用户在10月1号的签到状态
setbit user:sign:20201001 user01 1
setbit user:sign:20201001 user02 0
setbit user:sign:20201001 user03 1
10.02:
// 3个用户在10月2号的签到状态
setbit user:sign:20201002 user01 1
setbit user:sign:20201002 user02 0
setbit user:sign:20201003 user03 1
统计对31个Bitmap进行与操作,得到最后的Bitmap,通过bitcount获取结果。
BITCOUNT uid:sign:20201001
+
BITCOUNT uid:sign:20201002
+
BITCOUNT uid:sign:20201003
1个1亿为的Bitmap占用的内存为:10^8 bit = 10 ^8 / 8 = 1.25 * 10^7 字节 = 1.2207 * 10^4 KB = 12 MB
https://mp.weixin.qq.com/s?spm=a2c6h.12873639.0.0.5c245c15Ac1CVJ&__biz=MzAxNjM2MTk0Ng==&mid=2247484427&idx=1&sn=cb810acc286b9f85796ef4dc35587309&chksm=9bf4b4beac833da86eff09b2f68195930e5fd1c374448c7b22b16725a4ec717dacac63ba1da7&scene=21#wechat_redirect - 如何优雅地使用Redis之位图操作
2、HyperLogLog
HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。
在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。你看,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
比如我们统计一个网页页面访问人数,可以使用Set类型去重统计。但是随着元素越多占用的内存就越大,可以使用HyperLogLog进行统计:
PFADD page1:uv user1 user2 user3 user4 user5
,查询:PFCOUNT page1:uv
实现原理:HyperLogLog算法、https://blog.csdn.net/u011489043/article/details/78727128
大概思想是:统计一组数据中不重复元素的个数,集合中每个元素经过hash函数后可以表示成0和1构成的二进制数字串。二进制串中0和1出现的次数就有点像抛硬币实现,通过局部不同的数组估算整体。会有误差,在0.8左右。
3、Geo
Geo底层基于Sorted Set实现,GEO 类型是把经纬度所在的区间编码作为 Sorted Set 中元素的权重分数。
如何编码?
采用GeoHash,即”二分区间,区间编码“的方法,对一组经纬度分别对经度和纬度编码,最终合成一个最终编码。
对经纬度区间不断二分,落在左区间的设置为0,落在右区间的设置为1.
最终编码的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始。
1110011101。
总结、几种对象使用的数据结构
几种对象应用场景
-
string:最常见的一种数据类型,普通的KV存储。
比如存放用户信息,key为用户id,value可以json格式存储:set userid {“name”:“aaa”,“age”:“123”}
唯一id生成:先初始化:
set uniqueid 1
,每次获取都执行incr uniqueid
返回递增后的id值,单线程处理不用考 虑并发; -
list:
重试队列,通过list添加任务;执行任务时不断查询list有无待执行任务。
-
set:
主要是去重。一些需要去重的场景可以使用set,比如统计、过滤功能。小数据量的统计可以使用set。
-
zset:
排行榜。如电影排行榜:
zadd movie_rank_board 1 功夫 2 逃学威龙 3 大话西游
-
hash:
秒杀场景下,不同商家的的不同商品限购xx件。可以采用 商家id作为key;参与抢购的商品id作为field,对应抢购数量作为value存储。如:
hmset 781287 product-001 100 product-002 50
, 抢购时,执行扣减操作:
内存回收
Redis在对象上加上引用计数器实现内存回收机制:
- 创建一个对象时,引用计数器refCount = 1;
- 当对象被引用时,refCount++;
- 使用完了之后,refCount–;
- refCount = 0时会被释放。
值对象共享- 只针对整数值的对象字符串才会进行对象共享
当键A创建一个整数值100的字符串对象作为值对象时, A <===> 100;键B也需要创建一个值100的字符串对象作为值对象时,Redis会将键A、B指向同一个字符串对象100,同时100这个字符串对象的refCount++ (=3,初始为1,被服务器引用)。
注:为什么不共享字符串数组的对象?
==》 Redis想要将一个共享对象设置为某个键的值对象时,需要检查这个共享对象是否是想要的。
因此这个共享对象保存的值不能太复杂,否则在验证的时候时间复杂度会过高。目前Redis只对包含整数值的字符串对象进行共享,验证时间复杂度是O(1)。
参考:
《Redis设计与实现》
《Redis核心技术与实战 》