有点长的博客:Redis不是只有get set那么简单

我之前还没接触Redis的时候,听到大数据组的小伙伴在讨论Redis,以为这东西好高端,要是哪天咱们组也可使用下Redis就行了,好长一段时间后,咱们项目中终于引入了Redis这个技术,我用了几下,感受Redis也就那么回事啊,不就是get set吗?当我又知道Redis还有自增、自减操做,并且这些操做仍是原子性的,秒杀就能够用这个技术,我就以为我已经熟悉Redis了。相信有很多curd boy是和之前的我一个想法:Redis不就是get set increment吗?其实否则,Redis远远没有咱们想象中的那么简单,今天我就在此献丑来谈谈Redis。linux

关于Redis是什么,如何安装等问题就不阐述了,咱们直接进入正题吧。程序员

Redis五种数据类型及应用场景

Redis有五种数据类型,即 string,list,hash,set,zset(sort set),我想这点只要稍微对Redis有点了解的小伙伴都应该清楚。下面,咱们就来讨论下这五种数据类型的应用场景。面试

string

这个类型相信是你们最熟悉的了,可是千万不要小瞧它,它能够作不少事情,也能够牵出一系列的问题。redis

咱们先从最简单的入手:算法

localhost:6379> set coderbear hello
OK
localhost:6379> get codebear
"hello"
复制代码

这两个命令相信你们都知道,我就不解释了,咱们再来用下strlen这个命令:shell

localhost:6379> strlen codebear
(integer) 5
复制代码

哦,我明白了strlen这个命令能够得到Value的长度啊,hello的长度是5,因此输出就是5。这个解释对不对呢?不着急,咱们慢慢往下看。数据库

咱们使用append命令为codebear这个key追加点东西:编程

APPEND codebear 中国
复制代码

若是咱们再次使用strlen命令会输出什么呢?固然是7啊,虽然我数学很差,可是10之内的数数,我仍是no problem的,可是当咱们再次执行strlen命令,你会发现一个奇怪的现象:windows

localhost:6379> strlen codebear
(integer) 11
复制代码

纳尼,为何是11,是否是咱们的打开方式不对,要再也不试下?不用了,就算再试上三生三世,你看到的输出仍是11。这是为何呢?这就牵扯到二进制安全问题了。api

二进制安全

所谓的二进制安全就是只会严格的按照二进制的数据存取,不会妄图以某种特殊格式解析数据。Redis就是二进制安全的,它存取的永远是二进制数据,也能够说存取的永远是字节数组。

咱们来get下codebear康康:

get codebear
"hello\xe4\xb8\xad\xe5\x9b\xbd"
复制代码

你会发现好端端的"hello中国",存储到Redis居然变成这样了,由于咱们的Xshell客户端使用的是UTF-8,在UTF-8下,一个中文一般是三个字节,两个中文就是6个字节,因此在Redis内部"hello中国"占了5+6=11个字节。

若是你还不信,咱们把Xshell的编码改为GBK看看,在GBK的世界里,一个中文一般占两个字节,因此:

localhost:6379> set codebeargbk 中国
OK
localhost:6379> get codebeargbk
"\xd6\xd0\xb9\xfa"
localhost:6379> strlen codebeargbk
(integer) 4
复制代码

因此说,醒醒吧,小伙计,在Redis里面是不可能存中文的,咱们之因此在程序里面能够轻轻松松的拿到中文,只是由于API作了解码的操做。

没想到一个String还牵出二进制安全的问题,看来真是不能小瞧任何一个知识点啊,这也就是常说的搜索地狱,当你查找一个问题,发现这个问题的答案又出现了一个你不懂的东西,因而你又开始看那个你不懂的东西,而后又冒出另一个你不懂的概念,因而...说多了都是泪啊。

咱们常常用Redis作缓存,用到的就是set get这两个命令了,咱们还能够用Redis作秒杀系统,在绝大部分状况下,用的也是String这个数据类型,让咱们继续往下看:

localhost:6379> set codebearint 5
OK
localhost:6379> incr codebearint 
(integer) 6
复制代码

也许你没用过incr命令,可是能够从结果和命令名称猜出incr这个命令是干吗的把,没错,就是自增,既然有自增,还能够作自减:

localhost:6379> decr codebearint 
(integer) 5
复制代码

