Redis教程

一问Redis只知道是key-value类型数据库?它可远不止这些!

本文主要是介绍一问Redis只知道是key-value类型数据库?它可远不止这些!,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

欢迎访问我的blog http://www.codinglemon.cn/

立个flag,8月20日前整理出所有面试常见问题,包括有:
Java基础、JVM、多线程、Spring、Redis、MySQL、Zookeeper、Dubbo、RokectMQ、分布式锁、算法。

8. Redis

文章目录

  • 8. Redis
    • 8.1 Redis有哪几种数据结构?
      • 8.1.1 String
      • 8.1.2 Hash
      • 8.1.3 Set
      • 8.1.4 Zset(sorted set)
      • 8.1.5 List
        • 8.1.5.1 lrange的坑
      • 8.1.6 HyperLogLog
      • 8.1.7 Geo
      • 8.1.8 Pub/Sub
      • 8.1.9 BitMap
      • 8.1.10 BloomFilter
    • 8.2 Redis中SDS是什么?为什么要用SDS?
    • 8.3 简单介绍一下Redis的底层链表结构?
    • 8.4 Redis的字典有哪些应用?如何处理hash冲突?
      • 8.4.1 如何解决hash冲突
      • 8.4.2 渐进式Rehash
      • 8.4.3 字典的收缩
      • 8.4.4 总结
    • 8.5 什么是跳表?他有什么特征?
    • 8.6 Redis有哪些常用命令?
      • 8.6.1 Keys
      • 8.6.2 SETNX
        • 8.6.2.1 setNX实现分布式锁(分布式问题常问!)
      • 8.6.3 expire
    • 8.7 Redis的持久化机制是怎样的?(问Redis必问!)
      • 8.7.1 RDB
      • 8.7.2 AOF
    • 8.8 Redis集群下的数据同步机制是怎样的?
      • 8.8.1 主从同步
      • 8.8.2 快照同步
      • 8.8.3 无盘复制
    • 8.9 Redis集群下的哨兵机制了解吗?
    • 8.10 什么是脑裂?如何解决脑裂问题?
    • 8.11 Redis-Cluster
      • 8.11.1 redis cluster节点分配
      • 8.11.2 Redis Cluster主从模式
      • 8.11.3 redis集群的搭建
    • 8.12 Redis常见问题
      • 8.12.1 什么是缓存雪崩?如何解决?(常问)
      • 8.12.2 什么是缓存击穿?如何解决?(常问)
      • 8.12.3 什么是缓存穿透?如何解决?(常问)
      • 8.12.4 如何保证MYSQL和Redis的双写一致性?
      • 8.12.5 Redis大key问题,如何解决?
      • 8.12.6 系统中遇到热点key的问题是如何解决的?
    • 8.13 Redis的缓存过期策略有哪几种?分别有什么特点?
      • 8.13.1 定时删除
      • 8.13.2 惰性删除
      • 8.13.3 定期删除
    • 8.14 Redis的内存淘汰机制有哪些?
    • 8.15 Redis有哪些限流策略?
      • 8.15.1 setnx ex
      • 8.15.2 zset
      • 8.15.3 令牌桶(Redis rateLimiter)
      • 8.15.4 漏桶算法(Leaky Bucket)
      • 8.15.5 令牌桶和漏桶算法的比较

Redis的特点:基于内存,快,规避了IO;持久化、HA(high availability)哨兵、KV形式的存储;每一种value都有本地方法,不用写代码来进行额外计算(因为计算方法本地就有),计算向数据移动。单线程,让所有的计算串行化。

8.1 Redis有哪几种数据结构?

8.1.1 String

介绍:string 数据结构是简单的 key-value 类型。,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。

常用命令:set,get,strlen,exists,dect,incr,setex等

应用场景:一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。

8.1.2 Hash

介绍 : hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。

常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。

应用场景: 系统中对象数据的存储。

8.1.3 Set

介绍 : set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。

常用命令: sadd,spop,smembers(查看set中所有元素),sismember(查看set中是否存在某个元素),scard(查看set长度),sinterstore(取交集),sunion(取并集) 等。

应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景

8.1.4 Zset(sorted set)

介绍: 和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。

常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。

应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。(成绩、积分、排行榜)
插入新的数据得时候,只需要调整插入的前后节点的指针即可;不止比较score,还会比较value。

8.1.5 List

介绍 :list 即是 链表。 链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

常用命令: rpush,lpop,lpush,rpop,lrange、llen 等。

应用场景: 发布与订阅或者说消息队列、慢查询。

8.1.5.1 lrange的坑

Redis 中有很多命令是 O(N) 的。比如:keys、sort、hgetall、smembers、lrange、zrange、sinter 等。使用这些命令刚开始感觉很爽,但是随着数据的沉淀。使用它们却让 CPU 发生了饱和现象。

