Redis学习之5种数据类型操作、实现原理及应⽤场景
Redis可以存储可以存储键与5种不同数据结构类型之间的映射。五种数据类型为:STRING(字符串)、LIST(列表)、SET(集合)、HASH(散列)、ZSET(有序集合)。
⼀、字符串类型String
  1、类型定义
  String 数据结构是简单的 key-value 类型,使⽤string时,redis(⼤多数情况下)并不会理解或者解析其含义,⽆论使⽤json、xml还是纯⽂本在redis看来都是⼀样的,只是⼀个字符串,只能进⾏strlen、append等对字符串通⽤的操作,⽆法针对其内容进⼀步操作。
  其基本操作命令有set、get、strlen、getrange、append:在⼤多数情况之外,就是string中存储的为纯数字的情况,redis可以将字符串当做数字进⾏进⼀步操作,这些操作包括decr、decrby、incr、incrby和incrbyfloat。
1)赋值:SET key value。如set hello world
2)取值:GET key。如get hello。返回是world
3)⾃增:INCR key。就是Mysql的AUTO_INCREMENT。每次执⾏INCR key时,该key的值都会+1.若key不存在,则先建⽴⼀个0,然后+1,返回1。
    如果值不是整数则报错。该操作是原⼦操作。
4)⾃减:DECR key。将指定key的值减少1.如DECR num,就是num-1
5)⾃增N:INCRBY key increment⽤来给指定key的值加increment。如INCRBY num 5就是num+5
6)⾃减N:DECRBY key increment⽤来给指定key的值减increment。如DECRBY num 5就是num-5
7)增加浮点数:INCRBYFLOAT key increment。
8)向尾部追加:APPEND key value。如set test:key 123    append test:key 456      get test:key就是123456
9)获取长度:STRLEN key。
10)同时给多个key 赋值:MSET title 这是标题 description 这是描述 content 这是内容。
11)同时获取多个key的值:MGET title description content
12)位操作之获取:GETBIT key offset。如字符a在redis中的存储为01100001(ASCII为98),那么GETBIT key 2就是1,GET key 0就是0。
13)位操作之设置:SETBIT key offset value。如字符a在redis中的存储为01100001(ASCII为98),那么SETBIT key 6 0,SETBIT key 5 1
    那么get key得到的是b。因为取出的⼆进制为01100010。
14)位操作之统计:BITCOUNT key [start] [end]:BITCOUNT key⽤来获取key的值中⼆进制是1的个数。⽽BITCOUNT key start end则是⽤来
    统计key的值中在第start和end之间的⼦字符串的⼆进制是1的个数(好绕啊)。
15)位操作之位运算:BITOP operation resultKey key1 key2。operation是位运算的操作,有AND,OR,XOR,NOT。resultKey是把运算结构
    存储在这个key中,key1和key2是参与运算的key,参与运算的key可以指定多个。
  2、应⽤场景
缓存功能:字符串最经典的使⽤场景,redis最为缓存层,Mysql作为储存层,绝⼤部分请求数据都是redis中获取,由于redis具有⽀撑⾼并发特性,所以缓存通常能起到加速读写和降低后端压⼒的作⽤。
计数器:许多运⽤都会使⽤redis作为计数的基础⼯具,他可以实现快速计数、查询缓存的功能,同时数据可以⼀步落地到其他的数据源。如:视频播放数、粉丝数、微博数就是使⽤redis作为计数的基础组件。
共享session:出于负载均衡的考虑,分布式服务会将⽤户信息的访问均衡到不同服务器上,⽤户刷新⼀次访问可能会需要重新登录,为避免这个问题可以⽤redis将⽤户session集中管理,在这种模式下只要保证redis的⾼可⽤和扩展性的,每次获取⽤户更新或查询登录信息都直接从redis中集中获取。
限速:处于安全考虑,每次进⾏登录时让⽤户输⼊⼿机验证码,为了短信接⼝不被频繁访问,会限制⽤户每分钟获取验证码的频率。
  3、实现原理