刚才是6,调用了decr 命令后,又变成5了。

好了,又有一个问题,String是字符串啊,它怎么能够作加法或减法的操做?

咱们用type命令来检查下codebearint 这个key的类型是什么:

localhost:6379> type  codebearint
string
复制代码

没错,是如假包换的String类型啊。

咱们再来看一个东西:

localhost:6379> object encoding codebear
"raw"
localhost:6379> object encoding codebearint
"int"
复制代码

原来在Redis的内部还会为这个key打上一个标记,来标记它是什么类型的(这个类型可和Redis的五种数据类型不同哦)。

bitmap

有这么一个需求:统计指定用户一年之中登陆的天数?这还不简单,我建个登陆表,不就能够了吗?没错,确实能够,可是这代价是否是有点高,若是有100万个用户,那么这登陆表要有多大啊。这个时候,bitmap就横空出世了,它简直是解决此类问题的神器。

咱们先来看看什么是bitmap,说穿了,就是二进制数组,咱们来画一张图说明下:

图片.png

这就是bitmap了,由许许多多的小格子组成,格子里面只能放1或者0,说的专业点,那一个个小格子就是一个个bit。 String就很好的支持了bitmap,提供了一系列bitmap的命令,让咱们来试试:

setbit codebear 3 1
(integer) 0
localhost:6379> get codebear
"\x10"
复制代码

这是什么意思呢,就是说如今有8个小格子,第四个格子里面放的是1(索引从0开始嘛),其余都是0,就像这样的:

图片.png

让咱们计算下,大小应该是多少,1 2 4 8 16 ,没错,用十进制表示是16,而咱们get codebear输出的是“\x10”,“\x”表明是十六进制,也就是16进制的10,16进制的10就是十进制的16了。

咱们再用下strlen命令看下:

localhost:6379> strlen codebear
(integer) 1
复制代码

看来只占据了一个字节,让咱们继续:

localhost:6379> setbit codebear 5 1
(integer) 0
复制代码

bitmap就变成了下面这个酱紫:

图片.png

大小用十进制表示就是20。

咱们继续看下strlen:

localhost:6379> strlen codebear
(integer) 1
复制代码

仍是只占据了一个字节,咱们明明已经存储了两个数据了,是否是很是神奇。

让咱们继续:

localhost:6379> setbit codebear 15 1
(integer) 0
localhost:6379> strlen codebear
(integer) 2
复制代码

从这里能够看出bitmap是能够扩展的,因为如今我在第16个格子里面放了1,因此bitmap扩展了,如今strlen是2。 那么我想知道如今16个格子里面有多少格子是1的,怎么办呢?用bitcount命令:

localhost:6379> bitcount codebear
(integer) 3
复制代码

到了这一步,是否是豁然开朗了,用bitmap能够轻松统计指定用户一年之中登陆的天数。 咱们假设codebear第一天登陆过,次日登陆过,最后一天登陆过:

localhost:6379> setbit codebear 0 1
(integer) 0
localhost:6379> setbit codebear 1 1
(integer) 0
localhost:6379> setbit codebear 364 1
(integer) 0
localhost:6379> bitcount codebear 
(integer) 3
复制代码

继续用strlen来看看,记录了整年登陆过的日子占据了多少字节:

localhost:6379> strlen codebear
(integer) 46
复制代码

仅仅46个字节,就算每一年都登陆,也只占用46个字节,我不知道这样的数据存在数据库应该是多大的,可是我想远远不止46个字节把,若是有100万个用户,也就不到50M,哪怕这100万个用户每天登陆占据的字节也是这些。

咱们再把上面的需求改下:统计指定用户在任意时间窗口内登陆的天数?

bitcount命令后面还能够带两个参数,即 开始 和 结束:

localhost:6379> bitcount codebear 0 2
(integer) 2
复制代码

咱们还能够把第二个参数写成-1,表明直到最后一位,即:

localhost:6379> bitcount codebear 0 -1
(integer) 3
复制代码

bitmap的强大远远不止这些,咱们再来康康第二个需求:统计任意日期内,全部用户登陆状况:

  1. 统计任意一天内登陆过的用户
  2. 统计任意几天内登陆过的用户
  3. 统计任意几天内天天都登陆的用户