单线程的 redis 处理命令时只能使用一个 CPU,CPU 饱和是指 redis 把单核的 CPU 使用率跑到接近 100%。如果你过多的使用上面的那些命令,并且当并发比较高时,如果某个命令执行时间过长,会造成其他命令阻塞,对 redis 来说是致命的。这就是说,虽然 Redis 采用了非阻塞 IO,但它还是会发生阻塞

除此之外,Redis 持久化也会引起阻塞。持久化引起主线程的阻塞操作主要有:fork 阻塞、AOF 刷盘阻塞、HugePage 写操作阻塞。

请少用或者尽量不要使用 O(N) 的命令。对于大对象统计出来后,采用分段进行 scan、hscan、sscan、zscan 操作。禁止使用 keys、flushall、flushdb 等命令。

8.1.6 HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法(比如统计网站的访问次数),HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存(因为 Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间。),就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

8.1.7 Geo

redis的GEO特性在Redis3.2版本发布,这个功能可以将用户给定的地理位置信息储存起来,并对这些信息进行操作。

8.1.8 Pub/Sub

Redis的pub/sub是一种消息通信模式,主要的目的是解除消息发布者和消息订阅者之间的耦合,Redis作为一个pub/sub的server,在订阅者和发布者之间起到了消息路由的功能。

Redis通过publish和subscribe命令实现订阅和发布的功能。订阅者可以通过subscribe向redis server订阅自己感兴趣的消息类型。redis将信息类型称为通道(channel)。当发布者通过publish命令向redis server发送特定类型的信息时,订阅该消息类型的全部订阅者都会收到此消息。

8.1.9 BitMap

就是通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。

8.1.10 BloomFilter

布隆过滤器,英文叫BloomFilter,可以说是一个二进制向量和一系列随机映射函数实现。 可以用于检索一个元素是否在一个集合中。即将要查询的值用多个hash函数映射,如果多个hash结果的地址都在集合中,则表明该值在缓存中;反之则不在缓存中。

布隆过滤器是用于判断一个元素是否在集合中。通过一个位数组和N个hash函数实现。

新来一个数据,我们如何判断其是否存在于这个布隆过滤器中呢?

很简单,我们只需要将这个新的数据通过上面自定义的几个哈希函数,分别算出各个值,然后看其对应的地方是否都是1,如果存在一个不是1的情况,那么我们可以说,该新数据一定不存在于这个布隆过滤器中。

反过来说,如果通过哈希函数算出来的值,对应的地方都是1,那么我们能够肯定的得出:这个数据一定存在于这个布隆过滤器中吗?

答案是否定的,因为多个不同的数据通过hash函数算出来的结果是会有重复的,所以会存在某个位置是别的数据通过hash函数置为的1。

我们可以得到一个结论:布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在。

优点:

  • 空间效率高,所占空间小。
  • 查询时间短。

缺点:

  • 元素添加到集合中后,不能被删除。
  • 有一定的误判率

应用场景

  1. 数据库防止穿库。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。

  2. 业务场景中判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。还有之前自己遇到的一个比赛类的社交场景中,需要判断用户是否在比赛中,如果在则需要更新比赛内容,也可以使用布隆过滤器,可以减少不在的用户查询db或缓存的次数。

  3. 缓存宕机、缓存击穿场景,一般判断用户是否在缓存中,如果在则直接返回结果,不在则查询db,如果来一波冷数据,会导致缓存大量击穿,造成雪崩效应,这时候可以用布隆过滤器当缓存的索引,只有在布隆过滤器中,才去查询缓存,如果没查询到,则穿透到db。如果不在布隆器中,则直接返回。

  4. WEB拦截器,如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。

实现:使用bitmap可以实现BloomFilter。

8.2 Redis中SDS是什么?为什么要用SDS?

SDS全拼为:simple dynamic string,解释为:简单动态字符串。

C语言字符串使用长度为n+1的字符数组来表示长度为n的字符串,并且字符数组的最后一个元素总是空字符’\0’,因为这种字符串表示方式不能满足Redis对字符串在安全性、效率以及功能方面的要求,所以Redis自己构建了SDS,用于满足其需求。在Redis里,C语言字符串只用于一些无须对字符串值进行修改的地方,比如:日志。在Redis中,包含字符串值的键值对都是使用SDS实现的,除此之外,SDS还被用于AOF缓冲区、客户端状态的输入缓冲区。

image.png

上图展示了一个SDS实例,len表示该SDS保存了一个5字节长度(不包含结束符)的字符串,free表示该SDS还有5个字节的未使用空间,buf是一个char类型的数组,保存了该SDS所存储的字符串值。

为什么使用SDS?

  1. 相比C语言字符串,使获取字符串长度时间复杂度降为O(1)
  2. 杜绝缓冲区溢出
  3. 减少修改字符串时带来的内存重分配次数

