Redis相关知识要点

Redis

Redis,key-Value类型的内存数据库,整个数据库系统在内存中操作,定期异步flush到硬盘上进行保存。常用于缓存,也可以作分布式锁。redis提供多种数据类型,支持事务,两种持久化方式,多种集群方案。
Redis为什么要作为缓存? 高性能和高并发:
高性能:用户第一次访问数据库时,是从硬盘上读取的,过程比较慢,效率比较低。redis作为缓存,将用户访问的诗句存在缓存中,下一次再访问这些数据时就可以直接从缓存中读取了,操作缓存就是直接存在内存,速度特别快。
高并发:直接操作缓存所能承受的请求远远大于直接访问数据库的。把数据库的部分数据存在缓存中,可提供并发能力。
redis/memcached分布式缓存和map/guava本地缓存的区别:
缓存分为本地缓存和分布式缓存,使用map或guava的是本地缓存,轻量而快速,随着jvm的销毁而结束,多实例情况下,每个实例都保存一份缓存,缓存不具有一致性。
分布式缓存,多实例情况下,各实例共用一份缓存数据,缓存具有一致性。缺点,架构复杂,要保证服务的高可用。
redis和memcached的区别:
1、redis支持更丰富的数据类型:redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储,memcached仅支持简单的数据类型,string.
2、reids支持数据的持久化,可以将内存中的数据保存到磁盘中,重启的时候可以再次加载使用,而memcached不支持持久化。
3、集群模式,memcached没有原生的集群模式,redis目前是原生支持cluster模式的。
4、memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路IO复用模型。

redis常用数据结构以及场景。
string,hash,list,set,zset;key都是string,但value是多种数据结构的。
数据类型:
String:  set、get、decr、incr、
hash(将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在redis里);hget,hset,hgetall
List,粉丝列表;存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表。以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询。)lpush,rpush,lpop,rpop,lrange
Set:无序集合,自动去重set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁。Sadd, Spop, Sunion, sinterstore(交集)
Sorted Set,去重但是可以排序;最大的特点是有个分数可以自定义排序规则。Zadd,Zrange,zcard
底层实现:
简单动态字符串SDS:char[]数组加len属性和free属性(记录数组中未使用的字节数);直接获取长度,防止溢出,
Redis还实现了双端链表,双端,无环,带长度属性len;
Redis 的字典使用哈希表作为底层实现。链地址法解决冲突的哈希表实现的。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。跳跃表通常是有序集合的底层实现之一,表中的节点按照分值大小进行排序。
1、由很多层结构组成;

2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
  3、最底层的链表包含了所有的元素;
 4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
  5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;
跳跃表的操作过程:
①、搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
  ②、插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层到k层。
  ③、删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。
ziplist编码的有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。
skiplist编码的有序集合底层是一个命名为zset的结构体,而一个zset结构同时包含一个字典和一个跳跃表。跳跃表按score从小到大保存所有集合元素。而字典则保存着从member到score的映射,这样就可以用O(1)的复杂度来查找member对应的score值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存。跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度。

redis可以为key设置过期时间,对过期的key,采用定期删除和惰性删除的方式。
定期删除:每100ms,随机抽取一些设置过期时间的key,检查是否过期,如过期,则删除。

惰性删除:一些过期的key,并没有被定性删除删除掉,只有当系统使用并检查其过期了,才会将其删除。

大量key到内存中,达到redis内存最大值,会进行内存淘汰机制。
Redis数据淘汰策略:noeviction,内存达到最大值,直接返回错误。allkeys-lru:所有键里最近最少使用;
allkeys-random:所有键随机回收;volatile-lru:设置过期时间的键中,回收最近最少使用的,volatile-random:设置过期集合键中,随机回收。
volatile-ttl:设置过期时间的键中,回收存活时间较短的键。
redis持久化机制:redis持久化就是将内存中的数据写入到硬盘中,为了备份重用数据。支持两种持久化方式,快照RDB和只追加文件AOF;
快照RDB,通过创建快照获得某个时间点的数据副本,可以对快照进行备份,或者复制到其他机器重用数据。默认的持久化方式。save 900 20,在15分钟之后,有20个key变化,触发快照。