脑阔疼啊,这特么的是人干的事情吗?别急,这一切均可以用bitmap来实现。

第一个需求,很好实现,假设用户codebear的userId是5,用户小强的userId是10,咱们能够创建一个key为日期的bitmap,其中第四个、第九个小格子是1,表明userId是五、userId是1的用户在这一天登陆过,而后bitcount下就万事大吉,以下所示:

localhost:6379> setbit 20200301 4 1
(integer) 0
localhost:6379> setbit 20200301 9 1
(integer) 0
localhost:6379> bitcount 20200301
(integer) 2
复制代码

要实现下面两个需求,得用新的命令了,直接看结果吧:

localhost:6379> setbit 20200229 9 1
(integer) 1
localhost:6379> bitop and andResult 20200301 20200229
(integer) 2
localhost:6379> bitcount andResult
(integer) 1
localhost:6379> bitop or orResult 20200301 20200229
(integer) 2
localhost:6379> bitcount orResult
(integer) 2
复制代码

下面来解释下,首先又建立了一个key为20200229的bitmap,其中第10个小格子为1,表明用户Id为10的用户在20200229这一天登陆过,接下来对key为20200301和20200229的bitmap作与运算,结果也是一个bitmap,而且把结果放入了andResult这个key中,下面就是熟悉的bitcount命令了,康康有多少个小格子为1的,结果是1,也就是这两天,天天都登陆的用户有一个。

既然有与运算,那么就有或运算,下面就是或运算的命令,求出了这两天有两位用户登陆。

这样后面两个需求就轻松搞定了。

还有大名鼎鼎的布隆过滤器也是用bitmap实现的,关于布隆过滤器在之前的博客也介绍过。

看了那么多的例子,你们有没有发现一个问题,全部的运算都在Redis内部完成,对于这种状况,有一个很高大上的名词:计算向数据移动。与之相对的,把数据从某个地方取出来,而后在外部计算,就叫数据向计算移动。

list

Redis的list底层是一个双向链表,咱们先来康康几个命令:

localhost:6379> lpush codebear a b c d e
(integer) 5
localhost:6379> lrange codebear 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"
复制代码

push,我懂,推嘛,可是前面+个l是什么意思呢,前面的l表明左边,lpush就是在左边推,这样第一个推动去的,就是在最右边,lrange是从左开始拿出指定索引范围内的数据,后面的-1就是表明拿到最后一个为止。

既然能够在左边推,那么必须能够在右推啊,咱们康康:

localhost:6379> rpush codebear z
(integer) 6
localhost:6379> lrange codebear 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"
6) "z"
复制代码

还有两个弹出命令也很经常使用,咱们来使用下:

localhost:6379> lpop codebear
"e"
localhost:6379> lrange codebear 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
5) "z"
localhost:6379> rpop codebear
"z"
localhost:6379> lrange codebear 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
复制代码

lpop是弹出左边第一个元素,rpop就是弹出右边第一个元素。

若是咱们使用lpush,rpop或者rpush,lpop这样的组合,就是先进先出,就是队列了;若是咱们使用lpush,lpop或者rpush,rpop这样的组合,就是先进后出,就是栈了,因此Redis还能够做为消息队列来使用,用到的就是list这个数据类型了。

相信你们必定都玩过论坛,后面发帖的,帖子一般在前面。为了性能,咱们能够把帖子的数据放在Redis中的list里面,可是总不能无限往list里面扔数据吧,通常前面几页的帖子翻看的人会多一些,再日后面的帖子就不多有人看了,因此咱们能够把前面几页的帖子数据放在list中,而后设定一个规则,定时去list删数据,咱们就能够用到list的ltrim吗,命令:

localhost:6379> ltrim codebear 1 -1
OK
localhost:6379> lrange codebear 0 -1
1) "c"
2) "b"
3) "a"
复制代码

这个ltrim有点奇怪,它是保留索引范围以内的数据,删除索引范围以外的数据,如今给定的第一个参数是1,第二个参数是-1,就是要保留从索引为1到结束的数据,因此索引为0的数据被删除了。

hash

如今有一个产品详情页,里面有产品介绍,有价格,有舒适提示,有浏览数,有购买人数等等一堆信息,固然咱们能够把整个对象都用String来存储,可是可能有一些地方只须要产品介绍,尽管是这样,咱们仍是必须得把整个对象都拿出来,是否是有点不太划算呢?hash就能够解决这样的问题:

localhost:6379> hset codebear name codebear
(integer) 1
localhost:6379> hset codebear age 18
(integer) 1
localhost:6379> hset codebear sex true
(integer) 1
localhost:6379> hset codebear address suzhou
(integer) 1
localhost:6379> hget codebear address
"suzhou"
复制代码

若是咱们是存储整个对象,如今想修改下age,怎么办?要把整个对象所有拿出来,而后再赋值,最后又得放回去,可是如今:

localhost:6379> hincrby codebear age 2 
(integer) 20
localhost:6379> hget codebear age
"20"
复制代码

set

set是一种无序,且去重的数据结构,咱们能够用它去重,好比我如今要存储全部商品的Id,就能够用set来实现,有什么场景须要存储全部商品的Id呢?防止缓存穿透,固然防止缓存穿透有不少实现方案,set方案只是其中的一种。咱们来康康它的基本用法:

localhost:6379> sadd codebear 6 1 2 3 3 8 6
(integer) 5
localhost:6379> smembers codebear 
1) "1"
2) "2"
3) "3"
4) "6"
5) "8"
复制代码

能够很清楚的看到咱们存进去的数据被去重了,并且数据被打乱了。

咱们再来看看srandmember这个命令有什么用?

localhost:6379> srandmember codebear 2
1) "6"
2) "3"
localhost:6379> srandmember codebear 2
1) "6"
2) "2"
localhost:6379> srandmember codebear 2
1) "6"
2) "3"
localhost:6379> srandmember codebear 2
1) "6"
2) "2"
localhost:6379> srandmember codebear 2
1) "8"
2) "3"
复制代码

srandmember 后面能够带参数,后面跟着2,就表明随机取出两个不重复的元素,若是想取出两个能够重复的元素,怎么办呢?

localhost:6379> srandmember codebear -2
1) "6"
2) "6"
复制代码

若是后面跟着负数,就表明取出的元素能够是重复的。

若是后面跟的数字大于set元素的个数呢?

localhost:6379> srandmember codebear 100
1) "1"
2) "2"
3) "3"
4) "6"
5) "8"
localhost:6379> srandmember codebear -10
 1) "8"
 2) "1"
 3) "1"
 4) "1"
 5) "6"
 6) "1"
 7) "1"
 8) "2"
 9) "6"
10) "8"
复制代码

若是是正数的话,最多把set中全部的元素都返回出来,由于正数是不重复的,再多返回一个出来,就重复了,若是是负数,那么不影响,后面跟着几,就返回多少个元素出来。

咱们作抽奖系统,就能够用到这个命令了,若是能够重复中奖,后面带着负数,若是不能重复中奖,后面带着正数。

set还能够计算差集、并集、交集:

localhost:6379> sadd codebear1 a b c
(integer) 3
localhost:6379> sadd codebear2 a z y
(integer) 3
localhost:6379> sunion codebear1 codebear2
1) "a"
2) "c"
3) "b"
4) "y"
5) "z"
localhost:6379> sdiff codebear1 codebear2
1) "b"
2) "c"
localhost:6379> sinter codebear1 codebear2
1) "a"
复制代码

上面的命令就不过多解释了,这有什么用呢,咱们能够利用它来作一个“骗取融资”的推荐系统:大家的共同好友是谁,大家都在玩的游戏是哪一个,你可能认识的人。

zset

set是无序的,而zset是有序的,其中每一个元素都有一个score的概念,score越小排在越前面,仍是先来康康它的基本使用把:

localhost:6379> zadd codebear 1 hello 3 world 2 tree
(integer) 3
localhost:6379> zrange codebear 0 -1 withscores
1) "hello"
2) "1"
3) "tree"
4) "2"
5) "world"
6) "3"
localhost:6379> zrange codebear 0 -1
1) "hello"
2) "tree"
3) "world"
复制代码

如今咱们就建立了一个key为codebear 的zset,往里面添加了三个元素:hello ,world ,tree,score分别为1,3,2,后面用zrange取出结果,发现已经按照score的大小排好序了,若是后面跟着withscores,就会把score一块儿取出来。

若是咱们想看看tree排在第几位,咱们能够用zrank命令:

