分享的是《深入理解JVM》,这篇给大家分享《Redis五种数据结构以及三种高级数据结构解析》。
前言
在Redis最重要最基础就属它丰富的数据结构了,Redis之所以能脱颖而出很大原因是他数据结构丰富,可以支持多种场景。并且Redis的数据结构实现以及应用场景在面试中是相当常见的,接下来就和大家聊聊Redis的数据结构。
Redis数据结构有:string、list、hash、set、sortedset这五个是大家都知道的,但Redis还有更高级得数据结构,比如:HyperLogLog、Geo、BloomFilter这几个数据结构,接下来聊聊Redis的这些数据结构吧。
String
基本概念:String是Redis最简单最常用的数据结构,也是Memcached唯一的数据结构。在平时的开发中,String可以说是使用最频繁的了。底层实现:
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于39字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。使用:
redis_cli#启动redis-cli客户端
sethelloworld#将键hello的值设置为world
OK#set命令成功后会返回OK
gethello#通过get命令获取键为hello的值
world#获得到的值
delhello#删除键为hello的值
(integer)1#返回的是删除的数量
mseta10b20c30#批量的设置值
OK
mgetabc#批量的返回值
1)10
2)20
3)30
existshello#是否存在该键
(integer)1#1表示存在,0表示不存在
expirehello10#给hello设置过期时间,单位,秒
(integer)1#返回1代表成功,0代表key不存在或无法设置过期时间
pexpirehello10#给hello设置过期时间,单位,毫秒
(integer)1#返回1代表成功,0代表key不存在或无法设置过期时间
接下来会重点讲一下setkeyvalue[EXseconds][PXmilliseconds][NX
XX]这个一系列命令,这块还是挺重要的,也很容易混淆。reids每次对以前的值覆盖时,会清空TLL值。(TTL是过期时间)
EXsecond:设置键的过期时间为second秒。SETkeyvalueEXsecond效果等同于SETEXkeysecondvalue。PXmillisecond:设置键的过期时间为millisecond毫秒。SETkeyvaluePXmillisecond效果等同于PSETEXkeymillisecondvalue。NX:只在键不存在时,才对键进行设置操作。SETkeyvalueNX效果等同于SETNXkeyvalue。XX:只在键已经存在时,才对键进行设置操作。#使用EX选项
setkey1helloEX#设置过期时间s
OK
ttlhello#获取hello的过期时间
(integer)
#使用PX选项
setkey1helloPX#设置过期时间ms
OK
ttlhello#获取hello的过期时间
(integer)
#使用NX选项
sethelloworldNX
OK#键不存在,设置成功
gethello
value
sethelloworldNX
(nil)#键已经存在,设置失败
gethello
world#维持原值不变
#使用XX选项
existshello#先确定hello不存在
(integer)0
sethelloworldXX
(nil)#因为键不存在,设置失败
sethellowolrd#先给hello设置一个值
OK
sethellonewWolrdXX
OK#这回设置成功了
gethello
newWorld
#NX或XX可以和EX或者PX组合使用
sethelloworldEXNX
OK
gethello
world
ttlhello
(integer)
sethellowolrdPXNX
OK
pttlhello
(integer)#实际操作中这个值肯定小于,这次是为了效果才这么写的
#EX和PX可以同时出现,但后面给出的选项会覆盖前面给出的选项
sethellowolrdEXPX
OK
ttlhello
(integer)30#这个是PX设置的参数,
pttlhello
(integer)
setnumber1
OK
incrnumber#对number做自增操作
(integer)2
在开发过程中,用redis来实现锁是很常用的操作。结合NX以及EX来实现。
sethelloworldNXEX10#成功加锁,过期时间是10s
OK
sethellowolrdNXEX10#在10s内执行这个命令返回错误,因为上一次的锁还没有释放
(nil)
delhello#释放了锁
OK
sethelloworldNXEX10#成功加锁,过期时间是10s
OK
setnxhelloworld#也可以这么写
setexhello10wolrd
锁可以通过设置过期时间以及手动del删除来释放锁。string的命令比较常用就多介绍了点,下面的命令我就挑重点介绍了。
应用场景:
缓存功能:string最常用的就是缓存功能,会将一些更新不频繁但是查询频繁的数据缓存起来,以此来减轻DB的压力。计数器:可以用来计数,通过incr操作,如统计网站的访问量、文章访问量等。-List
基本概念:list是有序可重复列表,和Java的List蛮像的,查询速度快,可以通过索引查询;插入删除速度慢。
底层实现:
列表对象的编码可以是ziplist或者linkedlist。列表对象保存的所有字符串元素的长度都小于64字节并且保存的元素数量小于个,使用ziplist编码;否则使用linkedlist;使用:
lpushmylista#从左边插入数据
(ineteger)1
lpushmylistb
(integer)1
rpushmylistc#从右边插入数据
(integer)1
lrangemylist0-1#检索数据,lrange需要两个索引,左闭右闭;0就是从第0个,-1是倒数第一个,-2倒数第二个...以此类推
1)b
2)a
3)c
lrangemylist0-2#0到倒数第2个
1)b
2)a
lpushmylistabc#批量插入
(integer)3
lpopmylist#从左侧弹出元素
b
rpopmylist#从右侧弹出元素
c
rpopmylist#当列表中没有元素时返回null
(nil)
brpoopmylist5#从右侧弹出元素,如果列表没有元素,会阻塞住,如果5s后还是没有元素则返回
1)mylist#列表名
2)b#弹出元素
delmylist#删除列表
(integer)1
使用场景:
消息队列:Redis的list是有序的列表结构,可以实现阻塞队列,使用左进右出的方式。Lpush用来生产从左侧插入数据,Brpop用来消费,用来从右侧阻塞的消费数据。数据的分页展示:lrange命令需要两个索引来获取数据,这个就可以用来实现分页,可以在代码中计算两个索引值,然后来redis中取数据。可以用来实现粉丝列表以及最新消息排行等功能。-Hash
简介:Redis散列可以存储多个键值对之间的映射。和字符串一样,散列存储的值既可以是字符串又可以是数值,并且用户同样可以对散列存储的数字值执行自增或自减操作。这个和Java的HashMap很像,每个HashMap有自己的名字,同时可以存储多个k/v对。底层实现:
哈希对象的编码可以是ziplist或者hashtable。哈希对象保存的所有键值对的键和值的字符串长度都小于64字节并且保存的键值对数量小于个,使用ziplist编码;否则使用hashtable;使用:
hsetstudentname张三#可以理解为忘名叫student的map中添加kv键值对
(integer)1#返回1代表不存在这个key,并且添加成功
hsetstudentsex男
(integer)1
hsetstudentname张三
(integer)0#返回0因为这个key已经存在
hgetallstudent
1)name
2)张三
3)sex
4)男
hdelstudentname#删除这key
(integer)1#返回1同样代表整个key存在并且删除成功
hdelstudentname
(integer)0#返回0是因为该key已经不存在
应用场景:
Hash更适合存储结构化的数据,比如Java中的对象;其实Java中的对象也可以用string进行存储,只需要将对象序列化成json串就可以,但是如果这个对象的某个属性更新比较频繁的话,那么每次就需要重新将整个对象序列化存储,这样消耗开销比较大。可如果用hash来存储对象的每个属性,那么每次只需要更新要更新的属性就可以。购物车场景:可以以用户的id为key,商品的id为存储的field,商品数量为键值对的value,这样就构成了购物车的三个要素。-Set
基本概念:Redis的set和list都可以存储多个字符串,他们之间的不同之处在于,list是有序可重复,而set是无序不可重复。底层实现:
集合对象的编码可以是intset或者hashtable。集合对象保存的所有元素都是整数值并且保存的元素数量不超过个,使用intset编码;否则使用hashtable;使用:
saddfamilymother#尝试将mother添加进family集合中
(integer)1#返回1表示添加成功,0表示元素已经存在集合中
saddfamilyfather
(integer)1
saddfamilyfather
(intger)0
smembersfamily#获取集合中所有的元素
1)mother
2)father
sismemberfamilyfather#判断father是否在family集合中
(integer)1#1存在;0不存在
sismberfamilyson
(integer)0
sremfamilyson#移除family集合中元素son
(integer)1#1表示存在并且移除成功;0表示存在该元素
sremfamilysom
(integer)0
saddfamily1mother
(integer)1
smembersfamily
1)mother
2)father
smemberfamily1
1)mother
sinterfamilyfamily1#获取family和family1的交集
1)mother
saddfamily1son
(integer)1
sunionfamilyfamily1#获取family和family1的并集
1)mother
2)father
sdifffamilyfamily1#获取family和family1的差集(就是family有但是family1没有的元素)
1)father
应用场景:
标签:可以将博客网站每个人的标签用set集合存储,然后还按每个标签将用户进行归并。存储好友/粉丝:set具有去重功能;还可以利用set并集功能得到共同好友之类的功能。-SortedSet
基本概念:有序集合和散列一样,都用于存储键值对:其中有序集合的每个键称为成员(member),都是独一无二的,而有序集合的每个值称为分值(score),都必须是浮点数。可以根据分数进行排序,有序集合是Redis里面唯一既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序来访问元素的结构。和Redis的其他结构一样,用户可以对有序集合执行添加、移除和获取等操作。底层实现:
有序集合的编码可以是ziplist或者skiplist有序集合保存的元素数量小于个并且保存的所有元素成员的长度都小于64字节。使用ziplist编码;否则使用skiplist;使用:
zaddclassmember1#将member1元素及其score值加入到有序集合class中
(integer)1
zaddclass90membermember3#批量添加
(integer)2
zrangeclass0-1withscores#获取有序集合中的值与score,并按score排序
1)member3
2)80
3)member2
4)90
5)member1
6)
zremclassmember1#删除class中的member1
(integer)1
应用场景:
排行榜:有序集合最常用的场景。如新闻网站对热点新闻排序,比如根据点击量、点赞量等。带权重的消息队列:重要的消息score大一些,普通消息score小一些,可以实现优先级高的任务先执行。HyperLogLog基本概念:Redis在2.8.9版本添加了HyperLogLog结构。RedisHyperLogLog是用来做基数统计的算法,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。使用:这里就拿一个统计网站年5月23日,有多少用户登录举例
pfadduser_login_0523tom#user_login_0523是key;tom是登录的用户
(integer)1
pfadduser_login_0523tomjacklilei的用户
(integer)1
pfcountuser_login_0523#获取key对应值的数量,同一个用户多次登录只统计一次
(integer)3
pfadduser_login_0522sira
(integer)1
pfcountuser_login_0523user_login_0522#统计22号和23号一共有多少登陆的用户
(integer)4
pfmergeuser_login_0522_23user_login_0522user_login_0523#将连个键内容合并
OK
pfcountuser_login_0522_23
(integer)4
应用场景:
可以用来统计网站的登陆人数以及其他指标GEO
基本概念:在Redis3.2版本中新增了一种叫geo的数据结构,它主要用来存储地理位置信息,并对存储的信息进行操作。使用:geoadd用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的key中。
GEOADDbeijing..935蘑菇睡不着
(integer)2
geopos用于从给定的key里返回所有指定名称(member)的位置(经度和纬度),不存在的返回nil。
GEOPOSbeijing蘑菇睡不着故宫
1)1).
2)39.935
2)(nil)
geodist用于返回两个给定位置之间的距离。
单位参数:
m:米,默认单位。
km:千米。
mi:英里。
ft:英尺。
GEOADDbeijing..故宫
(integer)1
GEODISTbeijing蘑菇睡不着故宫km
0.
应用场景:用于存储地理信息以及对地理信息作操作的场景。**科普一个地理小知识:经度范围:--。从0°经线算起,向东、向西各分作°,以东的°属于东经,习惯上用“E”作代号,以西的°属于西经,习惯上用“W”作代号。0°位置是:英国格林威治(Greenwich)天文台子午仪中心的经线为本初子午线。纬度范围:-90-90。位于赤道以北的点的纬度叫北纬,记为N;位于赤道以南的点的纬度称南纬,记为S。为了研究问题方便,人们把纬度分为低、中、高纬度。0°~30°为低纬度,30°~60°为中纬度,60~90°为高纬度。**
BloomFilter
基本概念:一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0。他的主要作用是:判断一个元素是否在某个集合中。比如说,我想判断20亿的号码中是否存在某个号码,如果直接插DB,那么数据量太大时间会很慢;如果将20亿数据放到缓存中,缓存也装不下。这个时候用布隆过滤器最合适了,布隆过滤器的原理是:
添加元素当要向布隆过滤器中添加一个元素key时,我们通过多个hash函数,算出一个值,然后将这个值所在的方格置为1。
判断元素是否存在:判断元素是否存在,是先将元素经过多个hash函数计算,计算到多个下标值,然后判断这些下标对应的元素值是否都为1,如果存在不是1的,那么元素肯定不在集合中;如果都是1,那么元素大概率在集合中,并不能百分之百肯定元素存在集合中,因为多个不同的数据通过hash函数算出来的结果是会有重复的,所以会存在某个位置是别的数据通过hash函数置为的1。总的来说:布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在。布隆过滤器的优缺点:优点:优点很明显,二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。缺点:随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。使用:redis4.0后可以使用布隆过滤器的插件RedisBloom,命令如下:
bf.add添加元素到布隆过滤器
bf.exists判断元素是否在布隆过滤器
bf.madd添加多个元素到布隆过滤器,bf.add只能添加一个
bf.mexists判断多个元素是否在布隆过滤器
bf.addboomFiltertc01
(integer)1#1:存在;0:不存在
bf.addboomFiltertc02
(integer)1
bf.addboomFiltertc03
(integer)1
bf.existsboomFiltertc01
(integer)1
bf.existsboomFiltertc02
(integer)1
bf.existsboomFiltertc03
(integer)1
bf.existsboomFiltertc04
(integer)0
bf.maddboomFiltertc05tc06tc07
1)(integer)1
2)(integer)1
3)(integer)1
bf.mexistsboomFiltertc05tc06tc07tc08
1)(integer)1
2)(integer)1
3)(integer)1
4)(integer)0
Redisson使用布隆过滤器:publicstaticvoidmain(String[]args){
Configconfig=newConfig();
config.useSingleServer().setAddress(redis://..15.:);
config.useSingleServer().setPassword(password);
//构造Redisson
RedissonClientredisson=Redisson.create(config);
RBloomFilterStringbloomFilter=redisson.getBloomFilter(userPhones);
//初始化布隆过滤器:预计元素为L,误差率为3%
bloomFilter.tryInit(L,0.03);
//将号码86插入到布隆过滤器中
bloomFilter.add();
//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains());//true
System.out.println(bloomFilter.contains());//false
}
Guava使用布隆过滤器:Guava是谷歌提供的Java工具包,功能非常强大
publicstaticvoidmain(String[]args){
BloomFilterStringbloomFilter=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),,0.01);
bloomFilter.put();
System.out.println(bloomFilter.mightContain());//true
System.out.println(bloomFilter.mightContain());//false
}
}
应用场景:
解决缓存穿透问题:一般得查询场景都是先去查询缓存,如果缓存没有,那么就去DB查询,如果查到了,先存在缓存中,然后返回给调用方。如果查不到就返回空。这种情况如果有人频繁的请求缓存中没有得数据,比如id=-1得数据,那么会对DB造成极大得压力,这种情况就可以使用redis得布隆过滤器了,可以先将可能得id都存在布隆过滤器中,当查询来的时候,先去布隆过滤器查,如果查不到直接返回,不请求缓存以及DB,如果存在布隆过滤器中,那么才去缓存中取数据。黑名单校验:可以将黑名单中得ip放入到布隆过滤器中,这样不用每次来都去db中查询了。总结
Redis丰富的数据结构是支撑Redis重要基石之一。他使Redis可以适应很多复杂的场景。这块的内容在面试中可以说是必考的内容,所以要在这方面要多下些功夫。
以上就是《Redis五种数据结构以及三种高级数据结构解析》的分享。也欢迎大家交流探讨,该文章若有不正确的地方,希望大家多多包涵。创作不易,你们的支持就是我最大的动力,如果对大家有帮忙给个赞哦~~~