AOF,实时性更好,主流方案,默认没有开启。开启后,每执行一条更改redis数据的命令,都将该命令写入磁盘的AOF中。三种AOF方式, always,每次修改都写入AOF; everysec,每秒钟同步一次,多个命令一次写入。 no,操作系统决定何时同步。
RDB优缺点:每个RDB都代表了某一时刻redis的数据,适合做冷备份,且恢复数据速度快。缺点:实时性不好,可能会丢失部分数据,如果文件过大, 可能会导致提供的服务暂停几秒。
AOF优缺点:实时性好,一般1s一写入,而且写入性能比较好。缺点:AOF文件更大,开启后QDS相对来说比较低,恢复速度慢。
综合AOF和RDB两种持久化方式,用AOF来保证数据不丢失,作为恢复数据的第一选择;用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,可以使用RDB进行快速的数据恢复。

redis事务。redis使用MULTI、EXEC、WATCH等命令实现事务功能。MULTI开始事务,EXEC,执行事务。DISCARD取消事务,WATCH监视key;
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
缓存雪崩和缓存穿透
缓存雪崩,缓存在同一时间内大面积失效,所有的请求都落在数据库中,数据库短时间内承受大量请求而崩掉。
缓存同一时间内失效课可能是服务器宕机或者设置了相同时间过期的key同时失效。
事前,尽量保证redis集群的高可用性,key的过期时间尽可能错开。
事中,本地ehcache缓存(先查本地缓存再查redis,然后数据库)+hystrix限流(每秒就接受一定量的请求)&降级(多余的请求走降级组件);
事后,利用持久化机制恢复缓存。

缓存穿透,查询一个根本不存在的数据,缓存层和数据库都不命中,失去了缓存的意义。
解决方案1、对空结果作缓存,意味着设置更多的键,占用更多的内存,所以要过期时间设置很短。
2、布隆过滤器:将所有可能存在数据哈希到一个bitMap中,拦截不存在的数据访问,缺点有一定的误识别率。

并发竞争key问题,多个系统同时对一个key进行操作,执行顺序不同导致与期望结果不同。使用分布式锁解决。
redis实现分布式锁。
zookeeper实现分布式锁。

如何保证缓存和数据库双写时的数据一致性?
当发生数据修改时,是先更新数据库还是先更新缓存。缓存的更改是更新缓存还是直让原有缓存失效。
一般来说是先更新数据库,然后让缓存失效,将失效信息发送到mq中,mq不断的重试让缓存失效,保证缓存和数据库的数据一致性。
先删除缓存,再更新数据库:A写然后删除缓存,然而B查询缓存不存在,查询数据库得到旧值,然后缓存旧值。然后A更新数据库。这就造成了数据库和缓存不一致。这时采用延时双删策略:先删除缓存,然后更新数据库,然后休眠一段时间内如1s,再删除缓存。淘汰这一段时间的缓存脏数据。
如果mysql采用了读写分离:也会造成数据不一致。A想写,先删缓存,然后写入数据库。B读请求,没有缓存,读取从库旧值,然后缓存旧值,数据库主从同步。还可以采用延时双删的策略,延时的时间长点,完成主从同步。
采用这种同步淘汰策略,吞吐量降低怎么办?第二步的删除作为异步,启动一个线程去删除。
第二次删除,如果删除失败怎么办?

先更新数据库,再删除缓存
缓存更新套路:
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
缓存刚好失效,A做查询,得到旧值,B做更新,让缓存失效。A将旧值缓存。但由于B是更新然后让缓存失效,这一耗时明显大于A查询然后缓存旧值,所以出现不一致的概率很低。如果非要解决,就用延时双删,再删除一次呗。
所以,如果删除缓存失败怎么办?
提供一个保障的重试机制即可。一、将需要删除的key发送给消息队列,保证其删除成功。
二、降低耦合性的方案:(1)更新数据库数据(2)数据库会将操作信息写入binlog日志当中(3)订阅程序提取出所需要的数据以及key(4)另起一段非业务代码,获得该信息(5)尝试删除缓存操作,发现删除失败(6)将这些信息发送至消息队列(7)重新从消息队列中获得该数据,重试操作。