localhost:6379> zrank codebear tree
(integer) 1
复制代码

由于是从0开始的,因此结果是1。

若是咱们想查询tree的score是多少:

localhost:6379> zscore codebear tree
"2"
复制代码

若是咱们想取出从大到小的前两个,怎么办:

localhost:6379> zrange codebear -2 -1
1) "tree"
2) "world"
复制代码

可是这样的结果是有些错误的,从大到小的前两个,第一个元素是world,又该如何呢:

localhost:6379> zrevrange codebear 0 1
1) "world"
2) "tree"
复制代码

像排行榜,热点数据,延迟任务队列均可以用zset来实现,其中延迟任务队列在我之前的博客有介绍过。

谈到szet,可能还会引出一个问题,Redis中的zset是用什么实现的?跳表。

关于跳表,在这里就不展开了,为何要用跳表实现呢,是由于跳表的特性: 读写均衡。

Redis为何那么快

这是一个经典的面试题,几乎面试谈到Redis,80%都会问这问题,为何Redis那么快呢?主要有如下缘由:

  1. 编程语言:Redis是用C语言编写的,更接近底层,能够直接调用os函数。
  2. 基于内存:由于Redis的数据都是放在内存的,因此更快。若是放在硬盘,性能要看两个指标:寻址(转速),吞吐。寻址是毫秒级别的,通常来讲,吞吐在500M左右,就算服务器性能再牛逼,也不会有几个G的吞吐,而放在内存,是纳秒级别的。
  3. 单线程:由于Redis是单线程的,因此避免了线程切换的消耗,也不会有竞争,因此更快。
  4. 网络模型:因为Redis的网络模型是epoll,是多路复用的网络模型。(关于epoll后面会展开讨论)
  5. Redis数据结构的优化:Redis中提供了5种数据类型,其中zset用跳表作了优化,并且整个Redis其实也都用hash作了优化,使其的时间成本是O(1),查找更快。
  6. Redis6.0推出了I/O Threads,因此更快。(关于I/O Threads后面会展开讨论)

Redis有什么缺点

这就是一个开放式的问题了,有不少答案,好比:

  1. 由于Redis是单线程的,因此没法发挥出多核CPU的优点。
  2. 由于Redis是单线程的,一旦执行了一个复杂的命令,后面全部的命令都被堵在门外了。
  3. 没法作到对hash中的某一项添加过时时间。

Redis为何能够保证原子性

由于Redis是单线程的,因此同时只能处理一个读写请求,因此能够保证原子性。

Redis是单线程的,到底该如何解释

咱们一直在强调Redis是单线程的,Redis是单线程的,可是Redis真的彻底是单线程的吗?其实否则,咱们说的Redis是单线程的,只是Redis的读写是单线程的,也就是work thread只有一个。

什么是I/O Threads

I/O Threads是Redis 6.0推出的新特性,在之前Redis从socket拿到请求、处理、把结果写到socket是串行化的,即:

图片.png

而Redis6.0推出了I/O Threads后:

图片.png
能够看到I/O Thread有多个,I/O Thread负责从socket读数据和写数据到socket,work thread在处理数据的同时,其余I/O Thread能够再从socke读数据,先准备好,等work thread忙完手中的事情了,立马能够处理下个请求。 可是work thread只有一个,这点要牢记。

什么是epoll

epoll是一种多路复用IO模型,在说epoll以前,不得不说下传统的IO模型,传统的IO模型是同步阻塞的,什么意思呢?就是服务端创建的socket会死死的等待客户端的链接,等客户端链接上去了,又会死死的等待客户端的写请求,一个服务端只能为一个客户端服务。

后来,程序员们发现能够用多线程来解决这个问题:

  1. 当第一个客户端链接到服务端后,服务端会启动第一个线程,之后第一个客户端和服务端的交互就在第一个线程中进行。
  2. 当第二个客户端链接到服务端后,服务端又会启动第二个线程,之后第二个客户端和服务端的交互就在第二个线程中进行。
  3. 当第三个客户端链接到服务端后,服务端又会启动第三个线程,之后第三个客户端和服务端的交互就在第三个线程中进行。

看起来,很美好,一个服务端能够为N个客户端服务,可是总不能无限开线程把 ,在Java中,线程是有本身的独立栈的,一个线程至少消耗1M,并且无限开线程,CPU也会受不鸟啊。