redis八种数据结构
通过 int、SDS(simple dynamic string)作为结构存储
int⽤来存放整型数据,sds存放字节/字符串和浮点型数据
redis3.2分⽀引⼊了五种sdshdr类型,
⽬的是为了满⾜不同长度字符串可以使⽤不同⼤⼩的Header,从⽽节省内存
  具体请可以参考:
⼆、列表类型List
  1、类型定义
  Redis对链表(Linked-list)结构的⽀持使得其在键值存储的世界独树⼀帜,⼀个列表结构可以有序地存储多个字符串。
  使⽤list时,value就是⼀个string数组,操作这组string时,可以像对待栈⼀样使⽤pop和push操作,但是这个栈两端都能进⾏操作;也可以像对待数组⼀样使⽤⼀个index参数来操作。list的操作命令略杂,主要分为两类:L开头的和R开头的,L代表LEFT或者LIST,进⾏⼀些从列表左端进⾏的操作,或者⼀些与端⽆关的操作;R代表RIGHT,进⾏⼀些从列表右端进⾏的操作。
1)向头部插⼊:LPUSH key 。返回增加后的列表长度。
2)向尾部插⼊:RPUSH key 。返回增加后的列表长度。
3)从头部弹出:LPOP key。返回被弹出的元素值。该操作先删除key列表的第⼀个元素,再将它返回。
4)从尾部弹出:RPOP key。返回被弹出的元素值。
5)列表元素个数:LLEN key。key不存在返回0。
6)获取列表的⼦列表:LRANGE start end。返回第start个到第end个元素的列表。包含start和end。⽀持负数索引。-1表⽰最后⼀个元素,-2表⽰倒数
    第⼆个元素。
7)删除列表中指定值:LREM key count value。删除key这个列表中,所有值为value的元素,只删除count。如果有count+1个,那么就保留最后⼀个。
    count不存在或者为0,则删除所有的。如果count⼤于0,则删除从头到尾的count个,如果count⼩于0,则删除从尾到头的count个。
8)获取指定索引值:LINDEX key index。如LINDEX key 0就是列表的第⼀个元素。index可以是负数。
9)设置索引和值:LSET key index value。这个操作只是修改指定key且指定index的值。如果index不存在,则报错。
10)保留⽚段,删除其它:LTRIM key start end。保留start到end之间的所有元素,含start和end。其他全部删除。
11)向列表插⼊元素:LINSERT key BEFORE/AFTER value1 value2。从列表头开始遍历,发现值为value1时停⽌,将value2插⼊,根据BEFORE
    或者AFTER插⼊到value1的前⾯还是后⾯。
12)把⼀个列表的⼀个元素转到另⼀个列表:RPOPLPUSH list1 list2。将列表list1的右边元素删除,并把该与元素插⼊到列表list2的左边。原⼦操作。
  2、应⽤场景
  消息队列: redis的lpush+brpop命令组合即可实现阻塞队列,⽣产者客户端是⽤lupsh从列表左侧插⼊元素,多个消费者客户端使⽤brpop命令阻塞时的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和⾼可⽤性。
⽂章列表:每个⽤户都有属于⾃⼰的⽂章列表,现在需要分页展⽰⽂章列表,此时可以考虑使⽤列表,列表不但有序同时⽀持按照索引范围获取元素。
使⽤列表技巧:
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
  3、实现原理
列表类型内部使⽤双向链表实现。
  其内部数据结构为:
  value对象内部以linkedlist或者ziplist来实现:
当list的元素个数和单个元素的长度⽐较⼩的时候,Redis会采⽤ziplist(压缩列表)来实现来减少内存占⽤。
否则就会采⽤linkedlist(双向链表)结构。
  redis3.2之后,采⽤的⼀种叫quicklist的数据结构,实现ziplist和双向链表的⼆者结合,quicklist仍然是⼀个双向链表,只是列表的每个节点都是⼀个ziplist。
三、集合Set
  1、类型定义
  Redis的集合Set和列表List都可以存储多个字符串,但是列表可以存储多个相同的字符串,⽽集合则通过使⽤散列表来保证⾃⼰存储的每个字符串都是不相同的。Redis使⽤⽆序的⽅式存储元素。
  集合类型是为了⽅便对多个集合进⾏操作和运算。集合中每个元素不同且没有顺序的概念,每个元素都是且只能是⼀个字符串。常⽤操作是对集合插⼊、删除、判断等操作。时间复杂度尾O(1)。可以进⾏交集、并集、差集运算。集合类型在redis中的存储是⼀个值为空的散列表(这些散列表只有键,但是没有与键相关的值)。
1)增加:SADD key value。
2)删除:SREM key value。
3)获取指定集合的所有元素:SMEMBERS key。
4)判断某个元素是否存在:SISMEMBER key value。
5)差集运算:SDIFF 。对多个集合进⾏差集运算。
6)交集运算:SINNER 。对多个集合进⾏交集运算。
7)并集运算:SUNION 。对多个集合进⾏并集运算。
8)获取集合中元素个数:SCARD key。返回集合中元素的总个数。
9)对差集、交集、并集运算的结果存放在⼀个指定的key中:SDIFFSTORE storekey key1 key2。对key1和key2求差集,结果存放在key为storekey的
    集合中。SINNERSTORE和SUNIONSTORE类似。
10)获取集合中的随即元素:SRANDMEMBER key [count]。参数count可选,如果count不存在,则随即⼀个。count⼤于0,则是不重复的count个元素。
    count⼩于0,则是⼀共|count|个元素,可以重复。
11)随即弹出⼀个元素:SPOP key。随即从集合中弹出⼀个元素并删除,将该元素的值返回。
  2、应⽤场景 
标签(tag):集合类型⽐较典型的使⽤场景,如⼀个⽤户对娱乐、体育⽐较感兴趣,另⼀个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到同⼀标签的⼈,以及⽤户的共同爱好的标签,这些数据对于⽤户体验以及曾强⽤户粘度⽐较重要。
使⽤技巧:
sadd=tagging(标签)
spop/srandmember=random item(⽣成随机数,⽐如抽奖)
sadd+sinter=social Graph(社交需求)
  3、实现原理
底层数据结构以intset或者值为空的hashtable来存储。
四、散列Hash
  1、类型定义
  Redis的散列可以存储多个键值对之间的映射,和字符串⼀样,存储的Redis是以字典(关联数组)的形式存储的,⼀个key对应⼀个value。在字符串类型中,value只能是⼀个字符串。那么在散列类型,也叫哈希类型中,value对应的也是⼀个字典(关联数组)。那么就可以理解,Redis的哈希类型/散列类型中,key对应的value是⼀个⼆维数组。但是字段的值只可以是字符串。也就是说只能是⼆维数组,不能有更多的维度。
  散列的值既可以是字符串也可以是数字值,并且⽤户可以对散列存储的数字值进⾏⾃增操作以及⾃减操作。
1)赋值:HSET key field value。如hset user name lane。hset user age 23
2)取值:HGET key field。如hget user name,得到的是lane。
3)同⼀个key多个字段赋值:HMSET key field1 value1
4)同⼀个KEY多个字段取值:HMGET key
5)获取KEY的所有字段和所有值:HGETALL key。如HGETALL user得到的是name lane age 23。每个返回都是独⽴的⼀⾏。
6)字段是否存在:HEXISTS key field。存在返回1,不存在返回0
7)当字段不存在时赋值:HSETNX key field value。如果key下⾯的字段field不存在,则建⽴field字段,且值为value。如果field字段存在,则不执⾏
    任何操作。它的效果等于HEXISTS + HSET。但是这个命令的优点是原⼦操作。再⾼的并发也不会怕怕。
8)⾃增N:HINCREBY key field increment。同字符串的⾃增类型,不再阐述。
9)删除字段:DEL key 删除指定KEY的⼀个或多个字段。
10)只获取字段名:HKEYS key。与HGETALL类似,但是只获取字段名,不获取字段值。
11)只获取字段值:HVALS key。与HGETALL类似,但是只获取字段值,不获取字段名。
12)获取字段数量:HLEN key。
  2、应⽤场景
