1.1缓存中间件,Memacache 和Redis的区别:
1.Memacache: 代码层次类似Hash。
Memacache的优点在于它非常简单易用,并且它跟Hash是非常类似的。是可以通过Hash这个数据结构来实现的
1.支持简单的数据类型
2. 不支持数据持久化存储,一旦服务器宕机之后,数据没有办法保存下来的
3. 不支持Mysql一样的主从同步
4.不支持分片,将数据分布到多个物理节点上的一个分区方案。
Redis:
1.数据类型非常丰富
2.支持数据磁盘持久化存储
3.支持主从
4.支持分片
2.为什么Redis能这么快?
100000+QPS(QPS即query per second 每秒内查询次数)
1.完全基于内存,绝大部分请求时纯粹的内存操作,执行效率高(Redis采用的是单进程单线程的KV数据库,由C语言编 写,讲数据储存在内存里面,读写的时候不会受到硬盘IO速度的限制)
2.数据结构简单,对数据操作也简单
3.采用单线程,单线程也能处理高并发请求,想多核也可启动多实例。
4.使用多路I/O复用模型,非阻塞IO
3.多路I/O复用模型:
FD:File Descriptor 文件描述符
一个打开的文件通过唯一的m描述符进行引用,该描述符是打开文件的元数据到文件本身的映射。在linux内核中,这个描述 符称为文件描述符
传统的阻塞I/O模型:
图2
Select系统调用:
Select可以这个方法能同时监控多个文件描述符的可读可写情况,当其中的某些文件文件描述符可读或者可写时,Select就 会返回可读或者可写的文件描述符个数
图3
Selector是监听这些文件是否是可读或者可写的,那么监听的任务交给Selector之后呢,程序又可以继续去做别的事情,而 不会堵塞了
Redis采用的I/O多路复用函数:epoll/kqueue/evport/select ?
epoll、kqueue、evport相比select性能是更加优秀的,同时也能支撑更多的服务。
4.这么多的多路复用函数,redis究竟采用哪个呢?
1.因为Redis需要在多个平台上运行,同时为了最大化的提高效率和性能,根据编译平台的不同,选择不同的多路复用函 数作为子模块,提供给上层统一的接口
2.优先选择时间复杂度为O(1)的I/O多路复用函数作为底层实现。
3.以时间复杂度为O(n)的select作为保底。一旦当前系统编译环境没有上述几个比select性能更好的函数,就会选择 select作为备选方案,由于其在使用时会扫描全部监听的描述符,所以其时间复杂度较差,通常呢是>O(n)
4.基于react设计模式监听I/O事件
1.2说说你用过的Redis的数据类型
1.2.1供用户使用的数据类型
String:最基本的数据类型,二进制安全,它的值最大能存储512M,Redis的String可以包含任何数据,如JPG图片,或者 序列化的对象
图4
Redis的单步操作都是原子性的,原子性的意思,一个事务是一个不可分割的最小单位,事务中的的诸多操作,要么都 做,要么都不做,redis的原子性,使得我们不用考虑并发问题,可以方便的利用原子性自身操作,incr来实现简单的技 术功能。结合上面的incr能够满足计数器的需要,比如由这样的一个场景,我们有一个网站,想要去记录用户每天访问 这个网站的次数,此时web应用只需要通过拼接用户ID和当前日期的字符串作为key,每次用户访问。对这个key执行一 下incr命令,命令:incr userId180903,就能够统计出他访问网站的次数了。
Redis的String是可以包含任何数据的,例如JPG的图片,或者序列化的对象等等,那么这个String类型之所以支持如此 多的数据类型,是离不开其底层的简单动态字符串,
图5
Hash:String元素组成的字典,适用于存储对象
创建一个名为李雷的映射表命令:hmset lilei name "Li Lei" age 26 title "Senior"
获取这个映射表的字段:hget lilei age
heget lilei title
修改一个字段:hset lilei title "sss"
List:列表,按照String元素插入顺序排序
往列表插入元素:lpush mylist aaa
lpush mylist bbb
每往列表里面push一个元素,它都会返回一个当前列表的SIZE,即元素的数量
取从第0个到第10个元素:lrange mylist 0 10
Set:String元素组成的无序集合,通过Hash表实现,不允许重复,
添加:sadd myset 111
添加重复元素就会返回0
查看所有元素:smembers myset
Sorted Set:通过分数来为集合中的成员进行从小到大的排序
添加: zadd myzset 3 adc
zadd myzset 2 aaa
zadd myzset 1 bbb
查看所有:zrangebyscore myzset 0 10
更高级的Redis类型:
HyperLogLog
用于支持存储地理位置信息的Geo
1.2.2底层数据类型基础:
1.简单动态字符串
2.链表
3.字典
4.跳跃表
5.整数集合
6.压缩列表
7.对象
1.2.3从海量的key里查询出某一固定前缀的Key
1.摸清数据规模,即问清楚边界
一定要问清数据量有多大,再结合实际去作答。
答:使用 KEYS pattern: 查找所有复合给定模式的key
此时这样问答,大概率会进入面试官的伏击圈:该Redis正在给线上的业务提供服务,使用Keys指令会有什么问题,
命令(事先已经在Redis里添加了两千万个key):
查看大小:dbsize
查找以k1打头的key:keys k1* 此时Redis卡住了,也就是说当key非常多的时候,对内存的消耗,和Redis的服务器,都 是一个隐患
Keys的缺点:
1.一次性要返回所有匹配的key
2.key的数量过大,会使服务卡顿
SCAN指令:这时候我们就可以使用SCAN指令,SCAN指令可以无阻塞的提取出指令模式的key列表,SCAN每次执行,只会返 回少量元素,所以可以用于生产环境,而不会出现像Keys命令可能带来的会阻塞服务器问题:
SCAN cursor(游标) [MATH pattern] [COUNT count]
1.基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。
2.以0作为游标开始依次新的迭代,直到命令返回游标0完成依次遍历
3.不保证每次执行都返回某个给定数量的元素,支持模糊查询
4.依次返回的数量不可控,只能是大概率符合count参数
命令:scan 0 match k1* count 10 (开始迭代,返回前缀为k1的key,并让其一次返回10个,传curros为0,就表示我们 刚开始进行迭代)
将获取到的cursor作为下一次cursor再次迭代:scan 11534336 match k1* count 10
获取到的cursor并不是递增的,有可能后一次比前一次还小,这就可能会遇到重复key的问题
1.2.4如何通过Redis实现分布式锁:
1.分布式锁需要解决的问题:
1.互斥性(任何时刻只能有一个客户端获取到锁,不能有两个客户端获取到锁)
2.安全性(锁只能被持有该锁的客户端删除,不能由其他客户端删除)
3.死锁(获取锁的客户端因为某些原因宕机,未释放锁,其他客户端再也无法获取到该锁,而导致了死锁。此时需要有机制避免这个问题的发生)
4.容错(当部分节点,如一些Redis节点等宕机的时候,客户端仍能获取锁和释放锁 )
2.如何通过Redis实现分布式锁
1.SETNX key value :如果key不存在,则创建并赋值
1.时间复杂度O(1),因此事非常高效的
2.返回值:设置成功,(set成功,如果key不存在,就返回1,如果存在就返回0)返回1,设置失败,返回0
命令:get locknx 返回nil
setnx locknx test 返回1,表示成功
setnx locknx task 返回0,表示失败
get locknx 返回“test” 表示locknx的值并未被修改
正因为setnx有上述功能,并且操作时原子性的,所以初期被用来实现分布式锁
咱们可以在执行某段代码逻辑的时候,尝试使用setnx对某个key设值,如果设值成功,则表示此时没有别的线程在执 行该段代码,或者说占用该独占资源,这时候我们的线程就可以顺利的执行该段代码逻辑,如果设置失败,则证明此 时有别的程序或线程在占用该资源,而当前线程就需要等待,直至setnx成功,这里有的同学就会问了,一旦setnx某 个key成功,那么该key就会长久存在,后续线程如何能再次获得到锁,其实很简单,接下来我们还需要另外一步,即 给该key设定一个过期时间,既然咱们的setnx并不支持传入EXPIRE参数,此时我们想到的就是EXOIRE指令了
EXPIRE key seconds:
1.设置key的生存时间,当key过期时(生存时间为0),会被自动删除
设置一个超时时间()2秒,让它在指定时间释放掉key
命令: expire locknx 2
两秒之后,执行命令:setnx locknx task (执行成功)
获取locknx命令: get locknx
2.我们可以在程序里面使用这个方案实现一个分布式锁,那么此方案的伪代码的逻辑如下:
RedisService redisService = SpringUtils.getBean(RedisService.class)
long status = redisService.setnx(key,"1")
if(status == 1){
redisService.expire(key,expire)
//执行独占资源逻辑
do0cuppieWork()
}
先执行setnx这个key,然后判断status是否成功,如果成功,则设置好key的过期时间,执行独占资源的代码段。之后 有别的线程想执行的时候,发现setnx的status为0,为0就代表了有别的线程已经占据这个资源了,不能执行 do0cuppiedWork()了,那么当前线程会阻塞,直到获取到status为1为止.
当然你也可以设置等待时间,就循环时候,每次sleep一个固定的时间,再去执行setnx,看看Status是否为1,为1才 去执行里面的代码逻辑,然而这种方式并不能让人满意,
3. 这段程序会有什么风险?
其实我们仔细看一下就会知道,假设我们这个Setnx执行成功之后,就直接挂掉了,来不及expire,此时key就会被 一直占用着,这就意味着其他线程永远也执行不了独占资源的逻辑了,出现该问题,最主要的原因,就是违背了我 们的初衷,利用Redis操作的原子性,这里我们先调用setnx,再调用expire, 虽然两个操作都是原子的,组合起来 就不是了。之所以讲这样一个不好的解法,是因为想让大家知道原子性的重要性。希望大家能尽量利用好Redis的原 子性。
4.那有没有别的方式,将二者结合起来?
从Redis2.6.12版本开始,我们就可以使用set原子操作,将setnx和expire揉在一块,去执行,具体做法如下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX second:设置键的过期时间为second秒
PX millsecond:设置键的过期时间为millisecond毫秒
NX:只在键不存在时,才对键进行设置操作。效果等同于setnx
XX:只在键已经存在时,才对键进行设置操作,就跟setnx反着来
SET操作完成时,返回OK,否则返回nil
实验一下:
命令:
设置:set locktarget 12345 ex 10 nx (ex:有效时间,nx:key存在,就失败,key不存在就成功)
十秒之内,想改些数字,就会返回nil:set locktarget 1222355 ex 10 nx
过十秒之后,再执行,就返回ok:set locktarget 1222355 ex 10 nx
这样就满足了我们分布式锁的原子性需求。
于是我们可以在程序中实现类似下面的伪代码,实现分布式锁了,在java里面它跟我们的Redis是如出一辙的,这样 就能保证我们分布式锁的原子性了:
RedisService redisService = SpringUtils.getBean(RedisService.class)
Spring result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIT,SET_WITH_EXPIRE_TIME, expireTime)
if("OK".equals(result)){
//执行独占资源逻辑
do0cuppieWork()
}
3.大量key同时过期的注意事项:
1.如果大量的key需要设置同一时间过期,需要注意什么?
如果大量的key设置的过期时间过于集中,在过期那个时间点,因为删除key是需要时间的,因此呢,可能因为出现 大量的批量的key出现卡顿现像,面对这种情况,解决方法非常简单,即,一般需要在时间上,加一个随机值,使得 过期时间分散一些,这样就能在最大程度上,避免卡顿现像的发生,
解决方案:在设置key的过期时间,给每个key加上随机值。
3.如何使用Redis来做异步队列
使用List作为队列,RPUSH生产消息,LPOP消费消息,来实现一个队列
命令:rpush testlist aaa
当队列有消息,我们通过lpop逐条弹出消息,来模拟对消息的消费:
命令:lpop testlist
缺点:lpop是不会等待队列里有值才去消费的。
弥补:此时可以在应用层编写sleep逻辑,适当的sleep一会儿,再通过lpop去重试,进而实现一个简单的异步队列.
4.此时如果面试官继续追问: 我不想通过sleep方式去重试,有什么方法?
BLPOP key [key ...] timeout:阻塞直到队列有消息或者超时
BLPOP能替代sleep做更精准的控制。
命令:blpop testlist 30 (等待30秒,如果在另一个客户端执行:rpush testlist aaa,就会弹出消息)
缺点:只能供一个消费者消费,lpop或者BLPOP之后就没了。
问题:能否生产一次,让多个消费者消费?
使用pub/sub:主题订阅者模式,可以实现一对多的消费队列
1.发送者(pub)发送消息,订阅者(sub)接受消息
2.订阅者可以订阅任意数量的频道
图8
那么下图展示了pub,以及订阅这个频道的三个客户端消费者Subscrlber的关系,当有游戏通过publish指令发送给频 道Topic的时候呢,这个消息就会被发送给订阅它的三个客户端。
演示发布订阅:
1.首先在客户端订阅一个叫myTopic的频道,这个频道不需要被创建,只需要订阅即可
命令:subscribe myTopic
2.订阅好之后,再打开另外一个客户端订阅同样的频道
命令:subscribe myTopic
3.之后我们再打开一个新的客户端,让这个客户端监听另外一个频道,
命令:subscribe annotherTopic
4.我们再新开启一个redis客户端,然后在同一个频道myTopic发布两次消息,这样我们前两个订阅myTopic的客户 端都会收到消息了。
命令:publish myTopic “Hello”
publish myTopic “I LOVE YOU”
结论:两个监听了myTopic频道的都接受到了消息,监听另外一个频道的,未收到消息,这除了证明Redis pub/sub能 实现生产一条消息供N个消费者消费,也证明了通过订阅的方式可以让消费者只获取到关心的并且消费的数据
缺点:消息的发布是无状态的,无法保证该消息发布完之后被接收到,是否在传输过程中丢失,对于发布者来说,消 息是即发即失的,此时某个消费者在发布消息的时候下线,重新上线之后呢,是接收不到该消息的,要解决这个问 题,就得使用专门的消息队列kafka来解决了
4.Redis如何做持久化
一旦服务器的进程退出,数据库的数据就会丢失,为了解决这个问题,Redis主要提供了三种持久化的方案将内存中的数 据保存到磁盘中,避免数据丢失。
1.RDB(快照)持久化:保存某个时间点的全量数据快照,RDB持久化会在一个特定的间隔,保存那个时间点的全量数 据的快照。
图9:redis.conf
save 900 1 : 900秒内有一条是写入指令,就出发一次快照,就可以理解为进行一次备份
save 300 10: 300秒内有10条写入
save 60 10000:60秒内10000条写入
根据Redis业务需求,可以合理的配置
stop - wrutes -on -bgsave-error yes :设置成yes,就表示当备份进程出错的时候,主进程就停止新的写入操作了,这样 做是为了保护持久化数据一致性的问题,如果自己的业务有完善的监控系统,就可以禁止此项配置,否则请开启
rdbcompression yes:顾名思义,和rdb文件压缩相关的。设置成yes,表示在备份文件的时候,将rdb文件压缩后再做 持久化保存,建议将其设置为no,毕竟Redis本身就属于cpu密集型服务器,开启压缩会带来更多的cpu消耗,相比硬盘 成本,cpu更值钱
将rdb的配置禁用,save ""
在src目录下,会有一个rdb文件,打开rdb会是一堆乱码,表示其是一个二进制文件
2.rdb的创建与载入
rdb文件可以通过两个命令来生成:
1.SAVE:阻塞Redis的服务器进程,直到RDB文件被创建完毕,SAVE很少被使用,因为SAVE操作是在主线程中保存快照的,由于Redis是用一个主线程处理所有请求,这种方式会阻塞所有client请求
2.BGSAVE(重要):Fork出一个子进程来创建RDB文件记录BGSAVE当时的数据库状态,父进程继续处理接收到的命令,子进程完成文件创建后,会发送信号 给父进程,父进程处理命令的同时,通过轮询处理子进程的信号,BGSAVE命令,是用后台保存RDB文件,调用此命令后会立刻返回OK,redis会产生一个子进程进行处理,并立刻恢复对客户端的服务,在客户端,我们可以使用lastsave 指令查看操作是否成功,lastsave指令记录上一次save或BGSAVE的时间,不阻塞服务器进程(save指令完成之后,Rdb文件就会重新生成,服务就会恢复正常,此时推出去就会发现rdb又被重新生成了,紧接着咱们再次删除dum.rdb文件,ls dump.rdb,,就发现没有这个文件了,再次连接Redis客户端,./redis-cli,我们首先执行以下lastsave这个指令,返回一串数字这串数字就是我们上次执行save的时间,接下来再次lastsave ,还是这串数字,咱们直接执行bgsave,执行之后发现客户端并未被卡顿,执行lastsave,还是上次这个时间,等待一段时间之后,再执行lastsave,就发现时间已经发生了变化,表明bgsave已保存成功,退出,然后查找一下dump.rdb,就发现又再次生成了,大家可以通过Java计时器,定时调集redis bgsave指令,备份rdb文件,并按时间戳存储不同的rdb文件,作为redis某段时间的全量备份脚本。)
命令:save
命令:exit
命令:ls dump.rdb
命令:rm -f dump.rdb
命令:lastsave
命令:bgsave
命令:lastsave
命令:lastsave(等待一段时间)
命令:mv dump.rdb dumpxxxx.rdb 基于某个时间点的全量数据备份文件
通过这种方式可以定期保存某个时间的全量备份
3.自动触发RDB持久化方式
1.根据redis.conf配置里的SAVE m n 定时触发,(用的是BGSAVE)
2.主从复制时,主节点自动触发
3.执行Debug Reload
4.执行Shutdown且没有开启AOF持久化
BGSAVE原理:图11
Copy-On-Write(写实复制):如果又多个调用者同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变
写实复制时计算机领域的一种优化策略,其核心思想是,如果有多个调用者同时请求相同资源,如内存或磁盘上的数据存储,它们会获取相同的指针,指向相同的资源, 直到某个调用者试图修改资源的内容时。系统才会复制一份专用副本,给该调用者,而其他调用者所见到的最初资源
仍然保持不变。 这个过程对其他的调用者是透明的,主要的优点是,如果调用者不修改该资源就不会有副本被创建,因此,多个调用者只是读取操作时可以共享同一份资源。COW处理的过程中,需要维持一个为读请求使用的指针,并在新数据在写入完成后,更新这个指针,以提升读写并发能力,COW也间接提升了数据更新过程中的原子性。保证数据完整性的同时,还保证了一定的读写效率。当Redis需要做持久化时,会fork一个子进程,将数据写到磁盘 中一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB给替换掉,好处就是可以实现copy-on-write,子进程继续接收其他请求,确保了Redis的性能。父进程继续处理client请求,子进程将内存内容写入到内存中,由于COW机制,父子进程会共享相同的物理页面,当父进程处理写请求时候,OS会为父进程要修改的叶面创建副本,而不是写共享的叶面,所以子进程的地址空间内的数据是fork时刻的整个数据库的快照,当子进程完成了临时文件的写入,用临时文件替换掉了原来的快照文件,然后子进程退出,进而完成一次备份操作。RDB文件的载入一般情况下是自动的,Redis服务器在启动时如果监测到RDB文件的存在,会自动载入这个文件。
4.RDB持久化
缺点:
1.内存数据的全量同步,数据量大会由于IO而严重影响性能
2.可能会因为Redis挂掉而丢失从当前至最近一次快照期间的全部数据,如果应用不能丢失任何修改,可以采用另一种持久化方式AOF
5.AOF
AOF(Append-Only-File)持久化:保存写状态,与RDB持久化不同,AOF是通过保存Redis服务器所执行的写状态来记 录数据库的
1.RDB相当于备份数据库状态,AOF相当于备份数据库接收到的指令,所有写入AOF的命令,都是以Redis协议来保存 的。
2.在AOF持久化的文件中,数据库会记录下所有变更数据库的命令,除了指定数据库的查询命令,其他的命令都是来 自于client的,这些命令会以追加,即append的形式追加保存到AOF文件中(增量)
AOF默认是关闭的,咱们可以修改redis.conf配置来让Append-Only来生效
appendonly no:修改为yes
appendfsync everysec:类似于RDB SAVE,主要是用来配置AOF的写入方式,有三个参数分别是
always:一旦缓存区内容发生变化,总是及时的将缓存内容写入到aof当中
everysec:将缓存内容每隔一秒就写入到aof里面
no:将写入aof的操作交由操作系统来决定,一般而言,为了提高效率,操作系统会等待缓存区被填满,才会开 始同步数据到磁盘当中,一般推荐每秒appendfsync一次,即默认方式,因为它的速度比较快,安全性也不错
修改之后需要重启redis.
命令:config set appendonly yes
命令:set aofTest "hehe"
命令:exit
src:ls appendonly.aof
5.AOF日志:
aof日志文件是一个纯追加的日志文件,就怕是遇到突然停电的情况,也能够尽最大的权利保证数据的无损,随着写操作的不断增加,aof文件会越来愈大,例如,你去递增一个计数器一百次,那么其实我们只保留最终结果100即可,但是aof会把这一百次记录完整保留下来,而事实上,要恢复这个记录,我们只需要一个命令就行,也就是说aof里面那一百条命令,可精简为1条,所以Redis支持这样一个功能,在不中断服务的情况下,后台重建aof文件,重写的原理如下, 同样用到了copy-on write:
1.Redis首先会调用fork(),创建一个子进程
2.子进程把新的AOF写到一个 临时文件里,新的aof的重写是直接把当前内存的数据生成对应的命令
3.主进程持续将新的变动同时写道内存和原来的AOF里
4.主进程获取子进程重写AOF完成信号,往新的AOF同步增量变动
5.使用新的AOF文件替换掉旧的AOF文件
6.RDB和AOF文件共存情况下恢复流程
图12
RDB有点:全量数据快照,文件小,恢复快
RDB缺点:无法保存最近一次快照之后的数据
AOF优点:可读性高,适合保存增量数据,数据不易丢失
AOF缺点:文件体积大,恢复时间长
7.RDB-AOF混合持久方式
图13
1.BGSAVE做镜像全量持久化,AOF做增量持久化
8.使用Pipeline的好处
1.Pipeline和linux的管道类似
2.Redis基于请求/响应模型,单个请求处理需要一一应答
3.Pipeline批量执行指令,节省多次IO往返时间
4.有顺序依赖的指令建议分批发送
9.Redis同步机制
主从同步原理: