对象 Object 以及类型
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
/* Objects encoding. */
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap 已废弃!*/
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
typedef struct redisObject {
unsigned type:4;/* 对象类型,分别是string、hash、list、set、和zset 见上述宏定义 */
unsigned encoding:4; // 底层编码方式,共11种,见上述宏定义
unsigned lru:LRU_BITS; /* LRU(Least recently used) TIME,最新被访问的时间
* 最低有效8位频率 和 最高有效16位访问时间,共24个bit */
int refcount; //引用计数器,类似于linux的文件系统,若无引用则释放对象
void *ptr; //指向 真实数据地址,8字节
} robj;// 一共占16个字节
在Redis中,任何一个对象都需要有一个对象头,每个对象头会占用16个字节的内存空间,所以为了提高效率,在不影响逻辑使用的情况下应优先选择其他对象类型而并非使用string 。
外部由一个kv对组成,但v可以是多种数据结构,但统一k对应的v只可以是一种数据结构。
而对于Key而言,Redis官方文档(redis.cn)是这样说的:
Redis key值是二进制安全的,这意味着可以用任何二进制序列作为key值,从形如”foo”的简单字符串到一个JPEG文件的内容都可以。空字符串也是有效key值。
关于key的几条规则:
- 太长的键值不是个好主意,例如1024字节的键值就不是个好主意,不仅因为消耗内存,而且在数据中查找这类键值的计算成本很高。
- 太短的键值通常也不是好主意,如果你要用”u:1000:pwd”来代替”user:1000:password”,这没有什么问题,但后者更易阅读,并且由此增加的空间消耗相对于key object和value object本身来说很小。当然,没人阻止您一定要用更短的键值节省一丁点儿空间。
- 最好坚持一种模式。例如:”object-type: id:field”就是个不错的注意,像这样”user:1000:password”。我喜欢对多单词的字段名中加上一个点,就像这样:”comment: 1234:reply.to”。
1. string:二进制安全字符串
这是最简单Redis类型。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器(注:memcache的数据仅保存在内存中,服务器重启后,数据将丢失)。
字符串类型的内部编码有三种: 1、int:存储 8 个字节的长整型(long,2^63-1)。 2、embstr:代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串), 存储小于 44 个字节的字符串,只分配一次内存空间(因为 Redis Object 和 SDS 是连续的)。 3、raw:存储大于 44 个字节的字符串,需要分配两次内存空间(分别为 Redis Object 和 SDS 分配空间)。
127.0.0.1:6379> set str:int 123321
OK
127.0.0.1:6379> set str:emb TEnth_is_my_name
OK
127.0.0.1:6379> set str:raw TEnth_is_my_nameTEnth_is_my_nameTEnth_is_my_nameTEnth_is_my_nameTEnth_is_my_nameTEnth_is_my_name
OK
127.0.0.1:6379> set str:raw2 123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789
OK
127.0.0.1:6379> OBJECT ENCODING str:int
"int"
127.0.0.1:6379> OBJECT ENCODING str:emb
"embstr"
127.0.0.1:6379> OBJECT ENCODING str:raw
"raw"
127.0.0.1:6379> OBJECT ENCODING str:raw2
"raw"
典型应用
- 对象存储。value极少修改的情况下,可以通过json 存储对象
- 累加器 64位。通过 INCR 命令细线,并且 INCR 命令是一个原子操作
- 分布式锁,加锁时 setnx 加锁,那么如果其他进程争夺锁就会返回setnx 结果为0,以做到加锁,解锁时使用del删除这个字段。
- 位运算存储,因为string的二进制安全。命令: getbit\setbig key offset 并可以获取当前多少个位为1 : bitcount key
2. list:链表
链表从redis3开始,底层实现就是通过 quicklist 实现的了,它有如下特性:
- 不能确保唯一性
- 有序,但仅仅保证插入有序
值得注意的是,LPUSH或者RPUSH 插入元素时,元素总是以头插法在L端或者R端插入到链表,如:
127.0.0.1:6379> lpush list:test 1 2 3 abc "tenth"
(integer) 5
127.0.0.1:6379> OBJECT ENCODING list:test
"quicklist"
127.0.0.1:6379> lrange list:test 0 -1
1) "tenth"
2) "abc"
3) "3"
4) "2"
5) "1"
典型应用
list可被用来实现聊天系统。还可以作为不同进程间传递消息的队列。关键是,你可以每次都以原先添加的顺序访问数据。这不需要任何SQL ORDER BY 操作,将会非常快,也会很容易扩展到百万级别元素的规模。
- 在评级系统中,比如社会化新闻网站 reddit.com,你可以把每个新提交的链接添加到一个list,用LRANGE可简单的对结果分页。
- 在博客引擎实现中,你可为每篇日志设置一个list,在该list中推入博客评论,等等。
- 实现栈和队列的功能:阻塞队列(关键特性)阻塞连接,等待有值并返回。由于可以C语言扩展实现自己的数据结构,通过阻塞特性实现一个分布式的公平锁
3. hash:哈希表
由于数据库层面就已经使用了 k-v 形式,所以hash在使用中类似于二级哈希。而哈希表底层是通过dict和listpack来实现的,当成员比较少时为了节约内存,则采用listpack(原为ziplist)。
127.0.0.1:6379> hmset hash:listpack name TEnth gender male height 178cm
OK
127.0.0.1:6379> OBJECT ENCODING hash:listpack
"listpack"
127.0.0.1:6379> hmset hash:ht k1 v1 k2 v2 adsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfdadsasgwrdqafasfd 24864354586124864354586124864354586124864354586124864354586124864354586124864354586124864354586124864354586
127.0.0.1:6379> OBJECT ENCODING hash:ht
"hashtable"
哈希表的特点如下:
- 多用于存储对象。可以存储大量元素,查找效率O(1)。
- 哈希是确保唯一的
- 哈希中保存的数据是无序的
在使用上,若我们想存储2个学生分别叫 TEnth 和 NInth 那么我们可以通过如下办法:
127.0.0.1:6379> hmset students:1001 name TEnth gender male height 178cm
OK
127.0.0.1:6379> hmset students:1002 name NInth gender female height 164cm
OK
127.0.0.1:6379> hget students:1001 height
"178cm"
127.0.0.1:6379> hmget students:1002 gender height
1) "female"
2) "164cm"
127.0.0.1:6379> HGETALL students:1001
1) "name"
2) "TEnth"
3) "gender"
4) "male"
5) "height"
6) "178cm"
4. set:集合
set在底层中,对应实现方式是intset或者dict,集合的特点是:
- 确保唯一性
- 无序性
但是,当集合中全为整数时,集合元素若全为整数并且数量不大的情况下,实现方式是intset,其实是可以保证有序的,并且查找的方式是二分查找,时间复杂度为O(logn)。
127.0.0.1:6379> sadd set:intset 1 2 100001
(integer) 3
127.0.0.1:6379> OBJECT ENCODING set:intset
"intset"
127.0.0.1:6379> sadd set:intset "tenth"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING set:intset
"hashtable"
典型应用
对于set来说,它的使用逻辑上更多是像是我们数学理解上的集合概念,比如:取交集、取并集 和 求差集。
127.0.0.1:6379> sadd set:years:1 2020 2022 1997
(integer) 3
127.0.0.1:6379> SMEMBERS set:years:1
1) "1997"
2) "2020"
3) "2022"
127.0.0.1:6379> SISMEMBER set:years:1 2023
(integer) 0
127.0.0.1:6379> SISMEMBER set:years:1 2022
(integer) 1
127.0.0.1:6379> SCARD set:years:1
(integer) 3
127.0.0.1:6379> sadd set:years:2 2022 1998
(integer) 2
127.0.0.1:6379> sinter set:years:1 set:years:2
1) "2022"
127.0.0.1:6379> sunion set:years:1 set:years:2
1) "1997"
2) "1998"
3) "2020"
4) "2022"
127.0.0.1:6379> sdiff set:years:1 set:years:2
1) "1997"
2) "2020"
还有一个很有意思的应用,假设我们有一套扑克牌,一共54张牌,给TEnth发3张牌看看他的手气:
127.0.0.1:6379> scard game:deck
(integer) 54
127.0.0.1:6379> spop game:1:deck
"C2"
127.0.0.1:6379> spop game:1:deck
"S3"
127.0.0.1:6379> spop game:1:deck
"D5"
127.0.0.1:6379> scard game:deck
(integer) 51
可以看到,卡牌从54张满牌,随机发了3张给TEnth之后,还剩下51张了。(PS:看来TEnth确实是干大事的手气,开局就抽到了235准备打飞机)
5. zset: 有序集合,以 member->score 形式存储
zset有序集合,顾名思义就是通过一个值score将集合中的各个元素在有序的情况下放入集合。Zset 对象是唯一一个同时使用了两个数据结构来实现的 Redis 对象,这两个数据结构一个是跳表或 listpack,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询,其中哈希表这个结构的存在主要是用于查询,大部分操作都还是通过跳表或者listpack实现的。
listpack:
- 元素数量小于128个
- 所有member的长度都小于64字节
skiplist:
- 不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改
- 对于一个
REDIS_ENCODING_ZIPLIST编码的 Zset, 只要满足以上任一条件, 则会被转换为REDIS_ENCODING_SKIPLIST编码
演示如下:
127.0.0.1:6379> zadd hackers 1957 "Sophie Wilson" 1949 "Anita Borg"
(integer) 2
127.0.0.1:6379> OBJECT ENCODING hackers
"listpack"
127.0.0.1:6379> zadd hackers 1957 "asdasdadasdasdadsasdadsadadsdadadsadadasdadasadasddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddsadasdasdasdSophie Wilson"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING hackers
"skiplist"
zset的特性:
- member确保唯一性
- score确保有序性
看个官方的使用示例:
127.0.0.1:6379> zadd hackers 1940 "Alan Kay"
(integer) 1
127.0.0.1:6379> zadd hackers 1957 "Sophie Wilson" 1949 "Anita Borg"
(integer) 2
127.0.0.1:6379> zadd hackers 1969 "Linus Torvalds"
(integer) 1
127.0.0.1:6379> zrange hackers 0 -1
1) "Alan Kay"
2) "Anita Borg"
3) "Sophie Wilson"
4) "Linus Torvalds"
127.0.0.1:6379> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Sophie Wilson"
3) "Anita Borg"
4) "Alan Kay"
127.0.0.1:6379> zrange hackers 0 -1 withscores
1) "Alan Kay"
2) "1940"
3) "Anita Borg"
4) "1949"
5) "Sophie Wilson"
6) "1957"
7) "Linus Torvalds"
8) "1969"
127.0.0.1:6379> zrangebyscore hackers -inf 1950
1) "Alan Kay"
2) "Anita Borg"
127.0.0.1:6379> zremrangebyscore hackers 1940 1960
(integer) 3