缓存HashMap结构型数据
模拟数据库关联查询
  由于Hash的value值就是⼀个HashMap数据结构,相对于字符串序列化缓存信息更加直观,并且在更新操作上更加便捷,所以常常⽤于**⽤户信息**等管理。
  哈希类型和关系型数据库有所不同,哈希类型是稀疏的,⽽关系型数据库是完全结构化的,关系型数据库可以做复杂的关系查询,⽽redis去模拟关系型复杂查询开发困难,维护成本⾼。
  3、实现原理
  Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构.当发⽣ hash 碰撞时将会把元素追加到链表上.值得注意的是在Redis的Hash中value只能是字符串,如图所⽰:
  源码如下:
struct dictht {
dictEntry **table;    //entry 数组
long size;            //数组长度
long used            //数组中的元素个数
...
}
struct dictEntry{
void *key;                //hash 的 key
void *val;                //hash 的 value
dictEntry *next;          //下⼀个dictEntry 链表结构
}
  在 Java 中 HashMap 扩容是个很耗时的操作,需要去申请新的数组,为了追求⾼性能,Redis 采⽤了渐进式 rehash策略.这也是 hash 中最重要的部分。那么什么是渐进式呢?
  在 hash 的内部包含了两个hashtable,⼀般情况下只是⽤⼀个。如图所⽰:
  在扩容的时候 rehash 策略会保留新旧两个 hashtable 结构,查询时也会同时查询两个 hashtable,Redis会将旧 hashtable 中的内容⼀点⼀点的迁移到新的 hashtable 中,当迁移完成时,就会⽤新的 hashtable 取代之前的.当 hashtable 移除了最后⼀个元素之后,这个数据结构将会被删除.如图所⽰:
  数据搬迁的操作放在 hash 的后续指令中,也就是来⾃客户端对 hash 的指令操作.⼀旦客户端后续没有指令操作这个 hash.Redis就会使⽤定时任务对数据主动搬迁。
正常情况下,当 hashtable 中元素的个数等于数组的长度时,就会开始扩容,扩容的新数组是原数组⼤⼩的 2 倍.如果 Redis 正在做
bgsave(持久化) 时,可能不会去扩容,因为要减少内存页的过多分离(Copy On Write).但是如果 hashtable 已经⾮常满了,元素的个数达到了数组长度的 5 倍时,Redis 会强制扩容.
当hashtable 中元素逐渐变少时,Redis 会进⾏缩容来减少空间占⽤,并且缩容不会受 bgsave 的影响,缩容条件是元素个数少于数组长度的 10%.
五、有序集合类型ZSet
  1、类型定义
  有序集合和散列⼀样,都⽤于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的,⽽有序集合的值则被称为分值(score),分值必须是浮点数,有序集合是Redis⾥⾯唯⼀⼀个既可以根据成员访问元素,⼜可以根据分值以及分值的排序来访问元素的结构。
  集合类型是⽆序的,每个元素是唯⼀的。那么有序集合就是有序的,每个元素是唯⼀的。有序集合类型和集合类型的差别是,有序集合为每个元素配备了⼀个属性:分数。有序集合就是根据分数来排序的。有序集合是使⽤散列表和跳跃表实现的。所以和列表相⽐,操作中间元素的速度也很快。时间复杂度尾O(log(N))。Redis数据类型中的有序集合类型⽐Redis数据类型中的列表类型更加耗费资源。
1)增加:ZADD key sorce1 value1 。
2)获取分数:ZSCORE key value。获取key的有序集合中值为value的元素的分数。
3)获取排名在某个范围内的元素列表:ZRANFGE key start stop [WITHSCORE]。获取排名在start和end之间的元素列表,包含start和end2个元素。
    每个元素⼀⾏。如果有WITHSCORE参数,则⼀⾏元素值,⼀⾏分数。时间复杂度为O(LOGn+m)。如果分数相同,则0<0<A<Z<a<z。
4)获取指定分数范围的元素:ZRANGEBYSCORE key min max [WITHSCORE] [LIMIT offset count]。
获取分数在min和max之间的元素列表。含两头。
    每个元素⼀⾏。如果有WITHSCORE参数,则⼀⾏元素值,⼀⾏分数。如果min⼤于max则顺序反转。