总结

  1. 键值的底层都是SDS
  2. AOF缓存区
  3. 记录本身长度C需要遍历
  4. 修改字符减少内存重新分配
    1. 空间预支配
    2. 惰性空间释放
  5. 二进制安全
    1. C只能保存文本数据 无法保存图片等二进制数据
    2. sds是使用长度去判断
  6. 杜绝缓冲区溢出
  7. 兼容部分C字符串函数

8.3 简单介绍一下Redis的底层链表结构?

  1. 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。

  2. 无环:表头节点的prev指针和表位节点的next指针都指向null,对链表的访问以null为终点。

  3. 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。

  4. 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,所以获取节点数量的复杂度为O(1)。

  5. 多态:链表节点使用void *指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点设置类型特定函数,所以链表可以用于保存各种不同类型的值。

链表在Redis中的应用非常多,比如列表键的底层实现之一就是链表,发布与订阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还是用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区(output buffer)。

8.4 Redis的字典有哪些应用?如何处理hash冲突?

Redis的字典使用哈希表作为底层实现。字典,又名映射(map)或关联数组(associative array),他是一种抽象的数据结构,由一集键值对组成,各个键值对的键各不相同,程序可以将新的键值对添加到字典中,或者基于键进行查找、更新或删除操作。

字典的主要用途有以下两个:

  1. 实现数据库键空间(key space)
      redis是一个键值对数据库,数据库中的键值对就是由字典保存:每个数据库都有一个与之相对应的字典,这个字典被称为键空间(key space)。当用户添加一个键值对到数据库时(不论数据库是什么类型),程序就将该键值对添加到键空间;当用户从数据库删除一个键值对时,程序就会将这个键值对从键空间删除

  2. 用作Hash类型键的其中一种底层实现
    redis的Hash类型键使用以下两种数据结构作为底层实现:字典、压缩列表。因为压缩列表比字典更节省内存,所以程序在创建新Hash键时,默认使用压缩列表作为底层实现,当有需要时,程序才会将底层实现从压缩列表转换为字典。

8.4.1 如何解决hash冲突

用单向链表解决(类似于HashMap的数组元素中的单链表)。

如果它们之间的比率ratio = used / size 满足以下任何一个条件的话,rehash 过程就会被激活:

  1. 自然rehash :ratio >= 1 ,且变量dict_can_resize 为真。(即当前元素个数大于等于数组长度)

  2. 强制rehash:ratio 大于变量dict_force_resize_ratio (目前版本中,dict_force_resize_ratio 的值为5)。(当前元素个数为数组长度的5倍以上,则强制hash)

8.4.2 渐进式Rehash

在一个有很多键值对的字典里,某个用户在添加新键值对时触发了rehash过程,如果这个rehash 过程必须将所有键值对迁移完毕之后才将结果返回给用户,这样的处理方式将是非常不友好的。另一方面,要求服务器必须阻塞直到rehash 完成,这对于Redis服务器本身也是不能接受的。为了解决这个问题,Redis 使用了渐进式(incremental)的rehash 方式:通过将rehash分散到多个步骤中进行,从而避免了集中式的计算。

渐进式rehash 主要由 dictRehashStepdictRehashMilliseconds 两个函数进行:

  • dictRehashStep用于对数据库字典、以及哈希键的字典进行被动rehash;

  • dictRehashMilliseconds则由Redis服务器常规任务程序(server cron job)执行,用于对数据库字典进行主动rehash;

dictRehashStep:每次执行dictRehashStep ,ht[0]->table 哈希表第一个不为空的索引上的所有节点就会全部迁移到ht[1]->table 。在rehash 开始进行之后(d->rehashidx 不为-1),每次执行一次添加、查找、删除操作,dictRehashStep 都会被执行一次。因为字典会保持哈希表大小和节点数的比率在一个很小的范围内,所以每个索引上的节点数量不会很多(从目前版本的rehash 条件来看,平均只有一个,最多通常也不会超过五个),所以在执行操作的同时,对单个索引上的节点进行迁移,几乎不会对响应时间造成影响。

dictRehashMilliseconds: 可以在指定的毫秒数内,对字典进行rehash 。当Redis 的服务器常规任务执行时,dictRehashMilliseconds 会被执行,在规定的时间内,尽可能地对数据库字典中那些需要rehash 的字典进行rehash ,从而加速数据库字典的rehash进程。

在哈希表进行rehash 时,字典还会采取一些特别的措施,确保rehash 顺利、正确地进行:

  • 因为在rehash 时,字典会同时使用两个哈希表,所以在这期间的所有查找、删除等操作,除了在ht[0] 上进行,还需要在ht[1]上进行。

  • 在执行添加操作时,新的节点会直接添加到ht[1] 而不是ht[0] ,这样保证ht[0] 的节点数量在整个rehash 过程中都只减不增。

