ACID:
原子性:数据库将事务中的多个操作当做一个整体来执行,服务要么执行事务中的所有操作,要么一个操作也不会执行;redis 具备一定的原子性,但是不支持回滚。
一致性:如果数据在执行事务之前是一致的,那么在执行事务之后无论事务是否成功,数据库也应该是一致的;redis 不具备一致性概念。
隔离性:数据库中有多个事务并发执行,各个事物之间不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果是完全相同的;reids 具备隔离性。
持久性:当一个事务执行完毕,执行这个事务得到的结果会被保存在持久化的存储中,即使服务器在事务执行完后就停机了,执行事务的结果也不会丢失;redis 通过一定策略可以保持持久性。
为什么要使用缓存?
用缓存主要有两个用途:高性能,高并发。
高性能:
对于一些需要复杂操的耗时查出来的结果,而且确定后面不怎么变化但是有很多请求,那么直接将查询出来的结果放到缓存中,后面直接度缓存就好了
高并发:
MySQL单机支撑打2000QPS 也就开始报警了,所以高并发时单机打的MySQL绝对不够用。这时候就加上缓存,把很多数据放到缓存里。缓存单机支撑的并发量轻松一秒几万甚至几十万,比MySQL强。
Redis 和memcached:
Redis实际上是个单线程工作模型,比memcached 拥有更多的数据结构,能支持更丰富的数据操作,如果需要缓存能够支持更复杂的结构和操作Redis 是不错的选择。Redis 3.x 版本中能支持集群模式,而memcached 没有原声的集群模式,需要依靠用户客户端来实现集群中分片写入数据。
Redis值使用单核,而memcached 可以使用多核,所以平均每一个核上Redis 在存储小数据时性能更高。但是在数据量大的情况下性能会比memcached 稍有逊色。
Redis 内部使用文件事件处理器;采用iO 多路复用机制同时监听多个socket,将生产事件的scoket 压入内存队列,事件分派器根据socket 上的事件类型来选择对应的事件处理器来处理。
文件处理器结构:
多个socket
IO 多路复用
文件事件分派器
事件处理器
为什么Redis 单线程也能高效率?
纯内存操作
核心是基于非阻塞的IO 多路复用机制
C 语言实现,一般来说C 语言实现的程序“距离”操作系统更近,执行速度更快
单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
Redis 的数据类型:
主要有以下几个类型:
string:
最简单的类型,简单的KV 缓存
hash:
一个类似ma 的结构,可以缓存一些结构化数据,比如对象(前提是这个对象没有嵌套其他对象),每次读写缓存的时候,可以操作hash 里的某个字段。
list:
有序列表,可以存储一些列表型的数据结构,可以通过lrange 命令读取某个闭区间的元素,可以基于list 实现分页查询,基于redis 实现简单的高性能查询。
set:
无序列表,自动去重。基于set 将需要去重的数据存放进去就可以自动去重了,如果需要对一些数据进行快速全局去重,也可以基于jvm 内存里的hashset 进行去重。set 可以进行交集、并集、差集的操作。
sset(sorted set):
sset 是排序的set,去重且可以排序,写进去的时候给一个分数,会自动根据分数进行排序。
Redis 过期策略:
定期删除,惰性删除:
定期删除是指redis 会每个100ms 就随机抽取一些设置了过期时间的key 检查,如果过期了就进行删除。定期删除并不会便利所有的key 检查是否过期,所以定期三处可能会导致很多过期的key 到时间了但是没有被删除掉,所以就有了惰性删除,这就是说,在获取某个key 的时候redis 会进行检查这个是设置过期时间了是否已经过期,如果过期了就进行删除,不会返回数据。若果定期删除和惰性删除还是漏掉了很多过期了的可以删除不了导致内存耗尽就使用内存淘汰机制。
内存淘汰机制:
noeviction:当内存不足以容纳新的数据时,写写入就会报错。
allkeys-lru:当内存不足以容纳新的数据时,在键空间中移除最近最少使用的key。(常用)
allkeys-random:当内存不足以容纳新的数据时,在键空间中随机移除key。
volatile-lru:当内存不足以容纳新的数据时,在设置了过期时间的键空间中移除最近最少使用的key。
volatile-random:当内存不足以容纳新的数据时,在设置了过期时间的键空间中随机移除key;
volatile-ttl:当内存不足以容纳新的数据时,在设置了过期时间的键空间中有更早过期时间的key 优先移除。
Redis 的高并发和高可用:
redis 实现高并发主要依靠主从架构,一主多从,如果要在共并发的同时容纳大量数据就需要用到redis 集群。
redis 从主从架构部署的话叫上哨兵就可以实现redis 高可用。redis 的高可用框架叫做failover 故障转移,也叫主备切换,主节点在故障时,自动检测并将某个从节点转换为主节点的过程就做主备切换。
Redis 主从架构(master-slave):
单机的redis 一般用来支撑读高并发的;主从架构是一主多从,主负责写,并把数据复制到气的slave 节点,从节点负责读,所有的读请求全部走从节点,可以轻松实现水平扩容,支撑高并发。
Redis 主从复制的核心原理:
启动从节点的时候会发送一个PSYNC 命令给主节点。当从节点第一次链接到主节点时会触发一次全量复制,master 就会在后台启动一个线程生成一个RDB 快照文件,同时会将从客户端收到的命令缓存到d内存中。再将RDB 文件发送给salve,salve 会写入本地磁盘,然后从本地磁盘加载到内存中。接着master 将内存中的命令发行到salve,从节点同不这些数据
主从复制的断点续传:
就是在主从复制过程中如果连接断掉了,再连接上之后可以接着上次复制的地方开始继续复制,而不是从头开始。
无磁盘化复制:master 直接在内存创建RDB 发送给salve。不会在本地落地磁盘。
复制的完成整流程:
- 从节点启动时本地保存主节点的信息。
- 从节点内部的定时任务每秒检查是否有新的主节点要连接和复制,发现就建立连接
- 从节点发送ping 命令给主节点。
- 主节点第一次连接执行全量复制,将所有的数据发给从节点。后续就是异步复制。
全量复制:
master 执行bgsave ,在本地生成RDB 文件。
主节点将RDB 文件发送给从节点;如果RDB 复制时间超过60 秒就默认复制失败,可以适当调整这个参数。
主节点在生成RDB 时会将所有新的命令缓存在内存中,在主节点保存RDB 之后在将新的命令复制给从节点。
如果在复制期间内存缓冲区持续消耗超过64 MB,或者 一次性超过256MB,就会停止复制,复制失败。
增量复制:
如果在复制过程中连接断开,重新连接之后就出发增量复制。
增量复制就是主节点直接从自己的backlog 中获取部分丢失的数据发送给从节点,默认的backlog 是1MB。
主节点会根据从节点发送的psync 中的offset 来从backlog 中获取数据。
异步复制:
主节点每次接收到写命令之后先在内部写入数据,然后异步发送给从节点;
Redis 基于哨兵集群实现高可用:
哨兵(sentinel)是redis 集群机构中非常重要的一个组件,主要功能有:
- 集群监控:负责监控redis 的主从进程是否正常工作。
- 消息通知:如果某个redis 实力有故障,哨兵负责发送消息作为警报通知。
- 故障转移:主节点挂了就会自动转移到从节点上。
- 配置中心:故障转移发生了就通知客户端新的主节点地址。
哨兵至少需要3个实例来保证自己的健壮性,哨兵本身也是分布式的,互相协调工作;判断一个主节点是否宕机需要大部分哨兵同意才行;哨兵集群部署必须两个节点以上,即使部分哨兵节点挂掉了,哨兵集群也能正常工作。哨兵+redis 主从架构能保证redis 的高可用性,但是不保证数据零丢失。
哨兵主备切换数据丢失问题:
异步复制导致的数据丢失:
可能部分数据还没有复制到从节点就宕机了
脑裂导致的数据丢失:
某个主节点所在的机器连不上从节点机器,即使主节点还在运行,哨兵也会认为主节点宕机了,因此再次恢复时会被作为从节点,自己的数据就会清空。
解决方案:
- sdown 和odowm 转化机制:
- 哨兵集群的自动发现机制:
- slave 配置的自动纠正:
- salve ->master 选举算法:
- quorum 和majority:
- configuration epoch:
- configuration 传播:
Redis 的持久化:
reids 如果只是将数据缓存在内存里,当redis 宕机再重启后数据就丢失了,所以就需要用持久化机制将数据写入内存的同时,异步将数据写入磁盘文件里进行持久化。
持久化方式:RDB 和 AOF:
RDB:
RDB 持久化机制是对redis 中的数据执行周期性的持久化。
RDB 会生成多个数据文件,每个文件代表了某一时刻的redis 数据,非常适合做冷备;可以完整地将数据文件发送到一些远程的安全存储上。
RDB 对redis 对外提供的读写服务影响非常小,可以让redis 保持高性能。
相对于AOF 来说,基于RDB 数据文件的重启和恢复进程会更快。
RDB 再redis 故障是丢失数据做的没有AOF 好,
RDB 每次再子进程来执行RDB 快照数据文件生成的时候,如果数据文件太大,可能会导致对客户端提供的服务暂停。
AOF:
AOF 相对于RDB 可以更好地保护数据不会丢失,一般AOF每个一秒再后台程序执行一次fsync 操作,最多丢失一秒钟的数据。
AOF 的日志文件以append -only 模式写入,所以没有磁盘开销,写入性能非常高;而且文件不容易破损,即使文件尾部破损,也是很容易修复。
AOF 即使在日志文件过大,在后台出现重写操作,也不会影响到用户的读写操作。
AOF 的日日志文件的命令通过非常刻度的方式进行记录,这个特性非常适合灾难性的误删除的紧急恢复,误删后只要没有重写就可以立即拷贝AOF 文件,将最后一条flushall 命令删除,在把AOF 文件放进去,就可以通过恢复机制恢复所有的数据
对于一份数据来讲AOF 日志文件通常比RDB 数据快照文件要大。
AOF 开启后,支持写的QPS 会比RDB 低。
AOF 这种较为复杂的基于命令日志的回放方式,比基于RDB 每次持久化一份完整快照文件的方式更加脆弱,容易有bug。不过AOF 为了避免重写过程导致的bug,因此每次重写并不是基于旧的指令日志进行的。而是基于当时内存中的数据进行指令的重新构建,这样健壮性就会好很多。
Redis 集群:
redis cluster 架构下,每个redis 要开放两个端口,主要针对海量数据+高并发+高可用场景,redis 集群支撑多个主节点,每个主节点又可以挂载多个从节点,这样整个redis 就可以横向扩展了,每个节点就能存放更多数据。
它会自动将数据进行分片,每个master 上放一部分数据。
节点间的内部通信机制:
集群元数据的维护方式有两种:集中式、Gossip 协议,redis cluster 节点间采用gossip 协议进行通信;
集中式:
集中式是将集群元数据(节点信息,故障等)集中存储在某个节点上;redis 维护集群元数据采用另一种方式:gossip 协议。所有的节点都持有一份元数据,不同的节点如果出现了元数的变更,就不断将元数据发给其他的节点,让其他节点也进行变更。
集中式对于元数据的读取和更新的时效性非常好,一旦元数据出现变更,就立即更新到集中式的储存中,其他节点读取的时候就可以感知到。它的不好在于所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
gossip 协议:
gossip 协议包含了多种消息:ping、pong、meet、fail 等等;
meet:某个节点发送meet 给新加入的节点让新节点加入集群,然后开始与其他节点通信
ping:每个节点都会频繁地给其他节点发送ping,包含自己的状态和自己维护的集群元数据,节点之间互相通过ping交换元数据。
pong:返回ping 和meet,包含自己的状态和其他信息,用于信息广播和更新
fail:某个节点判断一个节点fail 后,就发分散,不像集中式的集中在一个地方,更新请求会陆陆续续地发送到所有的请求上,降低了压力;不好在于元数据的更新有延迟,可能导致集群中的一些操作会滞后。
分布式寻址算法:
hash 算法(大量缓存重建)
一致性hash 算法(自动缓存迁移)+虚拟节点(自动负载均衡)
redis 集群的hash slot 算法。
hash 算法:
来了一个key,首先计算hash 值,然后对节点数取模打在不同的主节点上,一旦某个主节点宕机,所有的请求都会基于最新剩余的主节点数去取模;这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。
一致性hash 算法:
一致性hash 算法将hash 值空间组织成一个虚拟的圆环,这个空间按顺时针方向组织。来了一个key,先计算hash 值,并确定数据在环上的位置,从这个位置顺时针走,遇到第一个主节点就是这个key所在的位置。如果一个节点挂了受影响的数据仅仅是此节点到环空间前一个节点之间的数据,其他不受影响;但是一致性hash 算法的节点太少时容易因为节点分布不均匀而造成缓存热点问题。不过一致性hash 算法引入了虚拟节点机制解决热点缓存问题。
redis cluster 的hash slot 算法:
redis 集群有hash slot(16384),对每个key 计算CRC16 值然后取模,就可以 获取对应的hash slot。redis cluster 中每个master 都会持有部分的slot。hash slot 增加和移除node 的方式时移动hash slot,移动它的成本很低。任何一台机器宕机,另外的节点时不影响的,因为key 找的是hash slot 不是机器。
redis cluster 的共可用和主备切换原理:
判断节点宕机:
如果一个节点认为另一个节点宕机,就是主观宕机;如果是多个几点认为一个节点宕机,就是客观宕机。跟哨兵原理几乎一样。一个节点一直没有返回就被认为主观宕机,然后会在gossip ping 消息中ping 给其他节点,超过半数的节点认为主观宕机了就会变成客观宕机。
从节点过滤:
主节点宕机后会在从节点中选择一个成为主节点。按断开连接的时间去判断有没有资格成为主节点。
从节点选择:
从节点根据自己对主节点复制数据的offset 来设置选举时间,offset 越大的从节点优先选举。
缓存雪崩:
一个系统每秒几千个请求,如果在缓存高峰期缓存机器意外发生了宕机,那么请求会全部落在数据库中,数据库处理不了那么多请求会报警告人后就挂掉了。解决方案:
事前:使用redis 高可用,主从+哨兵,redis 集群,避免全盘崩溃。
事中:本地ehcache 缓存+hystrix 限流降级,避免数据库挂掉。
事后:redis 持久化,一旦重启就自动从磁盘上加载数据,快速恢复缓存数据;
好处:
数据库绝对不会死,以为限流组件限制了每秒只有多少个请求能通过。
数据库不死,对于用户来说,五分之二的请求都是可以被处理的。
缓存穿透:
一个系统的几千个请求有大部分都是黑客发出的恶意请求,这些请求都没有经过缓存;直接查询数据库。这种恶意攻击场景的缓存穿透就会直接吧数据库打挂掉。解决:
每次只要从数据库中没有查到就写一个空值到缓存里去,然后设置一个过期时间,这样在缓存失效之前有相同的key 来访问时都可以直接在缓存数据中取。
缓存击穿:
缓存击穿就是某个key 访问非常频繁,处于集中式高并发访问的情况,当这个失效的瞬间,大量的请求就不经过缓存直接访问数据库,可能导致数据库不堪重压。解决:
可以将热点数据设置为永不过期;或者基于redis 实现互斥锁,等待第一个请求构建完缓存之后再释放锁,进而其他请求才能通过这个key 来访问数据。
缓存与数据库的双写一致性:
即读请求和写请求串行化,串到一个内存队列里去;如果系统不是严格要求缓存+数据库必须保持一致性的话不建议使用。
Cache Aside Patten:
最经典的缓存+数据库读写的模式。读的时候先读缓存,缓存没有就读数据库然后取出数据库的数据放进缓存同时返回响应。写的时候先更新数据库然后再删除缓存。
Redis 并发竞争问题:
就是多个客户端同事并发写一个key,本来先到的数据后到了,导致数据版本错了;或者多个用户同时取一个key,修改值后再写回去,只要顺序错了数据就错了。因此redis 会使用CAS 类的了关锁方案,在每次写入数据库前先判断当前这个值的时间戳是否比缓存的值的时间戳要新,如果是的话那么可以写,否则就不能用旧的数据覆盖新的数据。