Redis全解

Q:Redis是啥?

A:redis是当前非常热门的非关系型(NoSql)数据库,它以key-value的形式进行存取操作,基于内存的操作特性让他较传统DB拥有非常强大的性能优势!

Q:为什么你们会选择它?

A:在当前高并发的大背景下,传统数据库已经不能够支持我们业务的需求,按照我们现在的访问量,如果直接打到数据库,那会直接将数据库打宕机,给用户不好的体验,所以我们必须引入一个缓存中间件来解决这一瓶颈。

目前市面上比较常用的缓存中间件有Redis 和 Memcached 不过中和考虑了他们的优缺点,最后选择了Redis。

Q:哦?那说说他们两个的区别,为什么最终选择了redis?

A:我们先来看看Memcached的特性:

  • Memcached处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
  • Memcached功能简单,使用内存存储数据;
  • Memcached对缓存的数据可以设置失效期,过期后的数据会被清除;
  • 失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
  • 当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU (Least Recently Used,最近最少使用)策略对数据进行剔除。

另外,使用 Memcached有一些限制:

  • key 不能超过 250 个字节,大于该长度无法存储;
  • value 不能超过 1M 字节,超过1MB数据不予存储;
  • key 的最大失效时间是 30 天,设置为永久的也会在这个时间过期;
  • 只支持 K-V 结构,不提供持久化和主从同步功能。

他们之间存在的差异在于:

  • 与 Memcached不同,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
  • Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
  • 相比 Memcached,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
  • Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。

综上,由于redis的这些优势,以及Memcached自带的一些弊端,权衡之下,我们最终选择了redis来作为我们的缓存中间件。

Q:你是说,MC使用的是多线程的异步IO,redis使用的是单线程的IO多路复用,那这两种IO方式有什么区别吗?

A:好的,我们先说说IO多路复用:

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
在这里插入图片描述
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。

异步IO:

在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

异步IO模型使用了Proactor设计模式实现了这一机制。

在这里插入图片描述
异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。

Q:都说到这儿了,那你开始说说redis的特点吧!

A:好的。

  • redis是基于内存操作的,官方称它能够支撑10w的QPS正是因为内存的相应的速度达到0.00001秒。
  • redis能存储丰富的数据结构,常见有五种数据类型:String、List、Set、ZSet(Sorted Set)、Map
  • Redis 是单线程的,所有 Redis 的操作都是原子性的,所以能够在多线程的环境下使用。
Q:日常开发中redis各数据类型使用场景?

A:

  • String:计数器、登录的Session

  • Set:【∩、∪、补集、差集】粉丝关注(张三关注李四,王五也关注李四,那么张三关注的人和王五关注的人取交集中就有李四)

  • List:秒杀队列(多个任务执行队列:成功队列,失败队列)

  • Sort Set:排行榜(根据权重来排序)

  • Hash:涉及key的归类操作(也常被使用当作缓存的数据结果)

Q:你说redis是单线程的,那为什么他还能这么快?

A:

  • 首先还是因为他是直接操作内存的,所以读取速度很快;
  • 它使用了IO多路复用;
  • 由于他是单线程,所以没有线程之间的资源竞争和上下文切换,不会浪费CPU资源;
Q: 资源竞争我了解,你能说说啥叫上下文切换?

A:CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。

Q:了解,那你说他是单线程的,我们现在服务器都是多核的,岂不是会浪费资源?

A:emmm…是的,人家是单线程的,但是,你可以通过在单机开多个Redis实例嘛。

Q:既然提到了单机会有瓶颈,那你们是怎么解决这个瓶颈的?

A:我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node。

这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。

Q:哦?那问题就来了,他们之间是怎么进行数据交互的?主从之间的数据怎么同步?

A:我先说下为啥要用主从这样的架构模式,前面提到了单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。
在这里插入图片描述

Q:咳!跑题了,我只想知道他是怎么实现数据同步的?

A:别急嘛!在你启动一台从服务器slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。

至此slave就可以接受来自用户的读请求。Slave初始化后开始正常工作时,主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。以此来实现主从同步。

Q:你刚刚提到了RDB快照,那你对redis的持久化机制有了解吗?

A:redis有两种持久化机制,分别是:RDB和AOF。
RDB就是定时对redis中的所有数据进行全量备份保存;而AOF则类似于日志,将所有操作redis的命令进行保存。

效率上而言:RDB在保存数据时会fork一个子线程去执行持久化操作,主线程继续执行任务,所以他对redis性能影响不大;AOF他们通常设置为每秒执行一次保存(只要你愿意,你也每条命令都保存),所以对redis的性能还是会有影响的。而且RDB在数据恢复的时候速度也比AOF来的快。

安全性而言:RDB通常我们设置为五分钟甚至更久,来执行一次数据备份,这就意味着,如果突然的一次断电,可能会导致最近五分钟内的redis改动的数据丢失;而AOF我们采用每秒执行一次备份,所有我们最多也就丢失这一秒的数据!

在默认情况下,redis重启后会去读取AOF的备份数据,因为AOF的数据更全。我们可以设置两个备份同时进行保存,你单独用RDB你会丢失很多数据,你单独用AOF,你数据恢复没RDB来的快,真出什么时候第一时间用RDB恢复,然后AOF做数据补全,真香!冷备热备一起上,才是互联网时代一个高健壮性系统的王道。

Q:可以可以,我听你提到了高可用,Redis还有其他保证集群高可用的方式么?

A:还有哨兵集群sentinel。

主从切换技术的方法是:当主服务器宕机后,需要手动将另一台服务器切换为主服务器,这就需要人为的干涉,费时费力,而且会造成一段时间的服务器不可用,为了解决这个问题,这个时候就出现了哨兵模式!

哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用

在这里插入图片描述
这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

