Redis系列 - Redis基本数据类型与操作
- 字符串String
- 散列Hash(key-filed-value)
- 列表List
- 集合Set
- 有序集合sorted set(zSet)
字符串String
字符串是Redis中最常用的类型,是一个由字节组成的序列,它在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据。Value最多可以容纳的数据长度为512MB。
- set (set key value)
- get (get key)
- setnx(setnx key value) 如果key存在则设置失败
- mset(mset key1 value1 key2 value2 ... ) 批量设置键值对
- msetnx(mset key1 value1 key2 value2 ... ) 批量设置键值,如果key存在则设置失败
- getset(getset key newValue) 获取key的值然后设置新的值
- mget(mget key1 key2, key3) 批量获取
- incr(incr key) 自增1
- incrby(incrby key num) 指定增加的数量
- decr(decr key) 自减1
- decrby(decby key num) 指定减少的数量
- append(append key value) 给指定的字符串追加value的值
- strlen(strlen key) 获取指定的key对应的值长度
当我们存储一个string类型的键值对时会占用多少的空间呢?Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,但是会申请32字节,RedisObject的元数据和ptr指针分别占用8字节,共16字节,简单动态字符串(SDS)的len占用4字节,alloc占用4字节,buf存储实际数据,以raw编码的string为例,示意图如下:
为什么dicEntry会申请32字节呢?
jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。举个例子。如果你申请 6 字节空间,jemalloc 实际会分配 8 字节空间;如果你申请 24 字节空间,jemalloc 则会分配 32 字节。所以,在我们刚刚说的场景里,dictEntry 结构就占用了 32 字节。
当embstr和raw类型的存储的string数据时需要消耗80字节的额外存储空间,而int类型的需要消耗48字节的额外的存储空间,所以当需要存储的key-value的数据很小时(如key=5,value=10这种单值的键值对),string类型会消耗更多的空间,string类型就不再适合了,具体用什么存储在本文后面的章节会详细介绍。
散列Hash
Redis中的散列可以看成具有String key和String value的map容器,可以将多个key-value存储到一个key中。每一个Hash可以存储4294967295个键值对。
- hset(hset key field value) 给指定的key添加key-value元素
- hget(hget key field) 获取指定的key中field字段的值
- hsetnx(hsetnx key field value) 如果key和field不存在进行插入,如果key和field都存在不进行插入
- hexists(hexists key field) 判断指定的key中是否存在field这个字段
- hlen(hlen key) 获取哈希表中字段的数量
- hdel(hdel key field1 field2 ... ) 删除指定的key中的指定的字段和对应的值
- hincrby(hincrby key field count) 给指定的key中的field的字段添加或减去count这个值
- hgetall(hgetall key) 获取key中所有的键值对,返回的是一个键值对
- hkeys(hkeys key) 获取指定的key中的所有字段
- hmget(hmget key field1 field2 ...) 获取指定的key中的指定字段的值
- hmset(hmset key field1 value1 field2 value2 ...) 同时设置多个键值对数据
- hvals(hvals key) 获取指定的key中所有的value
在Redis常用的五种数据类型与底层数据结构对应关系图中,Redis Hash 类型的两种底层实现结构,分别是压缩列表和哈希表。
那么,Hash 类型底层结构什么时候使用压缩列表,什么时候使用哈希表呢?其实,Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了。
这两个阈值分别对应以下两个配置项:
- hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
- hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
如果我们往 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表。一旦从压缩列表转为了哈希表,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了。在节省内存空间方面,哈希表就没有压缩列表那么高效了。
当存储单值键值对时,我们可以使用Hash散列的方式进行存储,只要hash-max-ziplist-entries和hash-max-ziplist-value不超过设置的值,那么基本存储单值键值对只需要16字节就可以完成,避免string类型需要使用64字节浪费空间。
列表List
Redis的集合是无序不可重复的,此处的无序是数据不能重复。和列表一样,在执行插入和删除以及判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。
- lpush (lpush key value1 value2 ...) 往list集合中压入元素
- linsert (insert key before/after value newValue) 往指定元素前或后插入元素
- lset (lset key index newValue) 设置指定下标下的值
- lrem (lrem key count value) 删除count个与value相同的元素,count>0从开始位置进行删除,count<0从末尾开始删除,count=0删除所有
- ltrim (ltrim key startIndex endIndex) 删除指定范围内以外的元素,保留指定范围内的元素
- lpop (lpop key) 从list的头部删除元素
- lindex (lindex key index) 返回指定索引处的元素
- llen (llen key) 返回列表的长度
- rpush (rpush key value) 从末尾压入元素
- rpop (rpop key) 从末尾删除元素
- rpoplpush (rpoplpush key1 key2) 从key1链表中弹出最后一个元素然后压入到key2链表中
创建新列表时 redis 默认使用 redis_encoding_ziplist 编码, 当以下任意一个条件被满足时, 列表会被转换成 redis_encoding_linkedlist 编码:
- 试图往列表新添加一个字符串值,且这个字符串的长度超过 server.list_max_ziplist_value (默认值为 64 )。
- ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。
这两种存储方式的优缺点
- 双向链表linkedlist便于在表的两端进行push和pop操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
- ziplist存储在一段连续的内存上,所以存储效率很高。但是,它不利于修改操作,插入和删除操作需要频繁的申请和释放内存。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝。
Redis3.2+ list的新实现quickList
Redis中的列表list,在版本3.2之前,列表底层的编码是ziplist和linkedlist实现的,但是在版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现。
在版本3.2之前,当列表对象中元素的长度比较小或者数量比较少的时候,采用ziplist来存储,当列表对象中元素的长度比较大或者数量比较多的时候,则会转而使用双向列表linkedlist来存储。
可以认为quickList,是ziplist和linkedlist二者的结合;quickList将二者的优点结合起来。
quickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。本质上来说,quicklist里面保存着一个一个小的ziplist。结构如下:
quickList就是一个标准的双向链表的配置,有head 有tail。
每一个节点是一个quicklistNode,包含prev和next指针。
每一个quicklistNode 包含 一个ziplist,*zp 压缩链表里存储键值。
所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。
集合Set
Redis的集合是无序不可重复的,此处的无序是数据不能重复。和列表一样,在执行插入和删除以及判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。
- sadd (sadd key member1 member2 ...)
- scard (scard key)
- sismembers (sismembers key)
- smembers (smembers key)
- spop (spop key)
- srem (srem key member1 member2 ...)
- smove (smove source desition member)
- sdiff (sdiff key1 key2)
- sdiffstore (sdiffstore destionset key1 key2)
- sinter (sinter key1 key2 ...)
- sinterstore (sinterstore destionset key1 key2 ...)
- sunion (sunion key1 key2 ...)
- sunionstore (sunionstore destionset key1 key2)
集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。
集合对象set有两种实现:哈希表(hashtable)和整数数组(intset)。
intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。
hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现. HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null。
何时使用整数集合(intset),何时使用哈希表(hashtable):
- 集合对象中所有元素都是整数
- 集合对象所有元素数量不超过512
当满足上面的两个条件中的任何一个时会使用整数集合,否则使用哈希表。
有序集合sorted set:
和set很像,都是字符串的集合,都不允许重复的成员出现在一个set中。他们之间差别在于有序集合中每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。尽管有序集合中的成员必须是唯一的,但是分数(score)却可以重复。
- zadd (zadd key score1 member1 score2 member2 ...) 添加成员
- zcard (zcard key) 计算元素个数
- zincrby (zincrby key number member) 给指定的member的分数添加或减去number这个值
- zcount (zcount key min max) 获取分数在min和max之间的成员和数量;默认是闭区间
- zrange (zrange key start stop [WITHSCORES]) 返回指定排名之间的成员(结果是分数由低到高)
- zrevrange (zrevrange key start stop [WITHSCORES]) 返回指定排名之间的成员(结果是分数由高到低)
- zrangebyscore (zrangebyscore key min max [withscores] [limit offset count]) 根据分数的范围获取成员,按照分数由低到高
- zrevrangebyscore (zrevrangebyscore key min max [withscores] [limit offset count]) 根据分数的范围获取成员,按照分数由高到低
- zrank (zrank key member) 返回一个成员的排名,从低到高
- zrevrank (zrevrank key member) 返回一个成员的排名,从高到低
- zscore (zscore key member) 获取一个成员的分数
- zrem (zrem key member1 member2 ...) 删除指定的成员
有序集合sorted set有两种实现方式:压缩列表和跳表。那么这两种数据结构是如何使用的呢?
首先,我们来看这两个参数:
- zset-max-ziplist-entries 128
- zset-max-ziplist-value 64
ziplist 这种数据结构,顾名思义:压缩列表,怎么个压缩法,简单来说就是对于大的数据会用比较多点字节来存储,对于小的数据就用比较少的字节来存储。
当field-value对的数量大于ziplist.entries.size或者任意一个filed或value长度大于zset-max-ziplist-value时会使用zset进行存储
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
zset 结构体里有两个元素,一个是 dict,用来维护 数据 到 分数 的关系,一个是 zskiplist,用来维护分数所在链表的关系。