在Redis中有5种基本数据类型,分别是String,List,Hash,Set,Zset。除此之外,Redis中还有一些实用性很高的扩展数据类型,下面来介绍一下这些扩展数据类型以及它们的使用场景。
GeoGEO在Redis3.2版本后被添加,可以说是针对LBS(Location-BasedService)产生的一种数据类型,主要用于存储地理位置信息,并可以对存储的信息进行一系列的计算操作,下面来看一下常用的api操作。
geoadd:存储指定的地理空间位置
#语法格式:GEOADDkeylongitudelatitudemember[longitudelatitudemember...]#测试:GEOADDlocations..beijingGEOADDlocations..qingdao
看一下geo数据在Redis中的存储方式,可以看到是以zset格式进行存储的,因此geo是基于zset的一种扩展数据格式。
geopos:返回指定地理位置的经纬度坐标
#语法格式:GEOPOSkeymember[member...]#测试:GEOPOSlocationsbeijingqingdao..32991..
也可以使用zrange返回所有的位置元素而不带经纬度信息
ZRANGElocations0-1qingdaobeijing
geodist:计算指定位置间的距离,并可以指定返回的距离单位
#语法格式:GEODISTkeymember1member2[m
km
ft
mi]#测试:GEODISTlocationsbeijingqingdaokm.
georadiusbymember:找出以给定位置为中心,返回key包含的元素中,与中心的距离不超过给定最大距离的所有位置元素
#语法格式:GEORADIUSBYMEMBERkeymemberradius[m
km
ft
mi]#测试:GEORADIUSBYMEMBERlocationsbeijingkmbeijing#扩大范围GEORADIUSBYMEMBERlocationsbeijingkmqingdaobeijing
georadius与georadiusbymember类似,但是是以指定的经纬度为中心
#语法格式:GEORADIUSkeylongitudelatituderadius[m
km
ft
mi]#测试:GEORADIUSlocations..kmbeijing
geo并没有提供删除指令,但根据其底层是zset实现,我们可以使用zrem对数据进行删除
ZREMlocationsbeijing
基于geo,可以很简单的存储人或物关联的经纬度信息,并对这些地理信息进行处理,例如基于查询相邻的经纬度范围,能简单实现类似“附近的人”等功能。
BitmapBitmap也被称为位图,是以String类型作为底层数据结构实现的一种统计二值状态的数据类型。其中每一个bit都只能是0或1,所以通常用来表示一个对应于数组下标的数据是否存在。Bitmap提供了一系列api,主要用于对bit位进行读写、计算、统计等操作。
setbit:对key所存储的字符串值,设置或清除指定偏移量上的位(bit)
#语法格式:SETBITkeyoffsetvalue#测试:SETBITkeySETBITkey
getbit:对key所存储的字符串值,获取指定偏移量上的位(bit)
#语法格式:GETBITkeyoffset#测试:GETBITkey
bitcount:可以统计bit数组中指定范围内所有1的个数,如果不指定范围,则获取所有
#语法格式:BITCOUNTkey[startend]#测试:BITCOUNTkey2
bitpos:计算bit数组中指定范围第一个偏移量对应的的值等于targetBit的位置
#语法格式:BITPOSkeytartgetBit[startend]#测试:BITPOSkey
bitop:做多个bit数组的and(交集)、or(并集)、not(非)、xor(异或)。例如对key和key2做交集操作,并将结果保存在key:and:key2中
#语法格式:BITOPopdestKeykey1[key2...]#测试:BITOPandkey:and:key2keykey
Bitmap底层使用String实现,value的值最大能存储M字节,可以表示***8=个位,已经能够满足我们绝大部分的使用场景。再看一下底层存储数据的格式,以刚刚存储的key为例:
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x80
将16进制的数据转化为2进制数据,如下图所示,第位和第位为1,其他为0:
此外,由于Redis在存储string类型的时候存储形式为二进制,所以也可以通过操作bit位来对string类型进行操作,在下面的例子中,通过直接操作bit,将string类型的abc变成了bbc。
setkey2abcsetbitkeysetbitkeygetkey2bbc
另外,可以通过bitfield命令实现类似的效果
setkey3aBITFIELDkey3getuBITFIELDkey3setugetkey3b
使用bitfield命令可以返回指定位域的bit值,并将它转化为整形,有符号整型需在位数前加i,无符号在位数前加u。上面我们将8位转化为无符号整形,正好是a的ASCII码,再对ASCII码进行修改,可以直接改变字符串的值。
Bitmap的应用非常广泛,例如在缓存三大问题中我们介绍过使用Bitmap作为布隆过滤器应对缓存穿透的问题,此外布隆过滤器也被广泛用于邮件系统中拦截垃圾邮件的地址。另外,常用的用户签到、朋友圈点赞等功能也可以用它来实现。
以实现用户签到功能为例,可以将每个用户按月存储为一条数据,key的格式可以定义为sign:userId:yyyyMM,如果签到了就将对应的位置改为1,未签到为0,这样最多只需要31个bit位就可以存储一个月的数据,转换为字节的话也只要4个字节就已经足够。
#1月10日签到,因为offset从0起始,所以将天数减1SETBITsign::#查看1月10日是否签到GETBITsign::#统计签到天数BITCOUNTsign::#查看首次签到的日期BITPOSsign::9#提取整月的签到数据BITFIELDsign::getu
注意在使用bitfield指令时,有符号整型最大支持64位,而无符号整型最大支持63位。如果位数超过限制,会报如下错误:
bitfieldkey3getuERRInvalidbitfieldtype.Usesomethinglikei16u8.Notethatu64isnotsupportedbuti64is.
所以在存储签到数据时,如果按月存储的话在之后提取数据时会比较方便,如果按年存储数据,在提取整年的签到数据时可能需要进行分段。
HyperLogLogRedis在2.8.9版本添加了HyperLogLog结构,它是一种用于基数统计的数据集合类型。它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还非常小。
pfadd:向HyperLogLog中添加数据
#语法格式:PFADDkeyelement[element...]#测试:PFADDindex.htmluuid1uuid2uuid3uuid4
pfcount:返回HyperLogLog的基数统计结果
#语法格式:PFCOUNTkey[key...]#测试:PFCOUNTindex.html4
pfmerge:将多个HyperLogLog合并为一个,合并后的HyperLogLog的基数估算值是通过对所有给定HyperLogLog进行并集计算得出的。
#语法格式:PFMERGEdestkeysourcekey[sourcekey...]#测试:PFMERGEindex.htmlhome.htmlOKPFCOUNTindex.html6
例如在上面的例子中,使用HyperLogLog可以很方便的统计网页的UV。在官方文档中指明,Redis中每个HyperLogLog只需要花费12KB内存,就可以对2^64个数据完成基数统计。尽管使用Set或Hash等结构也能实现基数统计,但这些数据结构都会消耗大量的内存。而使用HyperLogLog时,和其他数据结构计算基数时,元素越多耗费内存就越多形成了鲜明对比。
需要注意的是,HyperLogLog是一种算法,并非是Redis独有的,并且HyperLogLog的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,官方给出的标准误算率是0.81%。HyperLogLog只会根据输入元素来计算基数,而不会存储输入的元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
针对以上这些特性,可以总结出,HyperLogLog适用于大数据量的基数统计,但是它也存在局限性,它只能够实现统计基数的数量,但无法知道具体的原数据是什么。如果需要原数据的话,我们可以将Bitmap和HyperLogLog配合使用,例如在统计网站UV时,使用Bitmap标识哪些用户属于活跃用户,使用HyperLogLog实现基数统计。
StreamStream是Redis5.0版本之后新增加的数据结构,实现了消息队列的功能,并且实现消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,保证消息不丢失,下面来看一下具体的指令。
xadd:向队列添加消息
#语法格式:XADDkeyIDfieldvalue[fieldvalue...]#测试:XADDstream1*phonenameHydra"5-0"XADDstream1*key1value1key2value2key3value3"8-0"
添加消息是生成的5-0,是生成消息的id,由时间戳加序号组成,时间戳是Redis的服务器时间,如果在同一个时间戳内,序号会递增来标识不同的消息。并且为了保证消息的有序性,生成的消息id是保持自增的。可以使用可视化工具查看数据,消息是以json格式被存储:
这里因为是不同时间戳,所以序号都是从0开始。我们可以通过redis的事务添加消息进行测试:
MULTI"OK"XADDstream*msg1"QUEUED"XADDstream*msg2"QUEUED"XADDstream*msg3"QUEUED"XADDstream*msg4"QUEUED"XADDstream*msg5"QUEUED"EXEC1)"OK"2)"2-0"3)"OK"4)"2-1"5)"OK"6)"2-2"7)"OK"8)"2-3"9)"OK"10)"2-4"11)"OK"
通过上面的例子,可以看见同一时间戳内,序号会不断递增。
xrange:获取消息列表,会自动过滤删除的消息
#语法格式:XRANGEkeystartend[COUNTcount]#测试:XRANGEstream1-+count51)1)"5-0"2)1)"phone"2)""3)"name"4)"Hydra"2)1)"8-0"2)1)"key1"2)"value1"3)"key2"4)"value2"5)"key3"6)"value3"
xread:以阻塞或非阻塞方式获取消息列表
#语法格式:XREAD[COUNTcount][BLOCKmilliseconds]STREAMSkey[key...]id[id...]#测试:XREADcount1STREAMSstream10-11)1)"stream1"2)1)1)"5-0"2)1)"phone"2)""3)"name"4)"Hydra"
xdel:删除消息
#语法格式:XDELkeyID[ID...]#测试:XDELstream14317444558-0"1"
除了上面消息队列的基本操作外,还可以创建消费者组对消息进行消费。首先使用xgroupcreate创建消费者组:
#语法格式:XGROUP[CREATEkeygroupnameid-or-][SETIDkeygroupnameid-or-][DESTROYkeygroupname][DELCONSUMERkeygroupnameconsumername]#创建一个队列,从头开始消费:XGROUPCREATEstream1consumer-group-10-0#创建一个队列,从尾部开始消费,只接收新消息:XGROUPCREATEstream1consumer-group-2
下面使用消费者组消费消息:
#语法格式XREADGROUPGROUPgroupconsumer[COUNTcount][BLOCKmilliseconds][NOACK]STREAMSkey[key...]ID[ID...]
注意这里消费消息的对象是consumer消费者,而不是消费者组。在消费消息时,不需要预先创建消费者,在消费过程中直接指定就可以。接下来再向stream1中发送一条消息,比较两个消费者组的消费顺序差异:
#重新发送一条消息XADDstream1*newmsghi"1-0"#使用消费者组1消费:XREADGROUPGROUPconsumer-group-1consumer1COUNT1STREAMSstream11)1)"stream1"2)1)1)"5-0"2)1)"phone"2)""3)"name"4)"Hydra"#使用消费者组2消费:XREADGROUPGROUPconsumer-group-2consumer2COUNT1STREAMSstream11)1)"stream1"2)1)1)"1-0"2)1)"newmsg"2)"hi"
可以看到,消费者组1从stream1的头部开始消费,而消费者组2从创建消费者组后的最新消息开始消费。在消费者组2内使用新的消费者再次进行消费:
XREADGROUPGROUPconsumer-group-2consumer4COUNT1STREAMSstream1XADDstream1*newmsg2hi2"2-0"XREADGROUPGROUPconsumer-group-2consumer4COUNT1STREAMSstream11)1)"stream1"2)1)1)"2-0"2)1)"newmsg2"2)"hi2"
在上面的例子中,可以看到在一个消费者组中,存在互斥原则,即一条消息被一个消费者消费过后,其他消费者就不能再消费这条消息了。
xpending:等待列表用于记录读取但并未处理完毕的消息,可以使用它来获取未处理完毕的消息
XPENDINGstream1consumer-group-21)"2"#2条已读取但未处理的消息2)"1-0"#起始消息ID3)"2-0"#结束消息ID4)1)1)"consumer2"#消费者2有1个2)"1"2)1)"consumer4"#消费者4有1个2)"1"
在xpending命令后添加startendcount参数可以获取详细信息:
XPENDINGstream1consumer-group-2-+)1)"1-0"#消息ID2)"consumer2"#消费者3)""#从读取到现在经历的毫秒数4)"1"#消息被读取次数2)1)"2-0"2)"consumer4"3)""4)"1"
xack:告知消息被处理完成,移出pending列表
XACKstream1consumer-group-21-0"1"
再次查看pending列表,可以看到1-0已被移除:
XPENDINGstream1consumer-group-21)"1"2)"2-0"3)"2-0"4)1)1)"consumer4"2)"1"
基于以上功能,如果我们的系统中已经使用了redis,甚至可以移除掉不需要的其他消息队列中间件,来达到精简应用系统的目的。并且,RedisStream提供了消息的持久化和主从复制,能够很好的保证消息的可靠性。