选主的策略简单来说有三个:

  • slave 的 priority 设置的越低,优先级越高;
  • 同等情况下,slave 复制的数据越多优先级越高;
  • 相同的条件下 runid 越小越容易被选中。

但是,一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

Q:你刚刚说到发布订阅者模式,你能说说什么是发布订阅者模式吗?

A:老哥。。。超纲了你不知道吗。。。
在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者),而是通过消息通道广播出去,让订阅改消息主题的订阅者消费到。

比如你打开你的微信订阅号,你订阅的作者发布的文章,会广播给每个订阅者。在这个场景里,作者就是一个Pulisher,微信公众号就是一个Pulisher/Subscriber Channel,而你就是一个Subscriber,你收到的文章就是一个Message。

特点
发布/订阅者模式最大的特点就是实现了松耦合,也就是说你可以让发布者发布消息、订阅者接受消息,而不是寻找一种方式把两个分离的系统连接在一起。当然这种松耦合也是发布/订阅者模式最大的缺点,因为需要中间的代理,增加了系统的复杂度。而且发布者无法实时知道发布的消息是否被每个订阅者接收到了,增加了系统的不确定性。

Q:假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?

A:使用keys指令可以模糊查询key列表。但是由于redis是单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

Q:你有没有考虑过,如果你多个系统同时操作(并发)Redis带来的数据问题?

A:这个问题我以前开发的时候遇到过,其实并发过程中确实会有这样的问题,比如下面这样的情况:
在这里插入图片描述
系统A、B、C三个系统,分别去操作Redis的同一个Key,本来顺序是1,2,3是正常的,但是因为系统A网络突然抖动了一下,B,C在他前面操作了Redis,这样数据不就错了么。

就好比下单,支付,退款三个顺序你变了,你先退款,再下单,再支付,那流程就会失败,那数据不就乱了?你订单还没生成你却支付,退款了?明显走不通了,这在线上是很恐怖的事情。

Q:那这种情况怎么解决呢?

A:我们可以找个管家帮我们管理好数据的嘛!
在这里插入图片描述
某个时刻,多个系统实例都去更新某个 key。可以基于 Zookeeper 实现分布式锁。每个系统通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。

你要写入缓存的数据,都是从 MySQL 里查出来的,都得写入 MySQL 中,写入 MySQL 中的时候必须保存一个时间戳,从 MySQL 查出来的时候,时间戳也查出来。

每次要写之前,先判断一下当前这个 Value 的时间戳是否比缓存里的 Value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。

Q:哦?你使用过Redis分布式锁么,它是什么回事?

A:先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

Q:如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

A:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

Q:缓存降级有了解吗?

A:当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

Q:感觉你掌握的都不错,最后给你提一个简单的问题吧!缓存雪崩、缓存击穿、缓存穿透怎么解决?

A:emmm…这尼玛也叫一个问题。。。
缓存雪崩:

  缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
  • 可以考虑用加锁来保证不会有大量的线程对数据库一次性进行读写,但会影响性能;
  • 设置过期时间时加上随机数,使key过期的时间尽量均匀;
  • 设置热点数据永不过期;
  • 缓存预热,如果查询不到就直接返回空;

缓存击穿:

  缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
  • 设置热点数据永不过期;
  • 缓存预热;

缓存穿透:

   缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
  • 对查询条件做基础判断;
  • 在redis中未查询到,且数据库也未查询到时,在redis中插入该key对应的value=0;
  • 布隆过滤器;
Q:???布隆过滤器???

A:emm。。。说好的最后一个??

布隆过滤器(Bloom Filter)可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

在这里插入图片描述
布隆过滤器配合redis使用,相当于在redis和DB之间加入一张过滤网,当一个key在redis中查询后没查到,就判断该key是否存在于布隆过滤器当中,如果存在于过滤器当中,那么说明该元素(可能)存在于数据库当中,可以打到数据库。如果布隆过滤器中不存在,那么该元素也一定不存在于数据库中,直接返回NULL即可!

Q:redis如何处理过期的key

A:分为定期删除和惰性删除。惰性删除是指当客户端访问某个key时,判断该key是否已过期,如果已经过期则返回空,并将该key从redis中删除。这样是不够的,因为有些过期的keys,永远不会访问他们。定期删除指定时的获取一些有过期时间的key,检查是否过期,并删除。

Q:但是在定期删除的随机选取没选到,且一直没被访问的key一直保存在redis中,导致redis内存即将耗尽,如何处理?

A:走redis的内存淘汰机制,即redis在内存占用过多的时候,会进行内存淘汰机制,有如下一些策略:

  • noeviction:当内存不足以容纳新写入的数据时,新写入操作会报错;
  • allkeys-lru:当内存不足以容纳新写入的数据时,移除所有键中最近最少使用的key;(通常使用)
  • allkeys-random:当内存不足以容纳新写入的数据时,随机移除所有键中某个key;
  • volatile-lru:当内存不足以容纳新写入的数据时,移除设置了过期时间的最近最少使用的key;
  • volatile-random:当内存不足以容纳新写入的数据时,移除设置了过期时间的随机某个key;
  • volatile-ttl:当内存不足以容纳新写入的数据时,移除设置了过期时间的最早过期时间的key;
Q:要不你手写一个LRU算法?

A:思路:使用LinkedHashMap模拟redis,每当一个key被使用后都将这个key移到队首,在删除的时候,就从队尾开始删!

public redis<K,V> extends LinkedHashMap<K,V>{
	//这里指的是redis能村饭多少数据
	private final int Rsize;
	//设置redis大小及map的大小
	public redis(int size){
		//设置map的初始大小及负载因子,true则表明最近访问的在对首
		super((int)Math.ceil(size/0.75)+1,0.75,true);
		Rsize=size;
	}
	
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值