虽而后面还经历了好几个时代才慢慢来到了epoll的时代,可是我做为一个curd boy,api boy就不去研究的那么深了,如今咱们跨过中间的时代,直接来到epoll的时代吧。

咱们先来认识下epoll的方法,在linux中,能够用man来看看OS函数:

man epoll
复制代码

在介绍中有这么一段话:

*  epoll_create(2) creates a new epoll instance and returns a file descriptor referring to that instance.  (The more recent epoll_create1(2) extends
          the functionality of epoll_create(2).)

       *  Interest in particular file descriptors is then registered via epoll_ctl(2).  The set of  file  descriptors  currently  registered  on  an  epoll
          instance is sometimes called an epoll set.

       *  epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.
复制代码

虽然我英语实在是烂,可是借助翻译,仍是能够勉强看懂一些,大概的意思是:

  1. epoll_create建立了一个epoll示例,而且会返回一个文件描述符。
  2. epoll_ctl用于注册感兴趣的事件。
  3. epoll_wait用于等待IO事件,若是当前没有感兴趣的IO事件,则阻塞,言外之意就是若是发生了感兴趣的事件,这个方法便会返回。

下面还给出了一个demo,咱们来试着看下:

epollfd = epoll_create1(0);//建立一个epoll实例,返回一个 epoll文件描述符
           if (epollfd == -1) {
               perror("epoll_create1");
               exit(EXIT_FAILURE);
           }

           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;
            // 注册感兴趣的事件
            // 第一个参数为epoll文件描述符
            // 第二个参数为动做,如今是要添加感兴趣的事件
            // 第三个参数为被监听的文件描述符
            // 第四个参数告诉内核须要监听什么事件
            // 如今监听的事件是EPOLLIN,表示对应的文件描述符上有可读数据
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }
           // 一个死循环
           for (;;) {
               //  等待IO事件的发生
               //  第一个参数是epoll文件描述符
               //  第二个参数是发生的事件集合
               //  第三个参数不重要
               //  第四个参数是等待时间,-1为永远等待
               //  返回值是发生的事件的个数
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_wait");
                   exit(EXIT_FAILURE);
               }
               // 循环 
               for (n = 0; n < nfds; ++n) {
                    // 若是发生的事件对应的文件描述符是listen_sock
                   if (events[n].data.fd == listen_sock) {
                       // 创建链接 
                       conn_sock = accept(listen_sock,
                                          (struct sockaddr *) &addr, &addrlen);
                       if (conn_sock == -1) {
                           perror("accept");
                           exit(EXIT_FAILURE);
                       }
                       setnonblocking(conn_sock);// 设置非阻塞
                       ev.events = EPOLLIN | EPOLLET;// 设置感兴趣的事件
                       ev.data.fd = conn_sock;
                       // 添加感兴趣的事件,为 EPOLLIN或者EPOLLET
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                                   &ev) == -1) {
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       do_use_fd(events[n].data.fd);
                   }
               }
           }

复制代码

因为本人没有学习过C语言,有些注释的不对的地方请多担待,可是自认为大概就是这么个意思。

若是学过Java中的NIO的话,就会发现这个模式和Java中的NIO很像,那是由于Java中的NIO最终调用的就是OS的epoll函数。

epoll究竟是个什么鬼呢,说的简单点,就是告诉内核我对哪些事件感兴趣,内核就会帮你监听,当发生了你感兴趣的事件后,内核就会主动通知你。

这有什么优势呢:

  1. 减小用户态和内核态的切换。
  2. 基于事件就绪通知方式:内核主动通知你,不牢你费心去轮询、去判断。
  3. 文件描述符几乎没有上限:你想和几个客户端交互就和几个客户端交互,一个线程能够监听N个客户端,而且完成交互。

epoll函数是基于OS的,在windows里面,没有epoll这东西。

好了,关于epoll的介绍就到这里了,又出现了三个新名词:用户态、内核态、文件描述符,就先不解释了,之后写NIO的博客再说吧。那时候,会更详细的介绍epoll。

Redis的过时策略