5)为某个元素增加分数:ZINCRBY key increment value。指定的有序集合的值为value的元素的分数+increment。返回值后更改后的分数。
6)获取集合中元素的数量:ZCARD key。
7)获取指定分数范围内的元素个数:ZCOUNT key min max。
8)删除⼀个或多个元素:ZREM key
9)根据排名范围删除元素:ZREMRANGEBYRANK key start end。删除排名在start和end中的元素。
10)按照分数范围删除元素:ZREMRANGEBYSCORE key min max。
11)获得元素排名(正序):ZRANK key value。获取value在该集合中的从⼩到⼤的排名。
12)获得元素排名(倒序):ZREVRANK key value。获取value在该集合中从⼤到⼩的排名。
13)有序集合的交集:ZINTERSTORE storekey [WEIGHTS weight [weight..]] [AGGREGATE SUM|MIN|MAX]。⽤来计算多个集合的交集,
    结果存储在storekey中。返回值是storekey的元素个数。AGGREGATE为SUM则storekey集合的每个元素的分数是参与计算的集合分数和。MIN是参
    与计算的分数最⼩值。MAX是参与计算分数最⼤值。WEIGHTS 设置每个集合的权重,如WEIGHTS 1 0.1。那么集合A的每个元素分数*1,集合B的每
    个元素分数*0.1
14)有序集合的并集:ZUNIONSTORE storekey [WEIGHTS weight [weight..]] [AGGREGATE SUM|MIN|MAX]
  2、应⽤场景
显⽰最新评论:score就是⼀个有序⽣成的不断增长的序列号,因此可以获取最新多少条评论。
获取Top N系列:有序集合经典使⽤场景。例如视频⽹站需要对⽤户上传的视频做排⾏榜,榜单维护可能是多⽅⾯:按照时间、按照播放量、按照获得的赞数等。
做交集、差集
  3、实现原理
内部是以ziplist或者skiplist+hashtable来实现。
  skiplist,也就是跳跃表,跳跃表是⼀种随机化的数据结构,在查、插⼊和删除这些字典操作上,其效率可⽐拟于平衡⼆叉树(如红⿊树),如下图:
六、特殊名词
  1、压缩列表ziplist
  压缩表(ziplist)是列表键(3.2之前的版本)和哈希键的底层实现之⼀。
  作为哈希键的实现底层条件:当⼀个哈希键只包含少量的键值对,并且每个键值对的键和值要么是⼩整数数值要么就是长度较短的字符串时,Redis就会使⽤压缩列表来作为哈希键的底层实现。
  压缩列表是Redis为了节约内存⽽开发的,由⼀系列特殊编码的连续内存块组成的顺序型数据结构。⼀个压缩列表可以包含任意多个节点(entry),每个节点保存⼀个字节数组或者⼀个整数值。
  源码如下:
//  ziplist的成员宏定义
//  (*((uint32_t*)(zl))) 先对char *类型的zl进⾏强制类型转换成uint32_t *类型,
//  然后在⽤*运算符进⾏取内容运算,此时zl能访问的内存⼤⼩为4个字节。
#define ZIPLIST_BYTES(zl)      (*((uint32_t*)(zl)))
//将zl定位到前4个字节的bytes成员,记录这整个压缩列表的内存字节数
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
//将zl定位到4字节到8字节的offset成员,记录着压缩列表尾节点距离列表的起始地址的偏移字节量
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
//将zl定位到8字节到10字节的length成员,记录着压缩列表的节点数量
#define ZIPLIST_HEADER_SIZE    (sizeof(uint32_t)*2+sizeof(uint16_t))
/
/压缩列表表头(以上三个属性)的⼤⼩10个字节
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)
//返回压缩列表⾸节点的地址
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
//返回压缩列表尾节点的地址
#define ZIPLIST_ENTRY_END(zl)  ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
//返回end成员的地址,⼀个字节。
/* We know a positive increment can only be 1 because entries can only be
* pushed one at a time. */
#define ZIPLIST_INCR_LENGTH(zl,incr) {        //增加节点数
if (ZIPLIST_LENGTH(zl) < UINT16_MAX)      //如果当前节点数⼩于65535,那么给length成员加incr个节点
ZIPLIST_LENGTH(zl) = intrev16ifbe(intrev16ifbe(ZIPLIST_LENGTH(zl))+incr);
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。