当前位置: 首页 > 科技观察

Redis存储总是使用String吗?你可能错过了更好的使用方法

时间:2023-03-18 01:40:15 科技观察

Redis为我们提供了5种数据类型。基本上使用频率最高的数据类型是String,其他四种数据类型的使用频率都略弱于String。原因是:String使用起来比较简单,可以方便的存储复杂的对象,使用场景多;由于Redis的过期时间只能设置在key上,比如List、Hash、Set、Zset都属于集合类型,管理的是一组item。我们无法为这些集合的项设置过期时间,因此使用expiretime来处理集合缓存失效会稍微复杂一些。但是String使用expire时间来管理过期策略更简单,因为它包含的项更少。这里提到的集合是广泛的相似集合。在更深层次上,我们对其他四种数据类型的用法和原理知之甚少。所以这个时候,在特定场景下使用某种数据类型会比String有更高性能的可能性往往被忽略了,比如使用Hash结构来改进对实体某一项的修改。这里不打算一一列举这5种数据类型的使用方法,因为网上有很多这样的资料。我们主要讨论这五种数据类型的功能特点,弄清楚它们适合处理哪些现实的业务场景,以及我们如何结合使用这五种数据类型来找到解决复杂缓存问题的最佳方案。一、Redis的数据类型及特点下面简单了解一下String、List、Hash、Set和Zset:1)StringString是Redis提供的字符串类型。String类型可以独立设置expire时间,通常用于存储长字符串数据,比如对象的json字符串。在使用上,String类型最巧妙的地方在于它可以动态拼接key。通常,我们可以将一组id放在Set中,然后动态检查String是否还存在。如果不存在,说明已经过期或者因为数据修改被主动删除,需要重新做一次缓存数据加载。虽然Set不能设置item的过期时间,但是我们可以将SetItem和StringKey关联起来,达到同样的效果。下图左侧是一个Set集合,其key为Set:order:ids。可能是一个全集合,也可能是某个查询条件得到的集合:有时候复杂的场景需要多个Set集合来支持计算,Redis服务器中可能有很多这样的集合。这些集合可以称为功能数据。这些数据用于辅助缓存计算。在执行各种集合操作后,就会得到当前查询需要返回的子集。只有这样,我们才能获得订单的真实数据。数据。这些String:order:{orderId}字符串键不一定服务于一个场景,而是整个系统最顶层的数据,需要在各种场景中获取。那些Set集合可以认为是查询条件数据,用来辅助查询条件的计算。Redis为我们提供了TYPE命令来查看某个key的数据类型,比如String类型:SETstring:order:100order-100TYPEstring:order:100string2)ListList由于其独特的LPUSH、RPUSH、LPOP和RPOP函数可以无缝支持生产者和消费者架构模型。这非常适合实现类似于JavaConcurrencyFork/Join框架的工作窃取算法(workstealing)。注意:JavaFork/Join框架使用并行来提高性能,但是会带来并发take任务带来的竞争条件(racecondition)问题,所以采用work-stealing算法来解决竞争问题带来的性能损失。下图模拟了一个典型的支付回调高峰场景:在高峰出现的地方,我们一般会采用加缓冲区的方式来加快请求处理速度,从而提高并发处理能力,提高吞吐量。支付网关收到回调后,不做任何处理,直接下发给分销商。分发器是一个无状态集群。各节点将handler队列列表拉取到注册中心,即获取下游processor注册到注册中心的消息通道。每个分发节点都会维护一个本地队列列表,然后依次向这些队列列表推送消息。这里会有一个小问题,就是支付网关调用分发器的时候,负载均衡怎么做?如果不是平均负载,可能有某个队列列表高于其他队列列表。Distributor不需要做软负载均衡,因为即使某个queuelist比其他queuelist多也没关系,因为下游的messagehandler会根据work-stealing算法窃取其他慢消费queuelist。RedisList的LPUSH、RPUSH、LPOP、RPOP特性在很多场景下确实可以提升这种横向扩展的计算能力。3)HashHash数据类型显然是基于Hash算法的。搜索项目的时间复杂度是O(1)。在极端情况下,可能会出现itemHash冲突。Redis内部使用链表和key判断来解决。Redis具体的内部数据结构我们后面会介绍,这里就不展开了。Hash数据类型的特点通常可以用来解决映射关系,同时需要更新或删除某些项。如果不是需要维护的item,一般可以使用String来解决。如果需要修改某个字段,使用String显然会产生很大的开销。需要读取并反序列化成一个对象然后操作,再序列化写回Redis。中间可能会出现并发问题。那么我们就可以使用RedisHash提供的实体属性Hash存储特性。我们可以把HashValue看成一个HashTable,实体的每一个属性都是通过Hash得到的该属性的最终数据索引。下图使用Hash数据类型记录了页面的a/bmetrics:左边是首页索引各个区域的统计,右边是营销各个区域的统计。在程序中,我们可以很方便的利用Redis的原子特性,将一个item累加到Hash中。HMSEThash:mall:page:ab:metrics:indextopbanner10leftbanner5rightbanner8bottombanner20productmore10topshopping8OKHGETALLhash:mall:page:ab:metrics:index1)"topbanner"2)"10"3)"leftbanner"4)"5"5)"rightbanner"6)"8"7)"bottombanner"8)"20"9)"productmore"10)"10"11)"topshopping"12)"8"HINCRBYhash:mall:page:ab:metrics:indextopbanner1(integer)11使用RedisHashIncrement执行原子增加操作。HINCRBY命令可以原子地增加任何给定的整数,HINCRBYFLOAT也可以原子地增加浮点型数据。4)SetSet集合数据类型可以支持集合操作,不能存储重复数据。Set最大的特点是集合的计算能力,interintersection,unionunion,diffdiff。这些特性可用于高性能的交叉计算或数据剔除。Set集合在使用场景上还是越来越自由。举个简单的例子,商品和事件场景在应用系统中比较常见。一个Set用来缓存有效的商品集合,一个Set用来缓存活跃的商品集合。如果产品有下架操作,只需要维护有效的产品集。每次拿到活跃的商品,都需要过滤是否有下架商品。如果有,您需要将它们从活动产品中删除。当然,下架时可以直接删除缓存的活跃商品,但是活跃商品是从营销系统加载的。货架商品。当然,这只是一个例子,一个场景有不同的实现方式。下图左右两边是两个不同的集合:左边是营销域可用产品id的集合,右边是营销域活跃产品id的集合,两组的交集是计算在中间。SADDset:营销:产品:可用:ids100010010001201000130100014010001501000160SMEMBERSset:营销:产品:可用:ids1)“1000100”2)“1000120”3)“1000130”4)“1000140”5)“1000”:01)设置“1DD0”6活动:产品:ids100010010001201000130100014010002001000300SMEMBERSset:营销:活动:产品:ids1)“1000100”2)“1000120”3)“1000130”4)“1000140”5)“1000200”6):“1000100”2)“1000120”3)“1000130”4)“1000140”5)“1000200”6):“1000200”6):“10TERS0030”营销:设置idsset:marketing:activity:product:ids1)"1000100"2)"1000120"3)"1000130"4)"1000140"在一些复杂的场景下,也可以使用SINTERSTORE命令将交集计算结果存入一个目标集。这在使用管道命令管道时特别有用,将SINTERSTORE命令包装在管道命令字符串中可以重用计算的结果集。由于Redis是Signle-Thread单线程模型,基于这个特性,我们可以使用Redis提供的pipeline管道来提交一系列逻辑命令集。这些命令在处理过程中不会受到其他客户端命令的干扰。5)ZsetZset排序集合类似于Set集合,但Zset提供了排序功能。在介绍Set集合的时候,我们知道Set集合中的成员是无序的,Zset填补了集合可以排序的空白。Zset最大的功能就是可以按照一定的分数进行排序,这在很多业务场景中是非常迫切需要的。例如,在促销活动中,根据产品的销售量对产品进行排序,在旅游景点中,根据流入的人数对热门景点进行排序。基本上人们所做的任何事情都需要按照一些标准进行分类。其实在我们的应用系统中,到处都可以使用Zset。这里我们举一个简单的例子。在团购系统中,我们通常需要根据参与人数对团购名单进行排序。每个人都希望加入这个即将成立的团体。下图是一个根据团购code创建的Zset,score分值就是参团人数累加和:ZADDzset:marketing:groupon:group:codes5G_PXYJY9QQFA8G_4EXMT6NZJQ20G_W7BMF5QC2P10G_429DHBTGZX8G_KHZGH9U4PPZREVRANGEBYSCOREzset:marketing:groupon:group:codes100001)"G_W7BMF5QC2P"2)"G_ZMZ69HJUCB"3)"G_429DHBTGZX"4)"G_KHZGH9U4PP"5)"G_4EXMT6NZJQ"6)"G_PXYJY9QQFA"ZREVRANGEBYSCOREzset:marketing:groupon:group:codes10000withscores1)"G_W7BMF5QC2P"2)"20"3)"G_ZMZ69HJUCB"2)"G_ZMZ69HJUCB"2)"GX4Z"5)"106)"10"7)"G_KHZGH9U4PP"8)"8"9)"G_4EXMT6NZJQ"10)"8"11)"G_PXYJY9QQFA"12)"5"Zset本身提供了很多集合排序的方法,如果需要打分打分,您可以使用withscore子句来显示每个项目的分数。在某些特殊场合,可能需要联合分拣。可能有多个Zset用来对不同维度的同一个entity进行排序,比如按时间排序,按人数排序等,这时候可以结合使用Zset带来的便利,可以结合pipeline与多个Zsets最终得到一个组合排序集。2.案例:沪江团购系统推广热顶接口缓存设计以沪江团购系统热顶接口缓存设计为例,总结了所提供的五种数据类型各自的特点和一般使用场景通过Redis。但是我们不能仅仅单独使用这些数据类型,我们可以综合使用这些数据类型来完成复杂的缓存场景。下面我们分享一个使用多个Zsets和Strings来优化团购系统前端界面的例子。限于篇幅和时间,这里只介绍与本案相关的信息。注:热顶界面是指热点和排名界面,也就是说它的浏览量和并发量都比较高。一般在大促的时候会有几个对性能要求比较高的接口。我们先来分析一下查询界面包含的一般信息。首先一个查询界面必须有querycondition查询条件,然后是sort排序信息,最后是page分页信息。这是通用接口的基本职责。当然,在特殊场景下,还需要支持master/slave复制的数据会话一致性需求,需要提供tracking标签从master来回查询数据,这里不再展开。我们可以抽象出这几个维度的信息:querycondition:查询条件,companyid=100,sellerid=1010101等等。sort:排序信息,一般是默认的列排序,但是在复杂场景下,接口用户可以自定义排序字段,比如一些租户信息列。page:分页信息,简单理解为数据记录排序后的行数。由于这里纯粹使用Redis来提高缓存能力,不涉及任何搜索能力,所以这里忽略其他复杂的查询。事实上,我们在复杂的地方使用Elastcsearch来提高搜索能力。上面我们已经分析总结了一个查询接口的基本信息。这里还有一个高并发接口的设计原则,就是将热顶接口和普通搜索接口分开,因为只有分而治之才能根据不同的特点选择不同的技术。.如果我们不分职责,将所有的查询场景都封装在一个接口中,那么后期优化接口性能会很麻烦。有些场景不能或者很难用缓存解决,因为接口加上各种Scene逻辑,即使能勉强达到性能也不高。前面做这些准备的目的是为了在介绍案例的时候达成基本的共识。下面我们来看一下这个团购系统热顶界面的具体逻辑。注:大促期间需展示团购清单。这个接口的流量非常大。团购活动需要按照参与人数倒序排序,分页返回指定人数的团名单。假设此接口名为getTopGroups(getTopGroupsRequestrequest)。1)仔细分析查询条件问题。首先,不同的查询条件从DB中查询到的数据是不同的。也就是说查询的组列表是不一样的,可能有公司和渠道。和其他过滤条件。由于一个团购活动下的团不会太多,最多一百个为限,所以一个查询条件的团列表最多可以有几十个,根据场景分析,热查询条件不会超过十个,所以我们选择将查询条件哈希成一个代码来缓存查询条件的全组列表集,但是这些结果集没有排序。2)排序问题看到按照参加人数排序的问题,我们马上就能想到用Zset来处理分组排序问题,因为排序只有一个维度,所以一个Zset就够了。我们使用一个Zset来缓存所有组的参与者数量,这是一个完整的组排序集。那么我们如何根据参与人数对用户查询条件得到的群列表进行排序呢?直接用Zset的求交运算直接计算出当前集合的Zset子集即可。3)页面分页问题在排序好的组列表Zset上使用Zrange得到分页集。我们来看看完整的流程,查询、排序、分页是如何处理的。下图根据查询条件计算HashCode,然后通过DB查询当前条件全群列表:zset:marketing:groupon:hottop:available:groupkey表示全群参与人数,使用一个Zset缓存。然后计算这两个Zset的交集,就可以得到当前查询需要的参与人数的Zset,最后用Zrevrange得到寻呼间隔。ZADDzset:marketing:groupon:hottop:condition:29860800G4ZD5732YZQ0G5VW3YF42UC0GF773FEJ7CC0GFW8DUEND8S0GKPKKW8XEY90GL324DGWMZM(integer)6ZADDzset:marketing:groupon:hottop:available:group5GN7KQH36ZWK10GS7VB22AWD415GF773FEJ7CC17G5VW3YF42UC18G4ZD5732YZQ32GTYJKCEJBRR40GKPKKW8XEY945GL324DGWMZM50GFW8DUEND8S60GYTKY4ACWLT(integer)10ZINTERSTOREzset:marketing:groupon:hottop:condition:interstore2zset:marketing:groupon:hottop:condition:2986080zset:营销:groupon:hottop:可用:组(整数)6ZRANGEzset:营销:groupon:hottop:条件:interstore0-1withscores1)“GF773FEJ7CC”2)“15”3)“G5VW3YF42UC”4)“17”5)“G4ZD5732YZQ”6)“18”7)“GKPKKW8XEY9”8)“40”9)“GL324DGWMZM”10)“45”11)“GFW8DUEND8S”12)“50”ZREVRANGEzset:营销:groupon:hottop:条件:interstore24withscores1)“GKPKKW8XEY9”2)"40"3)"G4ZD5732YZQ"4)"18"5)"G5VW3YF42UC"6)"17"设置好返回的群代码后,可以使用mget批量获取String类型的群详情信息,这里不再贴出代码,限于篇幅和时间,就不贴出来了expandtoomuch业务场景介绍,这里面还涉及到缓存过期时间的计算,这也涉及到促销活动的运行规则,还涉及到可能的查询条件hash冲突等,但是这些都与本节我们的话题,下一期我们将重点介绍Redis的内存数据结构和编码,了解Redis是如何支持这五种数据类型的,欢迎大家留言讨论。