老铁们,大家好,相信还有很多朋友对于yy6080电影网站源码分享和的相关问题不太懂,没关系,今天就由我来为大家分享分享yy6080电影网站源码分享以及的问题,文章篇幅可能偏长,希望可以帮助到大家,下面一起来看看吧!
Redis数据类型特点与使用场景
redis为我们提供了5种数据类型,基本上我们使用频率最高的就是string,而对其他四种数据类型使用的频次稍弱于string。
一方面是由于string使用起来比较简单,可以方便存储复杂大对象,使用场景比较多。还有一个原因就是由于redisexpiretime只能设置在key上,像list、hash、set、zset属于集合类型,会管理一组item,我们无法在这些集合的item上设置过期时间,所以使用expiretime来处理集合的cache失效会变得稍微复杂些。但是string使用expiretime来管理过期策略会比较简单,因为它包含的项少。这里说的集合是宽泛的类似集合。
导致我们习惯性的使用string而忽视其他四种数据类型的另一个深层次原因,大多是由于我们对另外四种数据类型的使用和原理不是太了解。这个时候往往会忽视在特定场景下使用某种数据类型可能会比string性能高出很多,比如使用hash结构来提高某个实体的某个项的修改等。
这里我们不打算罗列这5种数据类型的使用方法,这些资料网上有很多。我们主要讨论这5种数据类型的功能特点,这些特点分别适合用于处理哪些现实的业务场景,最重要的是我们如何组合性的使用这5种数据类型来解决复杂的cache问题。
String、List、Hash、Set、Zset
String
string是redis提供的字符串类型。可以针对string类型独立设置expiretime。通常用来存储长字符串数据,比如,某个对象的json字符串。
string类型我们在使用上最巧妙的是可以动态拼接key。通常我们可以将一组id放在set里,然后动态查找string还是否存在,如果不存在说明已经过期或者由于数据修改主动delete了,需要再做一次cache数据load。
虽然set无法设置item的过期时间,但是我们可以将setitem与stringkey关联来达到相同的效果。
上图中的左边是一个key为set:order:ids的set集合,它可能是一个全量集合,也可能是某个查询条件获取出来的一个集合。
有时候复杂点的场景需要多个set集合来支撑计算,在redis服务器里可能会有很多类似这样的集合。
这些集合我们可以称为功能数据,这些数据是用来辅助cache计算的,当进行各种集合运算之后会得出当前查询需要返回的子集,最后我们才会去获取某个订单真正的数据。
这些string:order:{orderId}字符串key并不一定是为了服务一种场景,而是整个系统最底层的数据,各种场景最后都需要获取这些数据。那些set集合可以认为是查询条件数据,用来辅助查询条件的计算。
redis为我们提供了TYPE命令来查看某个key的数据类型,如:string类型:
SETstring:order:100order-100\nTYPEstring:order:100\nstring\n
List
list在提高throughput的场景中非常适用,因为它特有的LPUSH、RPUSH、LPOP、RPOP功能可以无缝的支持生产者、消费者架构模式。
这非常适合实现类似JavaConcurrencyFork/Join框架中的work-stealing算法(工作窃取)。
javafork/join框架使用并行来提高性能,但是会带来由于并发taketask带来的racecondition(竞态条件)问题,所以采用work-stealing算法来解决由于竞争问题带来的性能损耗。
上图中模拟了一个典型的支付callback峰值场景。在峰值出现的地方一般我们都会使用加buffer的方式来加快请求处理速度,这样才能提高并发处理能力,提高throughput。
支付gateway收到callback之后不做任何处理直接交给分发器。分发器是一个无状态的cluster,每个node通过向注册中心pullhandlerqueuelist,也就是获取下游处理器注册到注册中心里的消息通道。
每一个分发器node会维护一个本地queuelist,然后顺序推送消息到这些queuelist即可。这里会有点小问题,就是支付gateway调用分发器的时候是如何做loadbalance,如果不是平均负载可能会有某个queuelist高出其他queuelist。
而分发器不需要做softloadbalance,因为哪怕某个queuelist比其他queuelist多也无所谓,因为下游messagehandler会根据work-stealing算法来窃取其他消费慢的queuelist。
redislist的LPUSH、RPUSH、LPOP、RPOP特性确实可以在很多场景下提高这种横向扩展计算能力。
Hash
hash数据类型很明显是基于hash算法的,对于项的查找时间复杂度是O(1)的,在极端情况下可能出现项hash冲突问题,redis内部是使用链表加key判断来解决的。具体redis内部的数据结构我们在后面有介绍,这里就不展开了。
hash数据类型的特点通常可以用来解决带有映射关系,同时又需要对某些项进行更新或者删除等操作。如果不是某个项需要维护,那么一般可以通过使用string来解决。
如果有需要对某个字段进行修改,使用string很明显是会多出很多开销,需要读取出来反序列化成对象然后操作,然后再序列化写回redis,这中间可能还有并发问题。
那我们可以使用redishash提供的实体属性hash存储特性,我们可以认为hashvalue是一个hashtable,实体的每一个属性都是通过hash得到属性的最终数据索引。
上图使用hash数据类型来记录页面的a/bmetrics,左边的是首页index的各个区域的统计,右边是营销marketing的各个区域统计。
在程序里我们可以很方便的使用redis的atomic特性对hash某个项进行累加操作。
HMSEThash:mall:page:ab:metrics:indextopbanner10leftbanner5rightbanner8bottombanner20productmore10topshopping8\nOK\nHGETALLhash:mall:page:ab:metrics:index\n1)”topbanner”\n2)”10″\n3)”leftbanner”\n4)”5″\n5)”rightbanner”\n6)”8″\n7)”bottombanner”\n8)”20″\n9)”productmore”\n10)”10″\n11)”topshopping”\n12)”8″\nHINCRBYhash:mall:page:ab:metrics:indextopbanner1\n(integer)11\n
使用redishashincrement进行原子增加操作。HINCRBY命令可以原子增加任何给定的整数,也可以通过HINCRBYFLOAT来原子增加浮点类型数据。
Set
set集合数据类型可以支持集合运算,不能存储重复数据。
set最大的特点就是集合的计算能力,inter交集、union并集、diff差集,这些特点可以用来做高性能的交叉计算或者剔除数据。
set集合在使用场景上还是比较多和自由的。举个简单的例子,在应用系统中比较常见的就是商品、活动类场景。用一个set缓存有效商品集合,再用一个set缓存活动商品集合。如果商品出现上下架操作只需要维护有效商品set,每次获取活动商品的时候需要过滤下是否有下架商品,如果有就需要从活动商品中剔除。
当然,下架的时候可以直接删除缓存的活动商品,但是活动是从marketing系统中load出来的,就算我将cache里的活动商品删除,当下次再从marketing系统中load活动商品时候还是会有下架商品。当然这只是举例,一个场景有不同的实现方法。
上图中左右两边是两个不同的集合,左边是营销域中的可用商品ids集合,右边是营销域中活动商品ids集合,中间计算出两个集合的交集。
SADDset:marketing:product:available:ids100010010001201000130100014010001501000160\nSMEMBERSset:marketing:product:available:ids\n1)”1000100″\n2)”1000120″\n3)”1000130″\n4)”1000140″\n5)”1000150″\n6)”1000160″\nSADDset:marketing:activity:product:ids100010010001201000130100014010002001000300\nSMEMBERSset:marketing:activity:product:ids\n1)”1000100″\n2)”1000120″\n3)”1000130″\n4)”1000140″\n5)”1000200″\n6)”1000300″\nSINTERset:marketing:product:available:idsset:marketing:activity:product:ids\n1)”1000100″\n2)”1000120″\n3)”1000130″\n4)”1000140″\n
在一些复杂的场景中,也可以使用SINTERSTORE命令将交集计算后的结果存储在一个目标集合中。这在使用pipeline命令管道中特别有用,将SINTERSTORE命令包裹在pipeline命令串中可以重复使用计算出来的结果集。
由于redis是Signle-Thread单线程模型,基于这个特性我们就可以使用redis提供的pipeline管道来提交一连串带有逻辑的命令集合,这些命令在处理期间不会被其他客户端的命令干扰。
Zset
zset排序集合与set集合类似,但是zset提供了排序的功能。在介绍set集合的时候我们知道set集合中的成员是无序的,zset填补了集合可以排序的空隙。
zset最强大的功能就是可以根据某个score比分值进行排序,这在很多业务场景中非常急需。比如,在促销活动里根据商品的销售数量来排序商品,在旅游景区里根据流入人数来排序热门景点等。
基本上人们在做任何事情都需要根据某些条件进行排序。
其实zset在我们应用系统中能用到地方到处都是,这里我们举一个简单的例子,在团购系统中我们通常需要根据参团人数来排序成团列表,大家都希望参加那些即将成团的团。
上图是一个根据团购code创建的zset,score分值就是参团人数累加和。
ZADDzset:marketing:groupon:group:codes5G_PXYJY9QQFA8G_4EXMT6NZJQ20G_W7BMF5QC2P10G_429DHBTGZX8G_KHZGH9U4PP\nZREVRANGEBYSCOREzset:marketing:groupon:group:codes10000\n1)”G_W7BMF5QC2P”\n2)”G_ZMZ69HJUCB”\n3)”G_429DHBTGZX”\n4)”G_KHZGH9U4PP”\n5)”G_4EXMT6NZJQ”\n6)”G_PXYJY9QQFA”\nZREVRANGEBYSCOREzset:marketing:groupon:group:codes10000withscores\n1)”G_W7BMF5QC2P”\n2)”20″\n3)”G_ZMZ69HJUCB”\n4)”10″\n5)”G_429DHBTGZX”\n6)”10″\n7)”G_KHZGH9U4PP”\n8)”8″\n9)”G_4EXMT6NZJQ”\n10)”8″\n11)”G_PXYJY9QQFA”\n12)”5″\n
zset本身提供了很多方法用来进行集合的排序,如果需要score分值可以使用withscore字句带出每一项的分值。
在一些比较特殊的场合可能需要组合排序,可能有多个zset分别用来对同一个实体在不同维度的排序,按时间排序、按人数排序等。这个时候就可以组合使用zset带来的便捷性,利用pipeline再结合多个zset最终得出组合排序集合。
案例:沪江团购系统大促hot-top接口cache设计
我们总结了redis提供的5种数据类型的各自特点和一般的使用场景。但是我们不仅仅可以分开使用这些数据类型,我们完全可以综合使用这些数据类型来完成复杂的cache场景。
下面我们分享一个使用多个zset、string来优化团购系统前台接口的例子。由于篇幅和时间限制,这里只介绍跟本次案例相关的信息。
hot-top接口是指热点、排名接口的意思,表示它的浏览量、并发量比较高,一般大促的时候都会有几个这种性能要求比较高的接口。
我们先来分析一个查询接口所包含的常规信息。
首先一个查询接口肯定是有querycondition查询条件,然后是sort排序信息_、最后是page分页信息_。这是一般接口所承担的基本职责,当然,特殊场景下还需要支持master/slavereplication时关于数据session一致性的要求,需要提供跟踪标记来回master查询数据,这里就不展开了。
我们可以抽象出这几个维度的信息:
querycondition
查询条件,companyid=100,sellerid=1010101诸如此类。
sort
排序信息,一般是默认一个列排序,但是在复杂的场景下会有可能让接口使用者定制排序字段,比如一些租户信息列。
page
分页信息,简单理解就是数据记录排完序之后的第几行到第几行。
由于这里我们纯粹用redis来提高cache能力,不涉及到有关于任何搜索的能力,所以这里忽略其他复杂查询的情况。其实我们在复杂的地方使用了elastcsearch来提高搜索能力。
上述我们分析总结出了一个查询接口的基本信息,这里还有一个有关于高并发接口的设计原则就是将hot-top接口和一般search接口分离开,因为只有分而治之才能分别根据特点选用不同的技术。如果我们不分职责将所有的查询场景封装在一个接口里,那么在后面优化接口性能的时候基本就很麻烦了,有些场景是无法或者很难用cache来解决的,因为接口里耦合了各种场景逻辑,就算勉强能实现性能也不会高。
前面做这些铺垫是为了能在介绍案例的时候达成一个基本的共识。现在我们来看下这个团购系统的hot-top接口的具体逻辑。
在大促的时候需要展现团购列表,这个接口的访问量是非常大的,团购活动需要根据参团人数倒序排序,并且分页返回指定数量的团列表。
我们假设这个接口名为getTopGroups(getTopGroupsRequestrequest)
querycondition查询条件问题
我们来仔细分析下,首先不同的查询条件从DB里查询出来的数据是不一样的,也就是说查询出来的团列表是不一样的,可能有company公司、channel渠道等过滤条件。由于一个团购活动下不会有太多团,顶多上百个是极限了,所以一个查询条件出来的团列表也顶多几十个,而且根据场景分析热点查询条件不会超过十个,所以我们选择将查询条件hash出一个code来缓存本次查询条件的全量团列表集合,但是这些结果集是没有任何排序的。
sort排序问题
再看根据参团人数排序问题,我们立刻就可以想到使用zset来处理团排序问题,因为只有一个排序维度,所以一个zset就够了。我们使用一个__zset__来缓存所有团的参团人数集合,它是一个全量的团排序集合。
那么我们如何将用户的查询条件出来的团列表根据参团人数排序尼,刚好可以使用zset的交集运算直接计算出当前这个集合的zset子集。
page分页问题
通过对已经排序之后的团列表zset使用zrange来获取出分页集合。
我们来看下完整的流程,如何处理查询、排序、分页的。
上图从querycondition计算hashcode,然后通过DB查询出当前条件全量团列表。
zset:marketing:groupon:hottop:available:groupkey表示全量团的参团人数,用一个zset来缓存。接着将这两个zset计算交集,就可以得出当前查询所需要的带有参团人数的zset,最后在使用zrevrange获取分页区间。
ZADDzset:marketing:groupon:hottop:condition:29860800G4ZD5732YZQ0G5VW3YF42UC0GF773FEJ7CC0GFW8DUEND8S0GKPKKW8XEY90GL324DGWMZM\n(integer)6\nZADDzset:marketing:groupon:hottop:available:group5GN7KQH36ZWK10GS7VB22AWD415GF773FEJ7CC17G5VW3YF42UC18G4ZD5732YZQ32GTYJKCEJBRR40GKPKKW8XEY945GL324DGWMZM50GFW8DUEND8S60GYTKY4ACWLT\n(integer)10\nZINTERSTOREzset:marketing:groupon:hottop:condition:interstore2zset:marketing:groupon:hottop:condition:2986080zset:marketing:groupon:hottop:available:group\n(integer)6\nZRANGEzset:marketing:groupon:hottop:condition:interstore0-1withscores\n1)”GF773FEJ7CC”\n2)”15″\n3)”G5VW3YF42UC”\n4)”17″\n5)”G4ZD5732YZQ”\n6)”18″\n7)”GKPKKW8XEY9″\n8)”40″\n9)”GL324DGWMZM”\n10)”45″\n11)”GFW8DUEND8S”\n12)”50″\nZREVRANGEzset:marketing:groupon:hottop:condition:interstore24withscores\n1)”GKPKKW8XEY9″\n2)”40″\n3)”G4ZD5732YZQ”\n4)”18″\n5)”G5VW3YF42UC”\n6)”17″\n
有了返回的团code集合之后就可以通过mget来批量获取string类型的团详情信息,这里就不贴出代码了。
由于篇幅和时间关系,这里就不展开太多的业务场景介绍了。这其中还涉及到计算cache过期时间的问题,这也跟促销活动的运营规则有关系,还涉及到有可能queryconditionhash冲突问题等,但是这些已经不与我们本节主题相关。
Redis内存数据结构与编码
我们已经了解了redis提供的5种数据类型,那么redis内部到底是如何支持这5种数据类型的,也就是说redis到底是使用什么样的数据结构来存储、查找我们设置在内存中的数据。
虽然我们使用5种数据类型来缓存数据,但是redis会根据我们存储数据的不同而选用不同的数据结构和编码。
我们日常使用的是redis提供的5种数据类型,但是这5种数据类型在内存中的数据结构和编码有很多种。随着我们存储的数据类型的不同、数据量的大小不同都会引起内存数据结构的动态调整。
本节只是做数据结构和编码的一般性介绍,不做过多细节讨论,一方面是关于redis源码分析的资料网上有很多,还有一个原因就是redis每一个版本的实现有很大差异,一旦展开细节讨论每一个点每一个数据结构都会很复杂,所以我们这里就不展开讨论这些,只是起到抛砖引玉作用。
OBJECTencodingkey、DEBUGOBJECTkey
我们知道使用type命令可以查看某个key是否是5种数据类型之一,但是当我们想查看某个key底层是使用哪种数据结构和编码来存储的时候可以使用OBJECTencoding命令。
SETstring:orderid:1010101010101010\nOK\nOBJECTencodingstring:orderid:10101010\n”int”\nSETstring:orderid:10101010″orderid:10101010″\nOK\nOBJECTencodingstring:orderid:10101010\n”embstr”\n
同样一个key,但是由于我们设置的值不同而redis选用了不同的内存数据结构和编码。虽然redis提供的string数据类型,但是redis会自动识别我们cache的数据类型是int还是string。
如果我们设置的是字符串,且这个字符串长度不大于39字节那么将使用embstr来编码,如果大于39字节将使用raw来编码。redis4.0将这个阀值扩大了45个字节。
除了使用OBJECTencoding命令外,我们还可以使用DEBUGOBJECT命令来查看更多详细信息。
DEBUGOBJECTstring:orderid:10101010\nValueat:0x7fd190500210refcount:1encoding:intserializedlength:5lru:6468044lru_seconds_idle:8\nDEBUGOBJECTstring:orderid:10101010\nValueat:0x7fd19043be60refcount:1encoding:embstrserializedlength:17lru:6465804lru_seconds_idle:1942\n
DEBUGOBJECT能看到这个对象的refcount引用计数、serializedlength长度、lru_seconds_idle时间,这些信息决定了这个key缓存清除策略。
简单动态字符串(simpledynamicstring)
简单动态字符串简称SDS,在redis中所有涉及到字符串的地方都是使用SDS实现,当然这里不包括字面量。SDS与传统C字符串的区别就是SDS是结构化的,它可以高效的处理分配、回收、长度计算等问题。
structsdshdr{\nunsignedintlen;\nunsignedintfree;\ncharbuf[];\n};\n
这是redis3.0版本的sds.h头文件定义,3.0.0之后变化比较大。len表示字符串长度,free表示空间长度,buf数组表示字符串。
SDS有很多优点,比如,获取长度的时间复杂度O(1),不需要遍历所有charbuf[]组数,直接返回len值。
staticinlinesize_tsdslen(constsdss){\nstructsdshdr*sh=(void*)(s-(sizeof(structsdshdr)));\nreturnsh->len;\n}\n
当然还有空间分配检查、空间预分配、空间惰性释放等,这些都是SDS结构化字符串带来的强大的扩展能力。
链表(linkedlist)
链表数据结构我们是比较熟悉的,最大的特点就是节点的增、删非常灵活。redisList数据类型底层就是基于链表来实现。这是redis3.0实现。
typedefstructlist{\nlistNode*head;\nlistNode*tail;\nvoid*(*dup)(void*ptr);\nvoid(*free)(void*ptr);\nint(*match)(void*ptr,void*key);\nunsignedlonglen;\n}list;\ntypedefstructlistNode{\nstructlistNode*prev;\nstructlistNode*next;\nvoid*value;\n}listNode;\n
在redis3.2.0版本的时候引入了quicklist链表结构,结合了linkedlist和ziplist的优势。
typedefstructquicklist{\nquicklistNode*head;\nquicklistNode*tail;\nunsignedlongcount;/*totalcountofallentriesinallziplists*/\nunsignedintlen;/*numberofquicklistNodes*/\nintfill:16;/*fillfactorforindividualnodes*/\nunsignedintcompress:16;/*depthofendnodesnottocompress;0=off*/\n}quicklist;\ntypedefstructquicklistNode{\nstructquicklistNode*prev;\nstructquicklistNode*next;\nunsignedchar*zl;\nunsignedintsz;/*ziplistsizeinbytes*/\nunsignedintcount:16;/*countofitemsinziplist*/\nunsignedintencoding:2;/*RAW==1orLZF==2*/\nunsignedintcontainer:2;/*NONE==1orZIPLIST==2*/\nunsignedintrecompress:1;/*wasthisnodepreviouscompressed?*/\nunsignedintattempted_compress:1;/*nodecan’tcompress;toosmall*/\nunsignedintextra:10;/*morebitstostealforfutureusage*/\n}quicklistNode;\n
quicklist提供了灵活性同时也兼顾了ziplist的压缩能力,quicklist->encoding指定了两种压缩算法。quicklist->compress表示我们可以进行quicklistnode的深度压缩能力。redis提供了两个有关于压缩的配置。
list-max-ziplist-size:ziplist长度控制
list-compress-depth:控制链表两端节点的压缩个数,越是靠近两端的节点被访问的机率越大,所以可以将访问机率大的节点不压缩,其他节点进行压缩
对比redis3.2的quicklist与redis3.0,很明显quicklist提供了更加丰富的压缩功能。redis3.0的版本是每个listnode直接缓存值,而quicklistnode还有强大的有关于压缩能力。
LPUSHlist:products:mall100200300\n(integer)3\nOBJECTencodinglist:products:mall\n”quicklist”\n
欢迎工作一到五年的Java工程师朋友们加入Java架构开发:854393687
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用”没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!
好了,文章到这里就结束啦,如果本次分享的yy6080电影网站源码分享和问题对您有所帮助,还望关注下本站哦!