redis是单线程的,为什么还那么快?
1、操作完全基于内存,速度快。
2、数据结构简单,对数据的操作也简单。
3、采用单线程,避免了不必要的上下文切换开销。
4、使用非阻塞的多路IO复用模型。
I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
select缺点:每次调用select,都需要把fd_set集合从用户态拷贝到内核态,都需要在内核遍历传递进来的所有fd_set,且内核对被监控的fd_set集合大小做了限制,为1024。
poll缺点:poll没有最大文件描述符数量的限制,其他缺点和select一样。数据结构变成了pollfd
epoll: 最大连接数没有限制,采用事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面
Unix五种IO模型:阻塞IO, 网络编程中,读取客户端的数据需要调用recvfrom。在默认情况下,这个调用会一直阻塞直到数据接收完毕,就是一个同步阻塞的IO方式。内核准备数据,并将数据从内核拷贝到用户内存,内核返回结果,用户进程再解除阻塞状态,重新运行起来。非阻塞IO:用户进程调用recvfrom之后,如果内核数据没有准备好,并不会阻塞用户进程,而是立即返回数据未准备好的结果,用户进程以后不断调用recvfrom来轮询内核是否准备好数据。信号驱动IO,调用之后,不等待数据就绪立即返回,等内核准备好数据之后,发送信号给用户进程。异步IO, 读取操作(aio_read)会通知内核进行读取操作并将数据拷贝至进程中,完事后通知进程整个操作全部完成(绑定一个回调函数处理数据)。读取操作会立刻返回,程序可以进行其它的操作,所有的读取、拷贝工作都由内核去做,做完以后通知进程,进程调用绑定的回调函数来处理数据。对比信号驱动IO,异步IO的主要区别在于:信号驱动由内核告诉我们何时可以开始一个IO操作(数据在内核缓冲区中),而异步IO则由内核通知IO操作何时已经完成(数据已经在用户空间中)。IO多路复用:可以处理多个连接。这里的select相当于一个“代理”,调用select以后进程会被select阻塞,这时候在内核空间内select会监听指定的多个datagram (如socket连接),如果其中任意一个数据就绪了就返回。此时程序再进行数据读取操作,将数据拷贝至当前进程内。由于select可以监听多个socket,我们可以用它来处理多个连接。

redis热key问题:某个key被大量访问,对redis服务器造成了很大的压力。
解决方案:
1、服务端缓存:即将热点数据缓存至服务端的内存中,利用 ehcache ,或者一个 HashMap 都可以。在你发现热key以后,把热key加载到系统的JVM中。针对这种热key请求,会直接从jvm中取,而不会走到redis层。这个可能发生缓存和redis数据不一致的情况。利用Redis自带的消息通知机制,对于热点Key建立一个监听,当热点Key有更新操作的时候,缓存也随之更新。
2、备份热key, 即将热点Key+随机数,随机分配至Redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。(Redis集群中包含了16384个哈希槽(Hash slot),集群使用公式CRC16(key) % 16384来计算Key属于哪个槽。那么同一个Key计算出来的值应该都是一样的,如何将Key分到其他机器上呢?只要再后面加上随机数就行了,这样就能保证同一个Key分布在不同机器上)
这都是知道热key是什么的情况,那么如何发现热key?
1、经验预估。2、Redis自带命令查询:Redis4.0.4版本提供了redis-cli –hotkeys就能找出热点Key.
3、客户端收集:在操作Redis之前对数据进行统计。

redis集群数据迁移方式
redis集群并没有使用一致性hash,而是使用数据分片引入哈希槽来实现。
redis集群共有16384个哈希槽,所有的键都将映射到哈希槽中,使用CRC16(key)%16384=哈希槽,集群按槽分片,每个节点指派不同数量的槽。
一致性hash,是将整个hash值空间0-232-1组成一个虚拟圆环,key的哈希函数对232取模得到哈希值,在圆环上顺时针转,遇到的第一个服务器就是定位到的服务器。这样即使增加或减去一个服务器,对数据的影响比较小,数据迁移也比较简单。缺点:在某些极端情况下,可能数据扎堆分布在一个服务器上,当这个服务器出现问题,对整个系统的影响依然很大。针对分配不均的情况下,提出了虚拟节点,将服务器后面的ip或主机名后面增加编号,生成多个虚拟节点,分配到虚拟节点的数据实际上是分配到了该服务器上。
普通的分布式缓存策略是:hash(obj)%N。当其中一个服务器宕机或者需要新增一个服务器时,缓存策略变为hash(obj)%(N-1);就意味着,所有的缓存都将失效。必定会造成缓存数据的丢失,会去向后端的服务器去请求。增加删除服务器时,代价比较大,所有的数据不得不根据id再次计算哈希值,然后%N,进行重新分配和大规模数据迁移。

分布式与集中式:
CAP:C一致性,A可用性,P分区容错性。这三个基本需求,最多能满足两个。
BASE: 基本可用,软状态,最终一致性。