总结:rehash时同时使用两个hash表,把ht[0]的元素存入ht[1]中,且新元素也存入ht[1]中,保证ht[0]中元素只减不增,直到ht[0]中没有元素,再将ht[0]清空,将ht[1]变为ht[0]。所以在这期间的所有查找、删除等操作,除了在ht[0] 上进行,还需要在ht[1]上进行。(有点类似于JVM的s0和s1内存区域)

8.4.3 字典的收缩

上面描述了通过rehash对字典的扩展,如果哈希表的未用节点数比已用节点数多很多,那么也可以通过哈希表进行rehash来收缩字典。执行步骤如下:

  1. 创建一个比ht[0]->table 小的ht[1]->table ;

  2. 将ht[0]->table中的所有键值对迁移到ht[1]->table ;

  3. 将原有的ht[0]的数据清空,并将ht[1]替换成ht[0];

字典收缩和字典扩展的一个区别是:

  • 字典的扩展操作自动触发的(不管是自动扩展还是强制扩展);

  • 字典的收缩操作则是由程序手动执行。因此,使用字典的程序可以决定何时对字典进行收缩:

  • 当字典用于实现哈希键的时候,每次从字典中删除一个键值对,程序就会执行一次htNeedsResize 函数,如果字典达到了收缩的标准,程序将立即对字典进行收缩;

  • 当字典用于实现数据库键空间(key space)的时候, 收缩的时机由redis.c/tryResizeHashTables 函数决定。

8.4.4 总结

  • 字典是由键值对构成的抽象数据结构;

  • Redis 中的数据库和哈希键都是基于字典来实现的;

  • Redis 字典的底层实现为哈希表,每个字典使用两个哈希表,一般情况下只使用0号哈希表,只有在rehash进行时,才会使用0号和1号哈希表;

  • 哈希表使用链地址法来解决键冲突的问题;

  • rehash可以用于扩展和收缩哈希表;

  • 对哈希表的rehash是分多次、渐进式地进行。

8.5 什么是跳表?他有什么特征?

跳跃表是一种随机化的数据,这种数据结构以有序的方式在层次化的链表中保存元素,如下图:

image.png

从上图中我们可以看出跳跃表的结构组成:

  • 表头(head):负责维护跳跃表的节点指针。

  • 跳跃表节点:保存着元素值,以及多个层。

  • 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。

  • 表尾:全部由NULL 组成,表示跳跃表的末尾。

小结:

  1. 跳跃表是一种随机化数据结构,它的查找、添加、删除操作都可以在对数期望时间下完成;

  2. 跳跃表目前在redis的唯一作用就是作为有序集类型的底层数据结构(之一,另一个构成有序集的结构是字典);

  3. 为了适应自身的需求,redis基于William Pugh 论文中描述的跳跃表进行了修改,包括:

    1. score值可重复;
    2. 对比一个元素需要同时检查它的score值和member域;
    3. 每个节点带有高度为1层的后退指针,用于从表尾方向向表头方向迭代。

8.6 Redis有哪些常用命令?

8.6.1 Keys

KEYS命令不能用在生产的环境中,这个时候如果数量过大效率是十分低的。同时也不要用KEYS正则匹配,官方建议直接用集合类型。那怎么解决这种类似的keys模糊匹配问题呢?其中常见的方法就是设置一个set,将需要使用的keys存储在set中。

有人说 KEYS相当于关系性数据的库的 select *,在生产环境几乎是要禁用的。

  • KEYS命令的性能随着数据库数据的增多而越来越慢。
  • KEYS命令会引起阻塞,连续的 KEYS命令足以让 Redis 阻塞。

试想如果Redis阻塞超过10秒,如果有集群的场景,可能导致集群判断Redis已经故障,从而进行故障切换;以上的情况严重会导致应用程序出现雪崩的情况。

8.6.2 SETNX

setNX,是set if not exists 的缩写,也就是只有不存在的时候才设置, 设置成功时返回1,设置失败时返回0。可以利用它来实现分布式锁。

8.6.2.1 setNX实现分布式锁(分布式问题常问!)

多个进程执行以下Redis命令:

SETNX lock.foo <current Unix time + lock timeout + 1>

如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。

如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。

解决死锁

虑一种情况,如果进程获得锁后,断开了与 Redis 的连接(可能是进程挂掉,或者网络中断),如果没有有效的释放锁的机制,那么其他进程都会处于一直等待的状态,即出现“死锁”。

上面在使用 SETNX 获得锁时,我们将键 lock.foo 的值设置为锁的有效时间,进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。

然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。

考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:

  1. P2和P3进程读取键 lock.foo 的值,检测锁是否已超时(通过比较当前时间和键 lock.foo 的值来判断是否超时)
  2. P2和P3进程发现锁 lock.foo 已超时
  3. P2执行 DEL lock.foo命令
  4. P2执行 SETNX lock.foo命令,并返回1,即P2获得锁
  5. P3执行 DEL lock.foo命令将P2刚刚设置的键 lock.foo 删除(这步是由于P3刚才已检测到锁已超时)
  6. P3执行 SETNX lock.foo命令,并返回1,即P3获得锁
  7. P2和P3同时获得了锁