通常来讲,经常使用的过时策略有三种:

  1. 定时删除:须要给每一个key添加一个定时器,一到期就移除数据。优势是很是精确,缺点是消耗比较大。
  2. 按期删除:每隔一段时间,就会扫描必定数量的key,发现过时的,就移除数据。优势是消耗比较小,缺点是过时的key没法被及时移除。
  3. 懒删除:使用某个key的时候,先判断下这个key是否过时,过时则删除。优势是消耗小,缺点是过时的key没法被及时移除,只有使用到了,才会被移除。

Redis使用的是按期删除+懒删除的策略。

管道

若是咱们有好多命令要交给Redis,第一个方案是一条一条发,缺点不言而喻:每条命令都须要通过网络,性能比较低下,第二个方案就是用管道。 在介绍管道以前,先要演示一个东西:

[root@localhost ~]# nc localhost 6379
set codebear hello
+OK
get codebear
$5
hello
复制代码

咱们往Redis发送命令,不必定必需要用Redis的客户端,只要链接上Redis服务器的端口就能够了,至于get codebear命令后面输出了$5是什么意思,就不在这里讨论了。

管道到底怎么使用呢,有了上面的基础,其实也很简单:

[root@localhost ~]# echo -e "set codebear hello1234 \n incr inttest \n set haha haha" | nc localhost 6379
+OK
:1
+OK
复制代码

把命令与命令之间用\n分割,而后经过nc发送给Redis。

咱们再来康康是否成功了:

[root@localhost ~]# nc localhost 6379
get inttest
$1
1
get codebear
$9
hello1234
get haha
$4
haha
复制代码

须要注意的,虽然多条命令是一块儿发送出去的,可是总体不具备原子性。 各大操做Redis的组件也提供了管道发送的方法,若是下次在项目中须要发送多个命令不妨试下。

发布订阅

当咱们有个消息须要以广播的形式推送给各个系统,除了采用消息队列的方式,还能够采用发布与订阅的方式,在Redis中就提供了发布订阅的功能,咱们来看下如何使用。

首先,咱们要建立一个订阅者,订阅名称为hello的channel:

localhost:6379> subscribe hello
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hello"
3) (integer) 1
复制代码

而后,要建立一个发布者,往名称为hello的channel发送消息:

localhost:6379> publish hello goodmorning
(integer) 1
复制代码

最后,再回到订阅者,发现接收到了消息:

1) "message"
2) "hello"
3) "goodmorning"
复制代码

可是须要注意,若是先发布消息,订阅者再去订阅,是收不到历史消息的。

是否是特别简单,在我还不知道有ZooKeeper的时候,我以为能够用Redis的发布订阅功能来作配置中心。

内存淘汰

若是Redis内存满了,再也容纳不下新数据了,就会触发Redis的内存淘汰策略,在redis.conf有一个配置,就是用来配置具体的内存淘汰策略的:

maxmemory-policy volatile-lru
复制代码

它有好几个配置,在讲具体的配置前,要先说两个名词,若是这两个名词不了解的话,那么每一个配置的含义真是只能死记硬背了。

  • LRU:最少使用淘汰算法:若是这个key不多被使用,被淘汰
  • LFU:最近不使用淘汰算法:若是这个key最近没有被使用过,被淘汰

下面就是具体的配置了,咱们一一来看:

  • volatile-lru:在设置了过时时间的key中,移除最近最少使用的key
  • allkeys-lru:在全部的key中,移除最近最少使用的key
  • volatile-lfu:在设置了过时时间的key中,移除最近不使用的key
  • allkeys-lfu:在全部的key中,移除最近不使用的key
  • volatile-random:在设置了过时时间的key中,随机移除一个key
  • allkeys-random:随机移除一个key
  • volatile-ttl:在设置了过时时间的键空间中,具备更早过时时间的key优先移除
  • noeviction:神马也不干,直接抛出异常

在生产环境中,到底应该使用哪一个配置呢? 能够说网上的答案千差万别,可是能够统一的是通常不会选择noeviction,因此这个问题仍是用万金油的答案,一个彻底正确的废话答案:看场景。

本篇博客到这里就结束了,还有不少东西没有提到,先抛开主从、集群,光单机版的Redis就还有持久化、事务、协议、modules、GEO、hyperLogLog等等,还有Redis的延伸问题——缓存击穿、缓存雪崩、缓存穿透等等问题,都没有提到,等之后再和你们唠唠嗑把。

相关文章
相关标签/搜索