分布式锁:惯用关系数据库固有的排他性实现不同进程之间的互斥,但关系数据库瓶颈所在,别啥都交给人家。
redis的SETNX命令可以方便的实现分布式锁。 setNX(SET if Not eXists)+ expire;
如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高;如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock;应该考虑类似Zookeeper的方案,或者支持事务的数据库。
1.获取当前时间戳
2.client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
3.client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功
4.如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁

Zookeeper分布式锁:
竞争分布式锁,在一个节点下,创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理,依次类推……

架构相关

主从+哨兵:
通过持久化功能,Redis能把内存中数据保存到硬盘上,保证了即使在服务器重启的情况下也不会损失(或少量损失)数据。数据是存储在一台服务器上的,如果这台服务器出现硬盘故障等问题,也会导致数据丢失。为了避免单点故障,通常的做法是将数据库复制多个副本以部署在不同的服务器上。Redis 提供了复制(replication)功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上。在复制的概念中,数据库分为两类,一类是主数据库(master),另一类是从数据库[1] (slave)。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
当从数据库启动时,会向主数据库发送sync命令,主数据库接收到sync后开始在后台保存快照rdb,在保存快照期间受到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从数据库。从数据库依据快照和命令和主节点保持同步。之后,主每受到1个命令就同步发送给从数据库。
当出现断开重连后,2.8之后的版本会将断线期间的命令传给重数据库、增量复制。
master每次接收到写命令之后,先在内部写入数据,然后异步发送给slave node
如果采用了主从架构,那么建议必须开启master node的持久化!否则重启之后,认为自己没有数据,会把从节点清空,丢失数据。
slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量。
Slave node不会设置过期key,如果主节点一个key过期了,或者通过LRU淘汰了一个key,发送del命令给从节点。
master和slave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况。

哨兵:
Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。
哨兵的作用就是监控redis主、从数据库是否正常运行,主出现故障自动将从数据库转换为主数据库。
(1)监控主数据库和从数据库是否正常运行。
(2)主数据库出现故障时自动将从数据库转换为主数据库。
主从切换过程:
(1) slave leader升级为master
(2) 其他slave修改为新master的slave
(3) 客户端修改连接
(4) 老的master如果重启成功,变为新master的slave
1、两种数据丢失的情况
(1)异步复制导致的数据丢失,数据还没复制到slave,master就宕机了。
(2)脑裂导致的数据丢失。集群里就会有两个master,
解决方法:要求至少有1个slave,数据复制和同步的延迟不能超过10秒
如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求。
哨兵:sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机
odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机。
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,
接下来会对slave进行排序
(1)按照slave优先级进行排序,slave priority越低,优先级就越高
(2)如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
(3)如果上面两个条件都相同,那么选择一个run id比较小的那个slave
集群:
主从架构+哨兵机制虽然保证了Redis的高可用性,但每个Redis实例都是全量保存,浪费内存。为了最大化的利用内存,可以使用集群,也就是分布式存储,每台redis存储不同的内容。
Reids分布式数据存储算法不是一致性哈希,而是哈希槽算法。
redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot。
redis cluster中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot。
hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去。移动成本很低。
Redis集群集成了主从复制和哨兵的功能。集群支撑N个redis master node,每个master node都可以挂载多个slave node
读写分离的架构,对于每个master来说,写就写到master,然后读就从mater对应的slave去读。
高可用,因为每个master都有salve节点,那么如果mater挂掉,redis cluster这套机制,就会自动将某个slave切换成master。redis cluster(多master + 读写分离 + 高可用);

在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379,另外一个就是加10000的端口号,比如16379。16379端口号是用来进行节点间通信的,也就是cluster bus的东西,集群总线。cluster bus的通信,用来进行故障检测,配置更新,故障转移授权。

集群是如何判断是否有某个节点挂掉:
每一个节点都存有这个集群所有主节点以及从节点的信息。它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。
集群进入fail状态的必要条件:
1、某个主节点和所有从节点全部挂掉,我们集群就进入faill状态。
2、如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态.
3、如果集群任意master挂掉,且当前master没有slave.集群进入fail状态。
主节点宕机之后,从节点发起投票,投票过程是集群中所有master参与。选举的依据依次是:网络连接正常->5秒内回复过INFO命令->10*down-after-milliseconds内与主连接过的->从服务器优先级->复制偏移量->运行id较小的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值