Redis总结一:
引言:(Redis只能做缓存?我震惊了)
1.为什么要使用redis?
在我们最初存放数据之前,都是去用文件存储,然后当数据库慢慢开始去使用数据比如常用Mysql、orcal等。像这些数据库都是磁盘数据库。其实数据从之前文件方式存储到数据库存储中间也经历了一系类的演变。因为某一个技术的出现,一定是开发人员无法解决新问题的产生。那么文件存储到底带来了哪些缺陷和不足呢。
常识:文件通常可以存放在两个地方,硬盘里和内存中。当数据存放在硬盘中时会有两个指标去衡量读取效率。
秒==>毫秒==>微妙==>纳秒
1s=1000毫秒
1毫秒=1000微妙
1微秒=1000纳秒级别
磁盘中:
- 带宽:也就是我们常说的吞吐量。准确点来说就是单位时间(1ms)内读取数据的速度,通常是百兆级别。若硬盘性能好的最高可能达到G的级别,但是也不会达到十G以上这种。
- 寻址时间 单位是毫秒。
内存中:数据读取效率通常来说只有一个指标去衡量,寻址时间。
- 寻址时间:单位是纳秒级别。准确的说,单位时间内(1000*1000毫秒)内读取数据的速度。
那么当我们存放在文件中的数据逐渐变大时,例如当达到1T,2T或者更多的时候,查询时间会变得越来越慢。那么请问为什么会变慢???
这里面有一个术语叫全量扫描或者叫做全量IO。
比如有一个2T文件里面数据,现在需要查询具体的某个数据,那么不管你用什么方式,你都需要对这整个2T的文件进行全局扫描,然后才能得到一个查询结果,有还是没有。那么我们结合刚刚常识来说,由于在磁盘中读取数据的时间是毫秒级别的,那么光着2个T的文件全局IO一遍花费的时间就十分的久了,因此体验度十分差。
因此这就找到了文件读取数据的痛点了,就是全量扫描,那么之后一定会有一个新技术的出现来解决这个问题。这个技术就是数据库存储,比如我们经常使用的Mysql。
2.为什么要使用MySQL?
Mysql:当第一次用数据库的时候给人的感觉就是中相同数据读取比之前在文件读取效率要快。那么为什么会变快???
- 分治思想
据我现阶段对Mysql的了解,它的底层数据存储是维护可一个叫做datapage这个数据结构或者也可以叫做block,在数据库底层会有多个这种block,这些block大小比如4k、16k、32k这个可以自己配。具体说的话,之前若有2T的数据存放在文件中那么他将会是以一个整体的形式存放在文件中的,但用了数据库之后,数据库底层会对这些数据进行分治,将这2T的数据分别存放在这些block中。那么如果现在你在去查询某一条数据,若这条数据在某一个block中,那么就不用在去扫描其他的blcok,那么就解决了文件读取的全量扫描的问题。但是我们仔细想一下,这里有个误区,比如我们去查询某一条数据,我们仍然需要先对所有的block去进行遍历,然后才能确定这个数据在具体的哪一个block中。因此这不又回到了文件读取的方式吗?其实所谓的数据库的快不仅仅是用了分治的思想,还用到了索引。
2.分治思想+索引
在数据库的底层会有一个索引这种数据结构。他主要是用来存放这些block的位置,比如数据库中有10列数据,那么我们如果对每一列建索引的话,这每一列的索引信息也要存放到一个相对应的block中。但是这个存放索引的block的体积肯定是要小于存放数据的block.但是如果数据库中药有多列数据,并且要对这多列数据建列索引的话,这个索引的block也会增多,查询的时候也需要对这索性进行遍历(也就是说现在你要对这些索引去做全量扫描),那么这个速度也是会变慢,那么如何去规避索引的全量扫描,那么肯定是需要去对这些索引
做一些变化。
3.分治思想+索引(索引结构)
为了去规避索引的全量扫描,数据库在底层又对这些索引结构做了一种数据结构处理方式,引用了B树结构或者B+树的结构去管理这些索引,将这些索引做成树的叶子,存放索引的地址结构做成树根,并将这个树根(索引的索引)存放在内存中。那么你在去查询某一条具体数据的时候,数据将会变得很快。但是也不能排除一种情况,如果你去查询某一条数据索引都没有命中的话,那么这个时候机会退化成一种全局扫描了,底层会扫描所有的block.
我看到过关于Mysql这样一道面试题。
如果数据库很大的话(意思就是说有非常多的记录比如几万条或者几十万条的时候),那么对这些数据进行CRUD的时候是否为变慢?并说明原因?
如果当面试官问这道题的时候 ,我觉得肯定会有很多人回答变慢,但是要是说原因的话,感觉不太好回到了。
当时回到这道题的面试者是这样说的。
所谓的CRUD,其实就说读写的一种抽象说法,查询(C)抽象的来说就是读,增、删、改抽象说来时写。首先增删改是一定为变慢的,因为当执行这些操作的时候Mysql底层的这些索引要进行相对应的调整去面对这些不用的写操作。那么查询的话需要分场景:
-
第一个维度:当数据库有索引,并且只有一条连接也就是说有一个用户请求过来的时候,且索引命中的话,它的查询仍然是毫秒级别的仍然很快。
-
第二个维度:当数据库 有索引,但是有多个并发请求过来的的时候,即使每一个请求都能命中(这种概率就不会太大)的话,它的查询一定不快。
因为当这些高并发用户发来的请求到达数据库的时候,由于每一条查询都是独立的,当请求到达数据库的时候,不能保证查询的速度达到毫秒,因为当数据库将查询的数据从磁盘加载到内存的时候,还受吞吐量的影响,因为磁盘的吞吐量只有百兆级别,并发量过高的话,数据从磁盘加载到内存中就不会讲所有的请求都返回到内存,因此查询将会变慢。
4.硬盘的瓶颈
综上来看,不管是文件读取数据还是数据库读取都是受硬盘影响,硬盘问题是整个计算机的痛点。如果把数据放在硬盘上就会很难受。那么如何规避硬盘读取这个问题。
5.内存存取数据
极端想法:能不能将所有数据存放到内存中去,并且能支持一些简单、复杂的sql查询语句的关系型数据库。我看过一遍关于这个新闻
貌似在国外的SAP公司,有一种叫做HANA的数据库,这个数据库大小是2T,它可以将所有数据存放到内存中去,但是这一套完整的数据库+所用服务器大约需要2亿人民币,这样一种报价针对一些中型互联网企业是无法承担的。
6.一道面试题:
若将一个2T的数据存放到数据库中,那么相同条件下,存放在内存中是比2T大还是小?并说明原因?
小
比如数据库中存放了1000个userd对象信息,其中这个对象拥有一个address属性,并且这个属性值为"深圳",那么这个对象信息如果存放在内存中的话,jvm虚拟机,只会在常量池中存放一次这个属性的值,但是如果是在磁盘中的话,这1000个对象的属性值将会存放1000份在磁盘中保存。
7.磁盘和内存的对比
从以上的分析我们可以得出,如果某一公司需要保存所有的数据信息时,全部使用磁盘存储将会带来读取速度慢的问题,如果使用内存存取的话,那么将会带来钱的问题,因此这时需要一种折中方案。
8.折中技术(Redis)
其实我们发现,公司中的所有数据其实只有一部分的数据被经常访问和使用的,这种数据我们称为热点数据。其它那些不被经常访问的数据我们称为冷数据。那么我们就可以将这些经常访问的数据也就是热点数据存放到内存中去,那些不被经常访问的数据存放到数据库中去,那么就可以保证所有人都可以取到数据,并且速度也不会很慢,体验度好。那么有哪些内存数据库呢,比如Redis、MemCach。
8.Redis的特点:
- 内存数据库
- 丰富的value(String、List、Set、Map、Zset )且每个value都对应自己的方法
- 速度快
- work单线程,在6.0以上出现了多线程(但是这个多线程只是IO多线程)从IO里是单线程去处理。
- 持久化
9.MemCache和Redis对比
Memcachez的value只支持一种数据类型(String),比如客户端现在想要存取一个list集合[a,b,c,d]。那么由于MeMCache只支持String,但是这个世界上有一个很好的东西叫做Json它可以将任何数据类型转换为Json的字符串数组,因此这时候Memcache中的value就存放了一个字符串。但是问题来了 如果客户端只想要取回List中下标为2的元素(也就c),那么该怎么取?
如果用Memcache的话,你没有任何的办法去取指定元素的值,只能通过key将整个字符串取出,然后客户端需要自行的对获取到的这个value进行反序列化,将其转换成list集合,然后再去取出指定下标的元素(这个取出指定下标元素的值的过程,我们称为计算)。
如果用Redis的话,我们直接用list来存放客户端想存放的数组数据,然后要是想取出指定下标元素的数据话,客户端直接调用index()方法,参数传递想要取值的下标,Redis就会将计算后的结果返回给客户端。
-
解释单线程特点:
例如有一个秒杀系统:5000个用户在同一个时间点同时对一件商品做抢购。
Mysql处理情况:当s1去操作这个商品的时候,先去对这个商品上一把锁,然后执行完提交等一系类操作后,断开此次连接,接着s2去执行类似的操作。
Redis操作的时候:虽然多个客户端开始IO请求,看起来是并行的,并且每个客户端的请求分别调用decr时候,真正进入到Redis里面去的时候,其实只有一个,因为Redis是单线程处理。因此其它客户端请求的时候,这个decr方法其实是排着队的。所以其实Redis是单线程。串行化处理的。
10.Redis五种value基本类型的应用场景:
1.String
底层是用字节数组存储的,strlen返回的是字节数组的宽度。
-
真正字符串类型:由于们知道了Redis是用字节数组存储的,因此我们可以将一些小文件、图片之类的存放到Redis中,一般大小大约只有几kb(文件过大的不要放到里面,因为若放太大,Redis需要Io处理,那么后面的兄弟们都要等一会时间了)。
-
数值类型
-
bitmap类型:(二进制操作)优点:【速度快,省空间】
扩展:(我们常常忽略的setbit)
当我执行setbit这个命令的时候 清楚的看到 除了我们要设置的key 和value 之外,还有一个offset ,这个offset其实是一个偏移量,那么既然是偏移量,那么就有相对性来说了,相对哪里偏移,是的这个offset是【从做到又并且按照下标开始数】因为一个字节占8位,因此字节的下标是【0-7】这个区间。比如我开始将offset设置为1
尽然得到了一个@??这是为什么呢?底层又是怎么搞得呢?
首先:初始的时候这8位都是 0000 0000,然后我们
将offset设置为1 value为1那么意思就是说我从做到右的第二位 被我设置成1。即下标为1的位置的值为1因此我们得到一个0100 0000,这个值是64 而64对应的@
现在继续设置 setbit k1 7 1,这时offset被设置成了7 value为1,那么意思就是我们从做到右的第八位被我设置成了1,即下标为7的位置为1。0100 0001,这个值为1+64=65 而65对应的ascii就位大写的A
那么我们这样设置 setbit k1 9 1,意思在偏移量为9的位置设置1,那么这个时候已经超过1个字节的范围,该怎么办?这时候其实redis会在自动扩充一个字节。
那么这个值为多少呢?我们可以计算一下:
0100 0001 0100 0000:这个值就为A@
我在这里面测试主要想引出下面的问题:
比入我这样设置:setbit k1 9999 1???那么会怎么用,是不是会扩充很多个字节,没错。我们可以看一下测试结果.
没毛病,Redis就想一团云,具有很大的弹性,你想体积变大,那么我就随之变大,你体积变小,我也自动跟着变小,就这么任性。这个字节看上去似乎挺大,但是其实我们计算一下,也就是1k多一点点,因为1k=1024个字节,这1250个字节,就那么点点。我靠,仔细想想,内存用了1k多大小就存了一千多个字节,效率贼高。但是到这里可能还是不能看到这玩意到底能干什么用,对的,其实单独用这个setbit其实没啥太大作用,它主要和bitcount这个命令搭配作用,就好比一个男程序员,让他自己在公司干可能没啥动力,比如这时候公司来了个前端妹子和他一起cp,那么它就会发挥出它无穷的价值。
那么我首先还是先来介绍一下这个bitcount命令:是用来统计指定key中1的个数,这有两个start 和end,这表示offset(偏移量)的区间长度。通常我们取【0,-1】表示真个偏移量的范围。
这里说明k1这个键中一共有4个1,那么对不对呢。在k1中 我们是这样设置的:第一次:0100 0000 第二次0100 0001 第三次0100 0001 0100 0000 第四次 是在9999下标为9999的位置上设置的1,因此k1确实是被设置了4个1。
注意:若bitcount k1 0 0【表示是第一个字节出现1的个数】
应用场景1:【统计任意时间窗口内,登录的用户数,二维矩阵】
分析:如果当项目经理提出了这样一个需求 让你来实现它的时候,你会怎么解决?
可能我们用Mysql数据库来做,比如我们建立一个用户表,专门用来记录注册用户的信息,然后里面设计Id、姓名、登录时间、等字段信息,然后以时间作为条件来进行sql查询,统计该该时间段的用户个数,然后做一个count()操作。当然这也能完成项目经理提出的需求,只不过性能会超慢,比如该时间段有10000个用户登录,那么你就要至少设计大于10000条记录的表来封装这些数据,那么你在进行对这个10000个记录进行count()操作,效率肯定会很低。那么项目经理要是让你够提高这个效率,该怎么办?
那么用redis的setbit命令操作就会变得十分高效。
设计:
- 模拟一个二维矩阵,以时间作为key,用户作为value
- 然后在用setbit命令
模拟场景:比如在20200924这一天 有三个用户登录分别纪为A、B、C、在20200925这一天有两个用户登录(偏移量代表不同用户登录,并且这个偏移量当用户足够多的时候可以自动扩充)那么我们可以这样做;
(9月24这一天)
A:setbit 20200924 1 1
B:setbit 20200924 0 1
C:setbit 20200924 2 1
(9月25这一天)
A:setbit 20200925 0 1
B:setbit 20200925 1 1
如果我们不用计算机去计算,人为的判断有多少人登录,我们认为是几个用户登录?是不是只有三个,对,是三个用户登录,剩下的没有登录的用户都是僵尸用户,那么计算机如何知道是三个用户呢?我们这样看:
A用户是在偏移量为1的位置设置为1 那么它内存中其实是这样的情况:0100 0000
B用户是在偏移量为0的位置设置为1 那么它内存中其实是这样的情况:1000 0000
C用户是在偏移量为2的位置设置为1 那么它内存中其实是这样的情况:0010 0000
A用户是在偏移量为0的位置设置为1 那么它内存中其实是这样的情况:1000 0000
B用户是在偏移量为1的位置设置为1 那么它内存中其实是这样的情况:0100 0000
然后进行or运算
那么结果就是1101 0000,
最后进行bitcount统计这个结果中的1的个数,那么就是3个。
我们看到三个用户一共才用了一个字节,而用mysql存储这三个用户大约在2kb,我们算一下当1024个用户的时候用redis来存取的话也就是1kb,而这个时候Mysql大约要用2k*1024=4048k,将近4000倍,这样的效率绝对贼强。
应用场景2【统计某个用户在任意时间窗口内,登录的次数,二维矩阵】
ps【其实统计某个用户的登录次数是较常见的,比如通过对你登录该网站的次数 可以定位你为普通用户还是热点用户,然后在通过算法给你推荐你浏览较多的信息输送,这样给你的体验度就很高。】
分析:如果当项目经理又提出了这样一个需求 让你来实现它的时候,你又会怎么解决?
同样我们先用Mysql数据库来做,我们仍然需要建立一个用户表,专门用来记录注册用户的信息,然后里面设计Id、姓名、登录时间、登录次数等字段信息,然后以时间作为条件来进行sql查询,并且当用户每登录一次就在数据库的登录记录字段上赋值1操作,最后统计该时间段的用户1的次数,同样这种效率低而且会占用较多的磁盘空间。
Redis设计:
- 模拟一个二维矩阵,以用户作为key,登录时间作为value(将上面的矩阵进行了一种置换)
- 然后在用setbit命令
模拟场景:笔比如有一个用户hzk,现在需要统计他在一年时间内登录该网站的次数,但是实际上我就只有四天登录上了,那么我可以这样设计:
setbit hzk 0 1
setbit hzk 1 1
setbit hzk 6 1
setbit hzk 364 1
hzk用户是在偏移量为0(表示第一天我登录了)的位置设置为1 那么它内存中其实是这样的情况:1000 0000
hzk用户是在偏移量为1(表示第一天我登录了)的位置设置为1 那么它内存中其实是这样的情况:0100 0000
hzk用户是在偏移量为6(表示第起七天我登录了)的位置设置为1 那么它内存中其实是这样的情况:0000 0010
hzk用户是在偏移量为364(表示第365天我登录了)的位置设置为1 那么它内存中其实是这样的情况:0000 0000…0001
然后用bitcount统计hzk这个key中出现的1个数
我们可以看到,一年之间hzk这个用户一共登录了四次,并且仅仅才占用了46个字节而已。
应用场景3【布隆过滤器的简单使用,线性使用】
简单介绍一下什么叫做布隆过滤器:
布隆过滤器就是一个二进制向量的数据结构,说白了就是一个数组,但是数组中的元素之只能是0和1
但是光理解这么一点概念还是远远不够的,通过我对布隆过滤器的了解,我大致在阐述一下它的核心思想,其实也比很好理解。
首先我开头强调一点,布隆过滤器不一定判断该元素一定存在,但是一定能判断该元素一定不存在。
猛的一听,这很懵逼,我擦,这是个啥意思。
由于我们知道布隆过滤器的底层是一个二进制数组,简称位图。并且它拥有一套hash算法比较规则。那么我们可以这样玩,当有多个请求过来的时候想要判断数据库中某个元素是否存在,那么我们可以先在数据库中取出每一个元素作为key,然后通过hash计算,对应到布隆过滤器中的下标位置,因为布隆过滤器初始的时候元素都是0,那么通过hash函数计算之后,将对应的位置0置为1,然后请求某个数据时,也先进行hash函数判断,因为同一个请求数据,通过hash计算一定能得到相同的下标,然后若在布隆过滤器中取到1,那么说明数据库中一定有想要的数据。但是由于使用的是hash计算,那么一定有hash碰撞,这个肯定避免不了。理论先说到这,我在下面举个例子:
首选存这三个元素叫URL1,URL2,URL3。当URL1通过hash函数(使用三个hash函数)计算之后,确定在2,6,7这三个下标位置,然后我们将这三个位置的0,都置为1;然后URL2通过hash函数计算后,确定在6,9,14这三个下标位置,继续讲这三个位置置1;接着URL3也通过Hash计算之后,在2,4,8这三个位置置1。然后我们通过URL1、URL2、URL3的请求,也通过三次hash计算,并将得到的每个1做and运算,若结果为1,则表示请求的资源存在,反之,不存在。
由于我们知道hash计算,一定存在误判,那么假如现在我请求一个叫做URL1000的资源,但是数据里并没有这存放这个元素,但是通过
hash计算之后位置入下图所示,那么它同过三个hash的and之后,也得到了1,于是便像请求者返回数据库中存在这个资源。因此这就造成了误判。那怎么去解决呢?
-
解决方法1:
通过设置多些的hash函数,因为当函数越多,计算的结果也越多, 相同位置重叠的情况就小,那么and之后结果越准确。
-
解决方法2:
将位图设置叫多,比如设置一个长度很长的位置,那么通过hash计算之后,碰撞的机会就很小。这点不知能不能理解。我举个例子
比如一辆汽车有期初有10个位置,但是我有15个人来做,不允许站着,那么两个人做同一个位置的几率就非常大,但是现在还是15个人,我有100个位置,那么两个人在做同一个位置基本就不存在,但是也不是没有这种可能。
那么我们到底采用哪种方法来处理呢,是用较多的hash函数策略来判断还是用较多的位置来判断,我自己感觉还是扩展较多位图比较好,因为一旦我们采用较多的hash函数来解决碰撞,但是你想一下,计算越多,那么速度就会越低,响应给请求就越慢。体验度会很差,相反,扩展位图来避免冲突的话,由于8位才占用一个字节,即使有1024个请求,那么也就占用1k的大小空间。而且寻址的速度肯定比计算速度快。
最后我附加一下布隆过滤器的特点:
- 由于存放的不是完整的数据(只是0、1)所以占用内存很少,而且新增、查询速度快
- 多个hash增大随机性,减少hash碰撞
- 扩大数组范围,使hash分布均匀,减少误判。
- 返回结果是随机性的,不确定的。只能判断一个元素不存在,但是不能判断该元素的存在
- 删除麻烦
那么通过介绍完布隆过滤器之后,我们就可以同redis支撑起来,因为支起来那个数组刚好是我们setbit 中的offset(理论无限大),比如我setbit URL1 2 1、setbit URL1 6 1、setbit URL1 7 1。最终这个URL1就是0010 0011。同理其它的key也是如此。接着就是通过计算将不同的key 进行and 运算,最后在去判断这个运算的结果。确定数据库中是否存在某个元素,若发现不存在,那么就不让这个请求到达数据库,从而减轻数据库的压力。
2.List
特点:是一个有序可以重复的链表,每个列表最多可以存储 232 - 1 个元素(40多亿)
若是同向操作,则模仿的是栈(lpush lpop)
若是异向操作,则模仿的是队列(lpush rpop)
可以模仿数组(lindex key index)
模仿栈(先进后出)
模仿队列(先进先出)
模仿数组
总结一下:
`lpush` + `lpop` = stack 先进后出的栈
`lpush` + `rpop` = queue 先进先出的队列
`lpush` + `ltrim` = capped collection 有限
`lpush` + `brpop` = message queue 消息队列
应用场景1:消息队列
list类型的lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能,故而可以用Redis的list类型实现简单的点对点的消息队列。不过我不推荐在实战中这么使用,因为现在已经有Kafka、NSQ、RabbitMQ等成熟的消息队列了,它们的功能已经很完善了,除非是为了更深入地理解消息队列,不然我得没必要去重复造轮子。
应用场景2:排行榜
list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销量排行、学校每次月考学生的成绩排名、斗鱼年终盛典主播排名等,下图是酷狗音乐“K歌擂台赛”的昨日打擂金曲排行榜,每日计算一次,存储在list类型中,接口访问时,通过page和size分页获取打擂金曲。 但是,并不是所有的排行榜都能用list类型实现,只有定时计算的排行榜才适合使用list类型存储,与定时计算的排行榜相对应的是实时计算的排行榜,list类型不能支持实时计算的排行榜,之后在介绍有序集合sorted set的应用场景时会详细介绍实时计算的排行榜的实现。
3.hash
本身就是一个键值对集合。可以当做Java中的Map<String,String>对待。每一个hash可以存储2^32-1个 键值对。大约40多亿
其实使用过Redis的朋友都知道,使用String类型几乎能解决很多问题,但是比如现在有一个用户对应的肯定有用户名、年龄、性别等属性,那么我么如果使用String 类型的话,可能不够灵活,而且这还只是一个用户,如果用户多的情况,那么字段将会更多,取出里面的属性会更慢,而且每次都要与服务进行通信,性能也会减少。那么这时候我们就可以用hash来存放。
现在假如有两个用户,hzk、zs.分别都有两个属性name、age.那么如果用hset来存的话,我们可以看到,存的可以是一个指定的用户,而不在像String那样将一个用户的某一个属性作为一个字段。那么这样的话在取的时候就会十分方便。比如我现在只想取hzk的所有属性那么我们只用getkeys hzk ,若是只取hzk这个用户的属性对应的值,我们只用getvals hzk。这样这就会让在后台取数据的时候十分灵活,因为通常我们也都知道,用户对应的属性,我们这在前端已经写好了,我们只需要将它的属性所对应的值取出来就行。
应用场景1. 购物车
以用户id为key,商品id为field,商品数量为value,恰好构成了购物车的3个要素,如下图所示。
应用场景2:存储对象
hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
在介绍string类型的应用场景时有所介绍,string + json也是存储对象的一种方式,那么存储对象时,到底用string + json还是用hash呢?
两种存储方式的对比如下表所示。
string + json | hash | |
---|---|---|
效率 | 很高 | 高 |
容量 | 低 | 低 |
灵活性 | 低 | 高 |
序列化 | 简单 | 复杂 |
当对象的某个属性需要频繁修改时,不适合用string+json,因为它不够灵活,每次修改都需要重新将整个对象序列化并赋值,如果使用hash类型,则可以针对某个属性单独修改,没有序列化,也不需要修改整个对象。比如,商品的价格、销量、关注数、评价数等可能经常发生变化的属性,就适合存储在hash类型里。
当然,不常变化的属性存储在hash类型里也没有问题,比如商品名称、商品描述、上市日期等。但是,当对象的某个属性不是基本类型或字符串时,使用hash类型就必须手动进行复杂序列化,比如,商品的标签是一个标签对象的列表,商品可领取的优惠券是一个优惠券对象的列表(如下图所示)等,即使以coupons(优惠券)作为field,value想存储优惠券对象列表也还是要使用json来序列化,这样的话序列化工作就太繁琐了,不如直接用string + json的方式存储商品信息来的简单。
4.set
Redis的set是string类型的无序集合。它是基于哈希表实现的。set类型插入数据时会自动去重。大可 以包含2^32-1个元素。
应用场景1. 好友/关注/粉丝/感兴趣的人集合
set类型唯一的特点使得其适合用于存储好友/关注/粉丝/感兴趣的人集合,集合中的元素数量可能很多,每次全部取出来成本不小,set类型提供了一些很实用的命令用于直接操作这些集合,如
①. SINTER命令可以获得A和B两个用户的共同好友
②. SISMEMBER命令可以判断A是否是B的好友
c. scard命令可以获取好友数量
应用场景2.随机事件
通常,app首页的展示区域有限,但是又不能总是展示固定的内容,一种做法是先确定一批需要展示的内容,再从中随机获取。如下图所示,酷狗音乐K歌擂台赛当日的打擂歌曲共29首,首页随机展示5首;昨日打擂金曲共200首,首页随机展示30首。
但是使用SRANDMEMBER 时需要主要count的取值,一共分为4种情况
-
正数:(小于集合元素的个数)代表去重的集合
场景:抽奖,假如有一台冰箱这个奖品,我只希望有三个不同的人中奖。
-
正数(大于集合元素的个数)代表去重元素的集合,但是只能返回集合元素的大小
-
负数:(大于集合元素个数的相反数)返回一个有可能有重复元素的集合
-
负数:(小于集合元素个数的相反数)一定返回重复元素的集合
场景:抽奖,假如有开心奖,一个人可以多次抽中。
应用场景3:去除重复元素(差集)
假如在某种情况下,我们上传一张图片到外部服务器上,当我们点击的时候,该图片已经被我提交到了服务器上了,但是这时候我并没有点击提交按钮,说白了就是我没有将表单信息提交到数据库中,那么如果我多次执行这种操作步骤,那么服务器上就会产生许多僵尸图片,数据库中没有,而服务器上却存放了很多,因此,我么可以将每次上传到服务器之后,将该图片也传一份到Redis,同时在提交到数据库之后,也上传到Redis中,然后我们在开个定时器,假设定时5分钟,去自动清除服务器中的垃圾图片。这个定时器的执行任务就是去将服务器中的图片和数据库中的图片做一次sdiff运算,然后在将运算的结果,给srem掉。
5.zset
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一 个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一 的,但分数(score)却可以重复。
应用场景:
1. 实现按点击量排序
以文章的ID作为键,文章的点击量作为分数,如果将键名命名为posts:page.view,每次访问某篇文章的时候就zincrby posts:page.view 1 文章ID 来更新点击量,需要按照点击量来显示文章的时候 zrevrange posts:page.view start end 来获取文章的某一范围内的文章id。
2. 按照文章的发布时间来排序
如果文章的发布时间是可以修改的,我们可以使用有序集合来实现,元素是文章的id,元素的分数是发布时间,通过修改元素的对应的分数就可以达到更改时间的目的。
3. 排行榜
比如要取得某个班级的前十名的学生名单,使用 zadd scoreboard 将学生的分数保存,那么取得前十名的同学名单是很容易的,zrevrange scoreboard 0 9
4. 可以做带权重的队列,
e)却可以重复。
应用场景:
1. 实现按点击量排序
以文章的ID作为键,文章的点击量作为分数,如果将键名命名为posts:page.view,每次访问某篇文章的时候就zincrby posts:page.view 1 文章ID 来更新点击量,需要按照点击量来显示文章的时候 zrevrange posts:page.view start end 来获取文章的某一范围内的文章id。
2. 按照文章的发布时间来排序
如果文章的发布时间是可以修改的,我们可以使用有序集合来实现,元素是文章的id,元素的分数是发布时间,通过修改元素的对应的分数就可以达到更改时间的目的。
3. 排行榜
比如要取得某个班级的前十名的学生名单,使用 zadd scoreboard 将学生的分数保存,那么取得前十名的同学名单是很容易的,zrevrange scoreboard 0 9
4. 可以做带权重的队列,
比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择score的倒序来获取工作任务。让重要的任务优先执行