从上面的情况可以得知,在检测到锁超时后,进程不能直接简单地执行 DEL 删除键的操作以获得锁。

为了解决上述算法可能出现的多个进程同时获得锁的问题,我们再来看以下的算法。

我们同样假设进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。接下来的情况:

  1. 进程P4执行 SETNX lock.foo 以尝试获取锁
  2. 由于进程P1已获得了锁,所以P4执行 SETNX lock.foo 返回0,即获取锁失败
  3. P4执行 GET lock.foo 来检测锁是否已超时,如果没超时,则等待一段时间,再次检测
  4. 如果P4检测到锁已超时,即当前的时间大于键 lock.foo 的值,P4会执行以下操作:
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
  1. 由于 GETSET 操作在设置键的值的同时,还会返回键的旧值,通过比较键 lock.foo 的旧值是否小于当前时间,可以判断进程是否已获得锁
  2. 假如另一个进程P5也检测到锁已超时,并在P4之前执行了 GETSET 操作,那么P4的 GETSET 操作返回的是一个大于当前时间的时间戳(因为设置时间是当前时间加上一个lock timeout的锁维持时间,所以肯定比P4的当前时间要大),这样P4就不会获得锁而继续等待。注意到,即使P4接下来将键 lock.foo 的值设置了比P5设置的更大的值也没影响。

另外,值得注意的是,在进程释放锁,即执行 DEL lock.foo 操作前,需要先判断锁是否已超时。 如果锁已超时,那么锁可能已由其他进程获得,这时直接执行 DEL lock.foo 操作会导致把其他进程已获得的锁释放掉。

总结

  1. 执行SETNX lock.foo 以尝试获取锁
  2. SETNX lock.foo返回0获取锁失败,执行GET lock.foo 来检测锁是否已超时,没有超时等待一段时间;
  3. 超时了,执行GETSET lock.foo,会在设置值的同时返回旧的值,比较旧的值与设置的值,如果旧的值比设置值要大,则放弃获取锁,继续等待。(此时设置了值也没关系,因为另外一个进程判断旧值比设置值要小,已经获取到了锁)
  4. 直到获取到锁为止。

tips:或者直接用setnx + expire,合并成一条指令,时间到了自动释放锁。

8.6.3 expire

expire是设置redis过期时间的命令,需要注意的点有以下几点:

  1. expire设置过期时间的单位是秒,如设置name的过期时间为1000秒:
expire name 1000 
  1. 超过时间后会自动删除key,但是不一定是立即删除,因为redis的过期策略是惰性删除和定期删除的策略。

  2. 超过时间以后,所有会改变此key的值都会立即触发对key的删除操作,例如:del,set,getset命令;另一种删除方式就是定期删除策略即redis会定期检查过期的key,然后统一删除。

  3. persist命令可以清除超时,让key变成一个永久的key

  4. rename命令,realName这个key原来就不存在,会将原来的key的过期时间转换到新的key上,算是移花接木吧!如下,那么realName的过期时间是5秒;假如realName这个key原来就有,那么realName会被nickName完全覆盖,不管realName原来是永久key还是过期key。

expire nickName 10
-- 期间过了5秒钟
rename nickName realName

6.expire设置的过期时间是与电脑设备的时钟相关的,比如你设置某key的过期时间为1000,但是在1000之内的时间范围内,你修改了电脑的时间为2000之后,那么此key会立即过期。所以redis的过期时间不是要持续多长时间,而是和电脑时钟相关联。

8.7 Redis的持久化机制是怎样的?(问Redis必问!)

redis是一个内存数据库,数据保存在内存中,但是我们都知道内存的数据变化是很快的,也容易发生丢失。幸好Redis还为我们提供了持久化的机制,分别是RDB(Redis DataBase)和AOF(Append Only File)。

持久化流程:
既然redis的数据可以保存在磁盘上,那么这个流程是什么样的呢?
要有下面五个过程:

  1. 客户端向服务端发送写操作(数据在客户端的内存中)。

  2. 数据库服务端接收到写请求的数据(数据在服务端的内存中)。

  3. 服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。

  4. 操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。

  5. 磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)。

这5个过程是在理想条件下一个正常的保存流程,但是在大多数情况下,我们的机器等等都会有各种各样的故障,这里划分了两种情况:

  1. Redis数据库发生故障,只要在上面的第三步执行完毕,那么就可以持久化保存,剩下的两步由操作系统替我们完成。

  2. 操作系统发生故障,必须上面5步都完成才可以。

在这里只考虑了保存的过程可能发生的故障,其实保存的数据也有可能发生损坏,需要一定的恢复机制,不过在这里就不再延伸了。现在主要考虑的是redis如何来实现上面5个保存磁盘的步骤。它提供了两种策略机制,也就是RDB和AOF。

8.7.1 RDB

RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。(在redis.conf中进行RDB和AOF两种持久化机制的各种配置)

RDB的触发机制
对于RDB来说,提供了三种触发备份机制:save、bgsave、自动化。

  1. save:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。执行完成时候如果存在老的RDB文件,就用新的替代掉旧的。我们的客户端可能都是几万或者是几十万,这种方式显然不可取。

  2. bgsave:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。

  3. 自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置,我们可以去设置:

  • save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
  • stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了。
  • rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。
  • rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
  • dbfilename :设置快照的文件名,默认是 dump.rdb
  • dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。

RDB的优劣势:

  1. 优势
    (1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
    (2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
    (3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

  2. 劣势
    RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。

特点总结:默认5分钟备份一次;冷备份(生成快照,对新数据不敏感);恢复的时候快;因为是全量备份,快照文件生成时间久,消耗cpu。

8.7.2 AOF

全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。

image.png

每当有一个写命令过来时,就直接保存在我们的AOF文件中。
AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。
重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

AOF的触发机制

  1. 每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好

  2. 每秒同步everysec(最常用):异步操作,每秒记录 如果一秒内宕机,有数据丢失

  3. 不同no:从不同步

AOF的优点:

  1. AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。

  2. AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。

  3. AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。

  4. AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

AOF的缺点:

  1. 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

  2. AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

  3. 以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。

AOF总结:appendOnly,只是在AOF的文件后面做命令追加;数据齐全;恢复大文件慢。

tips:一般来说,实际使用中将RDB和AOF结合在一起使用,一个做冷备,一个做热备。

8.8 Redis集群下的数据同步机制是怎样的?

8.8.1 主从同步

Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。

image.png

全量同步:

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

  • 从服务器连接主服务器,发送SYNC命令;
  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

增量同步:

Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。

增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

Redis主从同步策略:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)。因为内存的 buffer 是有限的,所以 Redis 主库不能将所有的指令都记录在内存 buffer中。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。

如果因为网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢复时,Redis 的主节点中那些没有同步的指令在 buffer 中有可能已经被后续的指令覆盖掉了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制 —— 快照同步。

8.8.2 快照同步

快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次 bgsave 将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步。在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环。

所以务必配置一个合适的复制 buffer 大小参数,避免快照复制的死循环。

8.8.3 无盘复制

主节点在进行快照同步时,会进行很重的文件 IO 操作,特别是对于非 SSD 磁盘存储时,快照会对系统的负载产生较大影响。特别是当系统正在进行 AOF 的 fsync 操作时如果发生快照,fsync 将会被推迟执行,这就会严重影响主节点的服务效率。所以从 Redis 2.8.18 版开始支持无盘复制。所谓无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。

8.9 Redis集群下的哨兵机制了解吗?

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

image.png

这里的哨兵有两个作用:

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

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

  1. 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。

  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds(默认为30s) 选项所指定的值,则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)。

  3. 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态。

  4. 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值,一般为N/2+1个)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)。

  5. 在一般情况下, 每个Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。

  6. 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

  7. 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

8.10 什么是脑裂?如何解决脑裂问题?

何为脑裂?当一个集群中的 master 恰好网络故障,导致与 sentinal 通信不上了,sentinal会认为master下线,且sentinal选举出一个slave 作为新的 master,此时就存在两个 master了。

此时,可能存在client还没来得及切换到新的master,还继续写向旧master的数据,当master再次恢复的时候,会被作为一个slave挂到新的master 上去,自己的数据将会清空,重新从新的master 复制数据,这样就会导致数据缺失。

总结:主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。

数据丢失可以通过合理地配置参数 min-slaves-to-write 和 min-slaves-max-lag 解决,比如

  • min-slaves-to-write:1
  • min-slaves-max-lag:10

如上两个配置:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒,如果超过 1 个 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。

**数据不一致:**在主从异步复制过程,当从库因为网络延迟或执行复杂度高命令阻塞导致滞后执行同步命令,这样就会导致数据不一致。

解决方案:可以开发外部程序来监控主从库间的复制进度(master_repl_offset 和 slave_repl_offset ),通过监控 master_repl_offset 与slave_repl_offset差值得知复制进度,当复制进度不符合预期设置的Client不再从该从库读取数据。

8.11 Redis-Cluster

Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

image.png

其结构特点:

  1. 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
  2. 节点的fail是通过集群中超过半数的节点检测失效时才生效。
  3. 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
  4. redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。
  5. Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。

8.11.1 redis cluster节点分配

现在我们是三个主节点分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot)的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:

  • 节点A覆盖0-5460;
  • 节点B覆盖5461-10922;
  • 节点C覆盖10923-16383.

8.11.2 Redis Cluster主从模式

redis cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。

上面那个例子里, 集群有ABC三个主节点, 如果这3个节点都没有加入从节点,如果B挂掉了,我们就无法访问整个集群了。A和C的slot也无法访问。

