Redis最常用来做缓存,是实现分布式缓存的首先中间件
Redis可以作为数据库,实现诸如点赞,关注,排行等对性能要求极高的互联网需求
Redis可以作为计算工具,能用很小的代价,统计诸如PV/PU,用户在线天数等数据
Redi还有很多其他的使用场景,诸如实现分布式锁,可以作为消息队列
Redis是一种基于键值对的NoSQL数据库,而键值对的值是由多种数据结构和算法组成的,Redis的数据都存储在内存中,因此它的速度惊人,读写性能可达10万/秒,远超关系型数据库。
关系型数据库是基于二维数据表来存储数据的,他的数据格式更为严谨,并支持关系查询。关系型数据库的数据存储于磁盘上,可以存放海量数据,但性能远不如Redis。
Redis支持5种核心的数据类型,分别是字符串、哈希、列表、集合、有序集合;
Redis还提供Bitmap、hyperLogLog、Geo类型,但这些类型都是基于上述核心数据类型实现的。
Redis在5.0新增加了Streams数据类型,它是一个功能强大,支持多播的,可持久化的消息队列
对服务端程序来说,线程切换和锁通常是性能杀手,而单线程避免了线程切换和竞争所产生的消耗。
Redis的大部分操作是在内存上完成的,这是它实现高性能的一个重要原因
Redis采用了IO多路复用机制,使其在网络IO操作中能并发的处理大量客户端的请求,实现高吞吐率
set
集合中的元素是无序、不可重复的,一个集合最多能存储232-1个元素
集合除了支持对元素的增删改查之外,还支持对多个集合取交集、并集、差集。
zset
有序集合保留了集合元素不可重复的特点
有序集合会给每个元素设置一个分数,并以此作为排序的依据
有序集合不能包含相同的集合,但是不同的元素的分数可以相同
很多时候,要确保事物中的数据没有被其他的客户端修改才执行该事务,Redis中提供了watch命令来解决这类问题,这是一种乐观锁的机制,客户端通过watch命令,要求服务器对一个或者多个key进行监视,如果在客户端执行事务之前,这些key发生了变化,则服务器将拒绝执行客户端提交的事务,并向他返回一个空值。
热点数据不设置过期时间,使其达到“物理” 上的永不过期,可以避免缓存击穿问题
在设置过期时,可以附加一个随机数,避免大量的key同时过期,导致缓存雪崩
实现Redis的高可用,主要有哨兵和集群两种方式
哨兵:
Redis Sentinel是一个分布式架构,它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点和其余的哨兵节点进行监控,当发现节点不可达时,会对节点做下线标识,如果被标识的是主节点,它就会与其他的哨兵节点进行协商,当多数哨兵节点都任务主节点不可达时,他们便会选举出一个哨兵节点来完成自动障碍转移的工作,同时还会将这个变化实时的通知给应用方,整个过程是自动的,不需要人工介入,有效的解决了Redis的高可用问题!
一组哨兵可以监控一个主节点,也可以同时监控多个主节点,两种情况的拓扑结构如下图:
哨兵节点包含如下的特征
哨兵节点会定期监控数据节点,其他哨兵节点是否可达
哨兵节点会将故障转移的结构通知给应用方
哨兵节点可以将从节点晋升为主节点,并维护后续正确的主从关系;
哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点的信息;
节点的故障判断是有多个哨兵节点共同完成的,可有效地防止误判;
哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用。整个集合依旧是健壮的;
哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令
集群
Redis集群采用虚拟槽分区来实现数据分片
虚拟槽分区具有以下特点:
解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景
Redis集群中数据的分片逻辑如下图:
Redis存的快是因为它的数据都存放在内存里,并且为了保证数据的安全性,Redis还提供了三种数据的持久化机制,即RDB持久化,AOF持久化,RDB-AOF混合持久化,若服务器断电,那么我们利用持久化文件,对数据进行恢复。理论上来说,AOF持久化,RDB-AOF混合持久化可以将丢失的数据的窗口控制在1s之内
Redis支持如下两种过期策略:
惰性删除:客户端访问一个key的时候,Redis会先检查它的过期时间,如果过期就立刻删除这个key
定期删除:Redis会将设置了过期时间的key放到一个独立的字典中,并对字典进行每秒10次的过期扫描
过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略,该策略的删除逻辑如下:
从过期字典中随机选择20个key;
删除这20个key中已过期的key;
如果已过期key的比例超过20%,则重复步骤1;
缓存穿透:
客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机。出现这种情况的原因,可能是业务层误将缓存和库中的数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。
解决方案:
缓存空对象:存储层未命中后,依然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回空值
布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值
缓存击穿:
一份热点数据,它的访问量非常大,在其缓存失效的瞬间,大量请求直达存储层,直至服务崩溃
解决方案:
永不过期:热点数据不设置过期时间
加互斥锁:对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。在这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。
缓存雪崩:
在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。
解决方案:
避免数据同时过期,设置过期时间时,附加一个随机数,避免大量的key同时过期。
启动降级和熔断措施:在发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。
构建高可用的Redis服务:采用哨兵或者集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用
四种同步策略:
想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:
先更新缓存,再更新数据库;
先更新数据库,再更新缓存;
先删除缓存,再更新数据库;
先更新数据库,再删除缓存。
从这4种同步策略中,我们需要作出比较的是:
更新缓存与删除缓存哪种方式更合适?
应该先操作数据库还是先操作缓存?
更新缓存还是删除缓存:
下面,我们来分析一下,应该采用更新缓存还是删除缓存的方式。
更新缓存
优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况。
缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。
删除缓存
优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除。
缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。
从上面的比较来看,一般情况下,删除缓存是更优的方案。
先操作数据库还是缓存:
下面,我们再来分析一下,应该先操作数据库还是先操作缓存。
首先,我们将先删除缓存与先更新数据库,在出现失败时进行一个对比:
如上图,是先删除缓存再更新数据库,在出现失败时可能出现的问题:
进程A删除缓存成功;
进程A更新数据库失败;
进程B从缓存中读取数据;
由于缓存被删,进程B无法从缓存中得到数据,进而从数据库读取数据;
进程B从数据库成功获取数据,然后将数据更新到了缓存。
最终,缓存和数据库的数据是一致的,但仍然是旧的数据。而我们的期望是二者数据一致,并且是新的数据。
如上图,是先更新数据库再删除缓存,在出现失败时可能出现的问题:
进程A更新数据库成功;
进程A删除缓存失败;
进程B读取缓存成功,由于缓存删除失败,所以进程B读取到的是旧的数据。
最终,缓存和数据库的数据是不一致的。
经过上面的比较,我们发现在出现失败的时候,是无法明确分辨出先删缓存和先更新数据库哪个方式更好,以为它们都存在问题。后面我们会进一步对这两种方式进行比较,但是在这里我们先探讨一下,上述场景出现的问题,应该如何解决呢?
实际上,无论上面我们采用哪种方式去同步缓存与数据库,在第二步出现失败的时候,都建议采用重试机制解决,因为最终我们是要解决掉这个错误的。而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行,如下图:
这里我们按照先更新数据库,再删除缓存的方式,来说明重试机制的主要步骤:
更新数据库成功;
删除缓存失败;
将此数据加入消息队列;
业务代码消费这条消息;
业务代码根据这条消息的内容,发起重试机制,即从缓存中删除这条记录。
好了,下面我们再将先删缓存与先更新数据库,在没有出现失败时进行对比:
如上图,是先删除缓存再更新数据库,在没有出现失败时可能出现的问题:
进程A删除缓存成功;
进程B读取缓存失败;
进程B读取数据库成功,得到旧的数据;
进程B将旧的数据成功地更新到了缓存;
进程A将新的数据成功地更新到数据库。
可见,进程A的两步操作均成功,但由于存在并发,在这两步之间,进程B访问了缓存。最终结果是,缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致。
如上图,是先更新数据库再删除缓存,再没有出现失败时可能出现的问题:
进程A更新数据库成功;
进程B读取缓存成功;
进程A更新数据库成功。
可见,最终缓存与数据库的数据是一致的,并且都是最新的数据。但进程B在这个过程里读到了旧的数据,可能还有其他进程也像进程B一样,在这两步之间读到了缓存中旧的数据,但因为这两步的执行速度会比较快,所以影响不大。对于这两步之后,其他进程再读取缓存数据的时候,就不会出现类似于进程B的问题了。
最终结论:
经过对比你会发现,先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。
参考答案
Redis集群的分区方案:
Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:
解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。
Redis集群中数据的分片逻辑如下图:
Redis集群的功能限制:
Redis集群方案在扩展了Redis处理能力的同时,也带来了一些使用上的限制:
key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上所以不被支持。
key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
key作为数据分区的最小粒度,因此不能将一个大的键值对象(如hash、list等)映射到不同的节点。
不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即DB0。
复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
Redis集群的通信方案:
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。
Redis集群采用P2P的Gossip(流言)协议,Gossip协议的工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。通信的大致过程如下:
集群中每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口号上加10000;
每个节点再固定周期内通过特定规则选择几个节点发送ping消息;
接收ping消息的节点用pong消息作为响应。
其中,Gossip协议的主要职责就是信息交换,而信息交换的载体就是节点彼此发送的Gossip消息,Gossip消息分为:meet消息、ping消息、pong消息、fail消息等。
meet消息:用于通知新节点加入,消息发送者通知接受者加入到当前集群。meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息封装了自身节点和一部分其他节点的状态数据。
pong消息:当接收到meet、ping消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内封装了自身状态数据,节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
虽然Gossip协议的信息交换机制具有天然的分布式特性,但它是有成本的。因为Redis集群内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。所以,Redis集群的Gossip协议需要兼顾信息交换的实时性和成本的开销。
集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。
如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的超时选项设置时长的一半(cluster-node-timeout/2),那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
每个消息主要的数据占用:slots槽数组(2KB)和整个集群1/10的状态数据(10个节点状态数据约1KB)。
参考答案
Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:
解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。
Redis集群中数据的分片逻辑如下图: