Redis是典型的key-value类型库,value有八种类型,分别是String(字符串)、List(列表)、hash(哈希结构)、set(集合)、sortedset(有序集合,也叫zset)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息定位)。
一、String(字符串)1、简单介绍字符串类型是Redis最基础的数据结构,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML)),数字(整数、浮点数),甚至是二进制(图片、音频、视频)。
String类型
2、应用场景(1)、缓存功能MySQL操作硬盘,速度很慢,Redis操作内存,速度很快。所以在高并发场景下,一般会在MySQL前面加上Redis作为缓存,起到加速读写和降低MySQL库压力的作用。
String类型用作缓存
UserInfogetUserInfo(StringuserId){StringuserRedisKey="user:info"+userId;UserInfouserInfo=redis.get(userRedisKey);if(userInfo!=null){returnuserInfo;}else{userInfo=mysql.get(userId);if(userInfo!=null){redis.setex(userRedisKey,,userInfo);}}}
Redis键的设计:与MySQL等关系型数据库不同的是,Redis没有命令空间,而且对键名也没有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突,提高项目的可维护性。推荐使用“业务名:对象名:id:[属性]”作为键名,例如MySQL的数据库名为vs,用户表名为user,那么对应的键可以用"vs:user:1","vs:user:1:name"来表示。
(2)、计数由String的incr自增命令,许多应用都会使用Redis作为计数的基础工具。比如点赞系统,incrkey表示点赞,decrkey表示取消点赞。
redisINCRuser:userId:like//点赞redisDECRuser:userId:like//取消点赞
(3)、限速限制网站或者APP在某段时间的访问次数,比如我们登录某个网站需要用手机获取验证码,但是我们发送验证码使用的是第三方系统,是要收费的,肯定不能让用户一直点,一直发短信。虽然前端JS可以做校验,但是如果有人用fiddler拦截绕过前台,那就麻烦啦,所以为了安全保证,后端还要再加一层拦截,这时候可以用redis的incr命令和expire结合起来做一个解决方案,控制1分钟内最多发送5次短信。
手机验证码示意图
StringphoneNum="xxxxxxxx";Stringkey="shortMsg:limit:"+phoneNum;//Redis的key//先判断Redis中是否有该key值if(redis.exists(key)){intnum=redis.get(key);//取出发送次数,Redis的value//如果发送次数大于等于最大次数if(num=5){return;//限速}//若不大于,则通过,发送手机验证码并将访问次数加1sundMsg();//通过,发送手机验证码redis.incr(key,1L);}else{sundMsg();//通过,发送手机验证码redis.set(key,1,"EX60");//过期时间设置为1分钟}
(4)、分布式系统共享SessionSession作用:一般我们会在用户进行操作的时候用一个拦截器去拦截用户的请求,然后再查看服务器中的Session中有没有该用户的信息,如果有就放行,如果没有就跳转到登录界面提示登录。
一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各个服务器中,这样会造成一个问题,在做Nginx负载均衡的时候,各个用户的请求都会被负载到各自的服务器上,这就会产生很不好的用户体验。
见图1,用户第一次请求被Nginx转发到了服务器A,用户登录之后,操作请求又随机被分发到了服务器B,这时B服务器的Session中没有用户信息,又提示用户需要登录,造成了非常不友好的用户体验。
Session图1
那应该怎么解决呢?见图2,使用Redis将用户的Session信息进行集中的管理,每次用户登录信息都从Redis中获取。
Session图2
3、底层实现(1)、RedisObject首先我们讲解一下Redis的RedisObject的数据结构,如下所示:
typedefstructredisObject{//对外的类型stringlistsethashzset等所占内存大小为4bitunsignedtype:4;//底层存储方式4bitunsignedencoding:4;//LRU时间24bitunsignedlru:LRU_BITS;//引用计数4byteintrefcount;//指向对象(Rediskey-value中的value)的指针8bytevoid*ptr;}robj;
(2)、string的底层实现对于不同的对象,Redis会使用不同的类型type来存储。对于同一种类型会有不同的存储方式encoding。对于string类型(type)的字符串,其底层编码方式(encoding)共有三种,分别为int、embstr和raw。
int:当存储的字符串中全是数字时,此时使用int方式来存储;embstr:当存储的字符串长度小于等于39个字节时,此时使用embstr方式来存储;raw:当存储的字符串长度大于39个字节时,此时使用raw方式来存储;
对于embstr和raw这两种encoding类型,其存储方式还不太一样。对于embstr类型,它将RedisObject对象头和SDS对象在内存中地址是连在一起的,但对于raw类型,二者在内存地址不是连续的。
string底层embstr数据结构string底层raw数据结构(3)、SDS我们知道Redis是用C语言写的,但是它却没有完全直接使用C的字符串(以空字符’\0’结尾的字符数组),而是自己又重新构建了一个叫简单动态字符串SDS(simpledynamicstring)的抽象类型,并将SDS作为Redis的默认字符串表示。
有一点需要注意:在redis数据库中,key-value键值对凡是含有字符串值的,都是由SDS来实现的。比如:在Redis执行一个简单的set命令时,这时Redis会新建一个键值对。
.0.0.1:sethelloworld
此时键值对的key和value都是一个字符串,而字符串的底层实现分别是两个保存着字符串hello和world的SDS结构。
与C语言的原始字符串结构相比,SDS多了一个sdshdr的头部信息,sdshdr基本数据结构如下所示:
structsdshdr{//表示buf[]数组所保存的字符串的长度intlen;//表示buf[]数组未使用的字节的长度intfree;//实际保存字符串的char类型数组charbuf[];}
用SDS保存字符串"hello"具体图示如下:
上图表示的是buf[]保存长度为5个字节的字符串,未使用的字节数free为0,但是我们发现这明明是6个字符,还有一个"\0"啊,为什么len是5呢?
这是因为SDS没有完全直接使用语言的字符串,但还是沿用了一些C语言特性的,,比如遵循C的字符串以空格符结尾的规则。这样做的目的是还可以使用一部分C字符串的函数。
(4)、为什么不用C语言的字符串,而是要使用自己定义的SDS呢,岂不是多此一举?
(a)、SDS效率更高工作中使用redis,经常会通过STRLEN命令获取一个字符串的长度,在SDS结构中len属性记录了字符串的长度,所以我们获取一个字符串长度时直接取len的值,复杂度是O(1)。
而如果用C语言的字符串,在获取一个字符串的长度时,需要对整个字符串进行遍历,直至遍历到空格符结束(C语言中遇到空格符代表一个完整字符串结束),时间复杂度是O(N)。
在高并发场景下,如果需要频繁获取字符串的长度,使用SDS比C语言的字符串效率要高得多。
(b)、SDS可以杜绝数据溢出
见下图,两个C语言字符串s1和s2在内存中相邻存储,s1保存了字符串"Hello",s2保存了字符串"Redis"。
字符串C语言的存储图示1此时我们想把s1由"Hello"改成"Hello",那就会出现一个问题,之前分配给s1的内存只有5个字节,修改后的字符串需要8个字节才能放下,s1空间不够了,它只能侵占相邻字符串s2的空间,就会造成自身数据溢出导致其他字符串的内容被修改的情况,见下图。
字符串C语言的存储图示2
与C语言字符串不同,SDS的空间分配策略完全杜绝了发生数据溢出的可能性:当我们需要修改数据时,首先会检查SDS的空间(len)是否满足修改所需的要求,如果不满足的话,则自动将SDS的空间扩展至修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的数据溢出问题。s1的修改,不会影响到s2,见下图。
字符串SDS的存储图示1字符串SDS的存储图示2见图2,我们可以看到在把"Redis"5个字节扩容到"Redis"8个字节后,发现free属性的值变成了扩容后字符串的总长度,这就是下边要说的内存重分配策略。
(c)、SDS的内存重分配策略,可以减少修改字符串长度时所需的内存重分配次数,相对于C字符串每次修改都要重新分配内存,可以显著提高性能C字符串的长度是一定的,所以我们每次在修改字符串长度时,都要做内存的重分配,内存重分配是一个比较耗时的操作,如果程序不需要经常修改字符串还是可以接受的,但是Redis作为一个数据库,里面的数据肯定会被频繁修改,如果每次修改都要执行一次内存重分配,那么就会严重影响Redis的性能。
SDS通过两种内存重分配策略,解决了字符串在修改时的内存分配问题。
第一种策略:空间预分配空间预分配策略用于优化SDS字符串增长操作,当修改字符串并且需要对SDS的空间进行扩展时,不仅会为SDS分配修改所必要的空间,还会为SDS分配额外的未使用空间free,下次再修改就先检查未使用空间free是否满足,满足则不用在扩展空间。
在扩展SDS空间之前,会先检查未使用空间是否足够,如果足够的话,就会直接使用未使用空间,而无须执行内存重分配。通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。
额外分配的未使用空间的长度由以下公式决定:(1)、如果对SDS进行修改之后,SDS的长度(len属性)小于1MB,那么此时额外分配的未使用空间free的大小与len相等。
举例说明:如果进行修改之后,SDS的len将变成12字节,那么程序也会分配12字节的未使用空间,SDS的buf数组的实际长度将变成12+12=24字节(len:12,free:12)。
初始SDS如下图所示:
SDS空间预分配图示1
执行strcat(s1,"Cluster"),在字符串"Redis"后面拼接上"Cluster",那么将触发一次内存重分配操作,将SDS的长度修改为12字节,并将SDS的未使用空间同样修改为12字节,如下图所示:
SDS空间预分配图示2如果这时我们再次执行strcat(s1,"Tutorial"),那么这次将不需要执行内存重分配,因为未使用空间里面的12字节足以保存8字节的"Tutorial",此时free变成12-8=4,len变成12+8=20。执行完这步操作之后的SDS如下图所示:
SDS空间预分配图示3(2)、如果对SDS进行修改之后,SDS的长度大于等于1MB,那么此时额外分配未使用空间free的大小为1M。
举例说明:如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB。
第二种策略:惰性空间释放惰性空间释放策略则用于优化SDS字符串缩短操作,当缩短SDS字符串后,并不会立即执行内存重分配来回收缩短后多出来的空间,而是用free属性将这些空间记录下来,如果后续有增长操作,则可直接使用。
举例说明:如果有个字符串:aaabbbccc,SDS如下图所示:
SDS惰性空间释放图示1现在要移除ccc,SDS移除后如下图:
SDS惰性空间释放图示2
注意执行字符串缩短操作之后,SDS并没有释放多出来的8字节空间,而是将这8字节空间作为未使用空间保留在了SDS里面(free=8),如果将来要对SDS进行增长操作的话,这些未使用空间就可能会派上用场。
现在想要对上面的字符串后面拼接上"",因为SDS里面预留的3字节空间已经足以拼接3个字节长的""了,所以不需要重新分配内存了。
SDS惰性空间释放图示3二、List(列表)1、简单介绍列表(list)是按照插入顺序排序的字符串列表,可以对列表两端执行插入(push)和弹出(pop)操作,还可以获取指定范围的元素列表、获取指定索引下标的元素等,列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
列表两端插入和弹出操作列表获取、删除操作列表(list)有两个特点,:第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表,见下图要获取第5个元素,可以执行lindexuser:1:message4(索引从0算起)就可以得到元素e。第二、列表中的元素可以是重复的,见下图列表中包含了两个字符a。
列表的两个特点:有序、可重复
2、应用场景(1)、消息队列如下图所示,使用Redis的lpush+brpop命令,可以实现一个阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式地“抢”列表尾部的元素。
Redis消息队列模型(2)、最新内容因为list底层是链表结构,所以查询两端附近的数据性能非常好,适合一些需要获取最新数据的场景,比如新闻类应用的“最近新闻”。
3、底层实现ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
下面的示例演示了列表类型的内部编码,以及相应的变化。
1)当元素个数较少且没有大元素时,内部编码为ziplist;
.0.0.1:rpushlistkeye1e2e3(integer)3.0.0.1:objectencodinglistkey"ziplist"
2)当元素个数超过个,内部编码变为linkedlist:
.0.0.1:rpushlistkeye4e5...忽略...ee(integer).0.0.1:objectencodinglistkey"linkedlist"
3)或者当某个元素超过64字节,内部编码也会变为linkedlist;
.0.0.1:rpushlistkey"onestringisbiggerthan64byte................................"(integer)4.0.0.1:objectencodinglistkey"linkedlist"三、hash(哈希结构)1、简单介绍
hash类型很像一个关系型数据库的数据表,hash的Key是一个唯一值,value部分是一个hashmap的结构。
hash类型
2、应用场景hash类型十分适合存储对象类型数据,相对于使用string存储对象需要把对象转化为json字符串进行存储,hash结构可以任意添加或删除‘字段名’,更加高效灵活。
hmsetuser:1nametomeage26height、底层实现
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时哈希类型中元素个数很多,使用ziplist读写效率会下降,而hashtable的读写时间复杂度为O(1),使用hashtable读写效率会提高。
下面的示例演示了哈希类型的内部编码,以及相应的变化。
1)当field个数比较少且没有大的value时,内部编码为ziplist:
.0.0.1:hmsethashkeyf1v1f2v2OK.0.0.1:objectencodinghashkey"ziplist"
2)当有value大于64字节,内部编码会由ziplist变为hashtable;
.0.0.1:hsethashkeyf3"onestringisbiggerthan64byte...忽略..."OK.0.0.1:objectencodinghashkey"hashtable"
3)当field个数超过,内部编码也会由ziplist变为hashtable;
.0.0.1:hmsethashkeyf1v1f2v2f3v3...忽略...fvOK.0.0.1:objectencodinghashkey"hashtable"四、set(集合)1、简单介绍
set数据类型是一个集合,集合中不允许有重复元素,并且集合中的元素是无序的。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
set类型
2、应用场景(1)、社交网站,好友/