所以我们在集群建立的时候,一定要为每个主节点都添加了从节点, 比如像这样, 集群包含主节点A、B、C, 以及从节点A1、B1、C1, 那么即使B挂掉系统也可以继续正确工作。

B1节点替代了B节点,所以Redis集群将会选择B1节点作为新的主节点,集群将会继续正确地提供服务。 当B重新开启后,它就会变成B1的从节点。

不过需要注意,如果节点B和B1同时挂了,Redis集群就无法继续正确地提供服务了。

8.11.3 redis集群的搭建

集群中至少应该有奇数个节点,所以至少有三个节点,每个节点至少有一个备份节点,所以应该最少使用6节点(主节点、备份节点由redis-cluster集群确定)

8.12 Redis常见问题

8.12.1 什么是缓存雪崩?如何解决?(常问)

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决方案:
缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

总结:

  1. 避免缓存集中失效,不同的key设置不同的超时时间(设置随机值)

  2. 增加互斥锁,控制数据库请求,重建缓存。

  3. 提高缓存的HA,如:redis集群。

8.12.2 什么是缓存击穿?如何解决?(常问)

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

解决方案:

  1. 设置热点数据永远不过期。

  2. 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。

  3. 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小。

  4. 加互斥锁

8.12.3 什么是缓存穿透?如何解决?(常问)

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

8.12.4 如何保证MYSQL和Redis的双写一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。

串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

但是一般来说,Redis中的数据都会比MYSQL的数据要新,会定期将Redis中的数据同步到MYSQL中。

延时双删

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:

  1. 先删除缓存
  2. 再写数据库
  3. 休眠500毫秒(根据具体的业务时间来定)
  4. 再次删除缓存。

那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。

这种方案解决了高并发情况下,同时有读请求与写请求时导致的不一致问题。读取速度快,但是可能会出现短时间的脏数据。

8.12.5 Redis大key问题,如何解决?

Redis使用过程中经常会有各种大key的情况, 比如:

  1. 单个简单的key存储的value很大
  2. hash, set,zset,list 中存储过多的元素(以万为单位)

bigkey会带来一些问题,如:

  1. 读写bigkey会导致超时严重,甚至阻塞服务。
  2. 大key相关的删除或者自动过期时,会出现qps突降或者突升的情况,极端情况下,会造成主从复制异常,Redis服务阻塞无法响应请求。

redis 是单线程,操作 bigkey 比较耗时,那么阻塞 redis 的可能性增大。每次获取 bigKey 的网络流量较大,假设一个 bigkey 为 1MB,每秒访问量为 1000,那么每秒产生 1000MB 的流量,对于普通千兆网卡,按照字节算 128M/S 的服务器来说可能扛不住。而且一般服务器采用单机多实例方式来部署,所以还可能对其他实例造成影响。

bigkey解决方案:

  1. 单个简单的key存储的value很大

    1. 对象需要每次都整存整取:可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;

    2. 该对象每次只需要存取部分数据:可以像第一种做法一样,分拆成几个key-value, 也可以将这个存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性

  2. hash, set,zset,list 中存储过多的元素

可以对存储元素按一定规则进行分类,分散存储到多个redis实例中。

对于一些榜单类的场景,用户一般只会访问前几百及后几百条数据,可以只缓存前几百条以及后几百条,即对用户经常访问的数据做缓存(正序倒序的前几页),而不是全部都做,对于获取中间的数据则可以直接从数据库获取。

  1. 一个集群存储了上亿的key

如果key的个数过多会带来更多的内存空间占用。

  1. key本身的占用。
  2. 集群模式中,服务端有时需要建立一些SlotToKey的映射关系,这其中的指针占用在key多的情况下也是浪费巨大空间。

所以减少key的个数可以减少内存消耗,可以参考的方案是转Hash结构存储,即原先是直接使用Redis String 的结构存储,现在将多个key存储在一个Hash结构中。

找bigkey方式:

  1. 自带命令redis-cli --bigkeys:该命令是redis自带,但是只能找出五种数据类型里最大的key。

  2. Redis 4.0引入了memory usage命令和lazyfree机制:

    1. 命令MEMORY USAGE 给出一个key和它值在RAM中占用的字节数;

    2. lazy free可译为惰性删除或延迟释放;当删除键的时候,redis提供异步延时释放key内存的功能,把key释放操作放在bio(Background I/O)单独的子线程处理中,减少删除big key对redis主线程的阻塞。有效地避免删除big key带来的性能和可用性问题。

8.12.6 系统中遇到热点key的问题是如何解决的?

热点问题产生的原因大致有以下两种:

  1. 用户消费的数据远大于生产的数据(热卖商品、热点新闻、热点评论、明星直播)
  2. 请求分片集中,超过单 Server 的性能极限:在服务端读数据进行访问时,往往会对数据进行分片。此过程中会在某一主机 Server 上对相应的 Key 进行访问,当访问超过 Server 极限时,就会导致热点 Key 问题的产生(即很多用户集中访问某一个Sever上的数据)。

热点key的危害

  • 流量集中,达到物理网卡上限。
  • 请求过多,缓存分片服务被打垮。
  • DB 击穿,引起业务雪崩。

当某一热点 Key 的请求在某一主机上超过该主机网卡上限时,由于流量的过度集中,会导致服务器中其它服务无法进行。如果热点过于集中,热点 Key 的缓存过多,超过目前的缓存容量时,就会导致缓存分片服务被打垮现象的产生。当缓存服务崩溃后,此时再有请求产生,会缓存到后台 DB 上,由于DB 本身性能较弱,在面临大请求时很容易发生请求穿透现象,会进一步导致雪崩现象,严重影响设备的性能。

解决方案:

  1. 热点key缓存时间不失效
  2. 多级缓存:比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。针对这种热key请求,会直接从jvm中取,而不会走到redis层。
  3. 布隆过滤器
  4. 读写分离

8.13 Redis的缓存过期策略有哪几种?分别有什么特点?

内存过期策略主要的作用就是,在缓存过期之后,能够及时的将失效的缓存从内存中删除,以减少内存的无效暂用,达到释放内存的目的。Redis内存过期策略分为三类,定时删除、惰性删除和定期删除

8.13.1 定时删除

含义:在设置key的过期时间的同时,为该key创建一个定时器,当定时器在key的过期时间来临时,对key进行删除。

优点:保证内存被尽快释放,减少无效的缓存暂用内存。

缺点:若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key。定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重。一般来说,是不会选择该策略模式。

8.13.2 惰性删除

含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。

优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)。

缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,此时的无效缓存是永久暂用在内存中的,那么可能发生内存泄露(无用的过期key占用了大量的内存)。

8.13.3 定期删除

含义:每隔一段时间对设置了缓存时间的key进行检测,如果可以已经失效,则从内存中删除,如果未失效,则不作任何处理。

优点:通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用–处理"定时删除"的缺点 定期删除过期key–处理"惰性删除"的缺点。

缺点:在内存友好方面,不如"定时删除",因为是随机遍历一些key,因此存在部分key过期,但遍历key时,没有被遍历到,过期的key仍在内存中。在CPU时间友好方面,不如"惰性删除",定期删除也会暂用CPU性能消耗。

难点:合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)

8.14 Redis的内存淘汰机制有哪些?

内存淘汰机制针对是内存不足的情况下的一种Redis处理机制。例如,当前的Redis存储已经超过内存限制了,然而我们的业务还在继续往Redis里面追加缓存内容,这时候Redis的淘汰机制就起到作用了。

根据redis.conf的配置文件中,我们可以得出,主要分为如下六种淘汰机制。

  1. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,利用LRU(按最近使用顺序排序)移除最近最少使用的key。

  2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)。

  3. volatile-lfu:当内存不足以容纳新写入数据时,在过期密集的键中,使用LFU(最近最不经常使用,数最近用的次数)算法进行删除key。

  4. allkeys-lfu:当内存不足以容纳新写入数据时,使用LFU算法移除所有的key。

  5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期的键中,随机删除一个key。

  6. allkeys-random:当内存不足以容纳新写入数据时,随机删除一个或者多个key。

  7. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

  8. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

8.15 Redis有哪些限流策略?

8.15.1 setnx ex

我们在使用Redis的分布式锁的时候,大家都知道是依靠了setnx的指令,在CAS(Compare and swap)的操作的时候,同时给指定的key设置了过期时间(expire),我们在限流的主要目的就是为了在单位时间内,有且仅有N数量的请求能够访问我的代码程序。所以依靠setnx可以很轻松的做到这方面的功能。

比如我们需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,当请求的setnx数量达到20时候即达到了限流效果。代码比较简单就不做展示了。

当然这种做法的弊端是很多的,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题。

8.15.2 zset

我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而key可以用当前时间戳表示,因为key我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求。缺点就是zset的数据结构会越来越大。

8.15.3 令牌桶(Redis rateLimiter)

令牌桶算法(Token Bucket)和 Leaky Bucket(漏桶) 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量。

总结:
1.定时push
2.然后leftpop
3.问题:
1. 空轮询
2. blpop
3. 空连接异常:重试

8.15.4 漏桶算法(Leaky Bucket)

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

image.png

总结:

  1. make_space 灌水之前调用漏水 腾出空间 取决于流水速率
  2. Hash原子性有问题:redis-cell

8.15.5 令牌桶和漏桶算法的比较

并不能说明令牌桶一定比漏洞好,她们使用场景不一样。令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。而漏桶算法,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。

总结:如果要让自己的系统不被打垮,用令牌桶。如果保证别人的系统不被打垮,用漏桶算法。

这篇关于一问Redis只知道是key-value类型数据库?它可远不止这些!的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!