Redis学习笔记

什么是Redis?

  • Redis是一款基于键值对的NoSQL数据库,key是String类型,它的value支持多种数据结构:字符串、哈希、链表、集合、有序集合等。(使得Redis 能够在实际业务场景中得到广泛的应用)

  • Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。

同时,Redis还可以将内存中的数据以快照(RDB,整体拷贝,会产生阻塞、不适合实时备份)或日志(AOF, 增量存,存指令,数据体积大、恢复慢)的形式保存到硬盘上,以保证数据的安全性。

  • Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。

Redis 和 Memcached、RocksDB 的对比

Memcached 和 RocksDB 分别是典型的内存键值数据库和硬盘键值数据库,应用得也非常广泛。

如果我们需要部署大规模缓存集群,Memcached 会是一个不错的选择。

RocksDB 还能支持表结构(即列族结构),如果你需要一个大容量的持久化键值数据库,并且能按照一定表结构保存数 据,RocksDB 是一个不错的替代方案。

支持的数据类型和大小

Redis的数据结构

Redis 之所以能快速操作键值对,一方面是因为 O(1) 复杂度的哈希表被广泛使用,包括 String、Hash 和 Set,它们的操作复杂度基本由哈希表决定,另一方面,Sorted Set 也采用了 O(logN) 复杂度的跳表。不过,集合类型的范围操作,因为要遍历底层数据结构,复杂度通常是 O(N)。这里,我的建议是:用其他命令来替代,例如可以用 SCAN 来代替, 避免在 Redis 内部产生费时的全集合遍历操作。(Scan命令的时间复杂度虽然也是O(N),但它是分次进行的,scan每次都只遍历一小部分数据,不会阻塞线程。Scan命令提供了 count 参数,可以控制每次遍历的集合数。SCAN命令是一个基于游标的迭代器, 这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程)

对于复杂度较高的 List 类型,它的两种底层实现结构:双向链表和压缩列表的操作复杂度都是 O(N)。因此,我的建议是:因地制宜地使用 List 类型。例如,既然 它的 POP/PUSH 效率很高,那么就将它主要用于 FIFO 队列场景,而不是作为一个可以随机读写的集合。

整数数组和压缩列表的设计,充分体现了Redis又快又省中的”省“,也就是节省内存空间。整数数组和压缩列表都是在内存中分配一块地址连续的空间,然后把集合中的元素一个接一个放在空间中,非常紧凑,避免了额外指针的空间开销。

Redis之所以采用不同的数据结构,是在性能和内存使用效率之间进行的平衡。

String

是redis中最基本的数据类型,一个key对应一个value。
⾃⼰构建了⼀种简单动态字符串SDS,不光可以保存⽂本数据还可以保存⼆进制数据,redis的string可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
使用:get 、 set 、 del 、 incr、 decr 等
应⽤场景 :⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等等。

String类型内存开销大(不适合保存大量数据)

因为在简单动态字符串SDS中,除了记录实际数据,还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫元数据。当实际保存的数据较小时,元数据的空间开销就显得比较大了。”喧宾夺主“

用String保存大量单值数据,存在大内存Redis实例生成RDB响应变慢的问题。

考虑用集合的压缩列表(ziplist),表头有三个字段,分别表示列表长度、列表尾的偏移量,表中entry个数。表尾zlend表示列表结束。由一系列连续的entry保存数据,节省内存。

采用基于Hash类型的二级编码方法,让集合保存单值的键值对。把一个单值的数据拆分成两部分,前一部分作为Hash集合的key,后一部分作为Hash集合的value。

list

Redis实现了⾃⼰的链表数据结构,为⼀个双向链表,即可以⽀持反向查找和遍历,更⽅便操作,不过带来了部分额外的内存开销。
常⽤命令: rpush,lpop,lpush,rpop,lrange、llen 等。
应⽤场景: 发布与订阅或者说消息队列、慢查询。

hash

类似于JDK1.8之前的HashMap,数组+链表。做了更多优化。是一个Mapmap,指值本身又是一种键值对结构。 value={{field1,value1},......fieldN,valueN}};
后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。
常⽤命令: hset,hset,hexists,hget,hgetall,hkeys,hvals 等。
应⽤场景: 系统中对象数据的存储,存储⽤户信息,商品信息
​
Hash 类型不支持对数据进行范围查询:
Hash 类型的底层结构是哈希表,并没有对数据进行有序索引。所以,如果要对 Hash 类型进行范围查询的话,就需要扫描 Hash 集合中的所有数据,再把这些数据取回到客户端进行排序,然后,才能在客户端得到所查询范围内的数据。显然,查询效率很低。
​

底层两种实现结构,压缩列表和哈希表。

Hash类型设置了用压缩列表保存数据的两个阈值,集合中的最大元素个数和集合中单个元素的最大长度;两个超过了任一个,redis就会自动把Hash类型的实现结构由压缩列表转为哈希表。(不会再转回)节省内存方面,哈希表不如压缩列表。

set

类似于Java中的HashSet
常⽤命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
应⽤场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景

set底层使用intset和hashtable存储数据。

当两个条件满足任一个:1.元素无法用整型表示;2.存储元素超过默认值512时,会换用hashtable。

intset,整数集合,包括:编码方式,集合包含的元素数量,以及保存元素的数组。

可以以16/32/64位存整数,这取决于它自己的类型编码。

zset(sorted set)

和set相⽐,sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score
进⾏有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap
和 TreeSet 的结合体。可以范围查询
常⽤命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
3.应⽤场景: 需要对数据根据某个权重进⾏排序的场景。⽐如在直播系统中,实时排⾏信息包
含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息(可以理解为按消息维度的消息排⾏
榜)等信息。

扩展数据类型

HyperLogLog高级日志

采用一种基数算法,用于完成独立总数的统计

  • 占据空间小,无论统计多少个数据,只占12K的内存空间。

  • 不精确的统计算法,标准误差为0.81%。

  • PFCount直接统计

Bitmap位图

不是一种独立的数据结构,实际上底层就是String类型。

用bit数组来记录0-1两种状态,然后再将具体数据映射到这个比特数组的具体位置,这个比特位设置成0表示数据不存在,设置成1表示数据存在。数据映射到bit上,一个字节就可以存8个数据。

BitMap算在在大量数据查询、去重等应用场景中使用的比较多,这个算法具有比较高的空间利用率。

  • 支持按位存取数据,可以将其看成是byte数组。

  • 适合存储大量的连续的数据的布尔值。

  • 提供了BitCount操作,统计bit数组中所有1的个数。

  • 支持与、或、异或运算。

高级日志和位图,都适合用来对网站的运营数据进行统计,并且在统计的时候十分节省内存。

HyperLogLog:性能好,且存储空间小;

Bitmap:性能好、且可以统计精确的结果。

GEO

可以记录经纬度形式的地理位置信息,被广泛应用在LBS(位置信息服务)服务中。GEO直接使用了Sorted Set集合。

GEOADD:把一组经纬度信息和对应的id记录到集合中;

GEORADIUS:根据输入的经纬度位置,查找以其为中心一定范围内的元素。

以叫车服务为例,一个车有自己的编号,需要将自己的经纬度信息发给叫车应用。一个key(车id)对应一个value(一组经纬度)。Hash集合可以用来记录,但是对于一个LBS应用来说,除了记录经纬度信息,还需要根据根据经纬度信息进行范围查询,涉及到范围查询,需要集合中元素有序,所以选择Zset

GEO使用GeoHash编码方法实现了经纬度到Zset中元素权重分数的转换。

GeoHash编码会把一个经度值编码成一个N位的二进制值。二分区间,区间编码。

对于一个地理位置信息来说,它的经度范围是[-180,180]。来对经度范围[-180,180]做 N 次的二分区操作,其中 N 可以自定义。在进行第一次二分区时,经度范围[-180,180]会被分成两个子区间:[-180,0) 和[0,180] (左、右分区)。查看一下要编码的经度值落在了左分区还是右分区。左分区用 0 表示;右分区用 1 表示。每做完一次二分区就得到 1 位编码值。然后,我们再对经度值所属的分区再做一次二分区,按照刚才的规则再做 1 位编码。当做完 N 次的二分区后,经度值就 可以用一个 N bit 的数来表示了。
​
纬度相同,当一组经纬度值都编完码后,把它们各自编码值组合在一起,组合的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始。
​
使用 GeoHash 编码后,我们相当于把整个地理空间划分成了一个个方格,每个方格对应了 GeoHash 中的一个分区。

跳表

跳跃表是有序集合的底层实现之一(zset)。
跳表是对链表的一个增强。我们在使用链表的时候,即使元素是有序排列的,但如果要查找一个元素,也需要从头一个个查找下去,时间复杂度是O(N)。
跳表顾名思义,就是跳跃了一些元素,可以抽象多层。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。复杂度O(logn)
​
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。zskiplistNode内部有很多层L1、L2等,指针指向这一层的下一个结点。
​
​
在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
​
SkipList具备如下特性:
由很多层结构组成,level是通过一定的概率随机产生的。每个跳跃表节点的层高都是1至32之间的随机数。
每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法。
最底层(Level 1)的链表包含所有元素
如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现。
每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

左边蓝色部分是skiplist,右边是4个zskiplistNode

跳跃表的查询是从顶层往下找,那么会先从第顶层开始找,方式就是循环比较,如果顶层节点的下一个节点为空说明到达末尾,会跳到第二层,继续遍历,直到找到对应节点。(如果节点大于要查找的数,也会跳到下一层)

查找66,先在L3层发现 66 大于 1。继续找顶层的下一个节点,发现 66 也是大于五的,继续遍历。由于下一节点为空,则会跳到 level 2。
跳到L2的5,发现下一个节点100>66,会跳到下一层L1的5,下一个节点66满足,找到。

跳跃表的删除和查找类似,都是一级一级找到相对应的节点,然后将 next 对象指向下下个节点,完全和链表类似。

基本对象结构 :RedisObject

RedisObject的内部组成包括了四个元数据和一个指针。

RedisObject 结构借助ptr指针,就可以指向不同的数据类型,例如,ptr指向一个 SDS 或一个跳表,就表示键值对中的值是 String 类型或 Sorted Set 类型

集合统计模式

聚合统计 统计多个集合元素的聚合结果,交集、差集、并集等。

统计每天的新增用户数和第二天留存用户数:

一个累计用户set记录所有登陆过的用户id;一个每日用户set记录每日用户id,key是当天日期,value是当天登陆的用户ID;

每天的新增用户就是每日用户Set和累计用户set的差集。第二天留存用户就是第二天用户set和前一天用户set的交集。

注意:Set的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行会导致Redis实例阻塞。可以从主从集群中选择一个从库,让他专门负责聚合计算,或者把数据读取到客户端,在客户端来完成聚合统计。

排序统计

list是按照元素进入List的顺序进行排序,Sorted Set根据元素的权重进行排序。

list在分页操作时,当有新元素插入,原来的元素在list中的位置会移动,可能会导致LRANGE按位置读取的数据不准确;所以在面对需要展示最新列表、排行榜等场景,如果数据更新频繁或需要分页显示,优先考虑使用Sorted Set。

二值状态统计

集合元素的取值只有0和1两种,比如签到打卡。

用Bitmap。

统计1亿个用户10天连续签到情况:

可以把每天的日期作为key,每一个key对应1亿位的Bitmap,每一个bit对应一个用户当天签到情况。

对10个Bitmap做”与“操作,得到的结果也是一个Bitmap,在这个Bitmap上值为1对应10天都签到的用户。

基数统计

指统计一个集合中不重复的元素个数,比如网页的独立访客UV。

当数据量很大时,考虑用HyperLogLog,统计结果有一定的误差。

保存时间序列数据

与发生时间相关的一组数据,没有严格的关系模型,仅表示为键和值的关系。

比如记录用户行为,统计设备的实时状态。

时间序列数据通常是持续高并发写入的,写入主要是插入新数据,不是更新已存在的数据。

要求快速写入,查询特点有三个:点查询(根据一个时间戳,查相应时间的数据),范围查询(查询起始和截至时间戳范围内的数据),聚合查询(针对起始和截至时间戳范围内的数据进行计算,例如求最大最小值,求均值等)。有两种方案

基于Hash和Sorted Set类型

既可以利用Hash类型实现对单键的快速查询,还能利用Sorted Set实现对范围的高效查询。

要保证写入Hash和Sorted Set是一个原子性的操作,涉及到事务的MULTI和EXEC命令。

但是该方案有两个不足,1.在聚合计算时,需要把数据读取到客户端再进行聚合,当有大量数据要聚合时,数据传输开销大;2.所有的数据会在两个数据类型中各保存一份,内存开销不小,可以通过设置适当的数据过期时间,释放内存,减少内存压力。

基于RedisTimeSeries模块

专门为存储时间序列数据设计的拓展模块。

能直接支持再Redis实例上进行多种数据聚合计算,避免了大量数据在实例和客户端之间传输。

底层数据结构使用了链表,范围查询复杂度是O(N)。同时,它的TS.Get查询只能返回最新的数据,没办法像Hash类型一样,返回任一时间点的数据。

建议:

如果你的部署环境中网络带宽高、Redis实例内存大,优先考虑第一种;

如果你的部署环境中网络、内存资源有限、而且数据量大,聚合计算频繁,需要按数据集合属性查询,优先考虑第二种。

过期、淘汰策略

过期策略

Redis会把设置了过期时间的key放入一个独立的字典里,在key过期时并不会立刻删除它。

Redis会通过两种策略,来删除过期的key:

惰性删除:客户端访问某个key时,redis会检查该key是否过期,过期则删除。

定期扫描:redis默认每秒执行10次过期扫描,扫描策略:1.从过期字典中随机选择20个key;2.删除这20个key中已过期的key;3.如果过期的key的比例超过25%,重复步骤1。

淘汰策略

仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致⼤量过期 key 堆积在内存⾥。

当Redis占用内存超出最大限制时,可采用如下策略让redis淘汰一些数据,以腾出空间继续提供读写服务。

Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略。

1.noeviction默认方式:不淘汰,对可能导致增大内存的命令返回错误(大多数写命令,delete除外);
2.volatile-ttl:在设置了过期时间的key中,选择剩余寿命最短的key,将其淘汰;
3.volatile-lru:在设置了过期时间的key中,选择最少使用的key(LRU),将其淘汰;
4.volatile-random:在设置了过期时间的key中,随机选择一些key,将其淘汰;
5.allkeys-lru:在所有的key中,选择最少使用的key(LRU),将其淘汰;(最常用)
6.allkeys-random:在所有的key中,随机选择一些key,将其淘汰。
Redis4.0以后新增
7.volatile-lfu: 会使用LFU算法选择设置了过期时间的键值对。
8.allkeys-lfu: 策略,使用LFU算法在所有数据中进行筛选。

LRU算法(最近最少使用):维护一个链表,最新访问过的数据放在表头,最少访问的在表尾;
核心思想:如果一个数据刚刚被访问,那么这个数据肯定是热数据,还会被再次访问。
redis用的近似LRU算法:Redis是用RedisObject结构来保存数据的,RedisObject结构中设置了一个lru字段,用来记录数据的访问时间戳;给每个key维护一个时间戳,淘汰时随机采样5个时间戳,从中淘汰掉最旧的key。如果还是超出内存限制,继续随机采样淘汰。(比LRU节约内存)

LFU:与 LRU 策略相比,LFU 策略中会从两个维度来筛选并淘汰数据:一是,数据访问的时效性(访问时间离当前时间的远近);二是,数据的被访问次数。当使用LFU策略筛选淘汰数据时,首先会筛选并淘汰访问次数少的数据,然后针对访问次数相同的数据,再筛选并淘汰访问时间最久远的数据。
相对于LRU策略,Redis只是把原来24bit大小的lru字段,又进一步拆分成了16bit的ldt和8bit的 counter,分别用来表示数据的访问时间戳和访问次数。为了避开8bit最大只能记录255的限制,LFU策略设计使用非线性增长的计数器来表示数据的访问次数。
通过设置不同的 lfu_log_factor 配置项,来控制计数器值增加的速度;
使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减(考虑到在一些场景下,有些数据在短时间内被
大量访问后就不会再被访问了。那么再按照访问次数来筛选的话,这些数据会被留存在缓存中,但不会提升缓存命中率。)

作为缓存使用建议:

如果你的业务数据中有明显的冷热数据区分,我建议你使用 allkeys-lru。

如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。

如果你的业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。

缓存被污染了怎么办

缓存污染:在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一 次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。

如果发生了缓存污染,缓存对业务应用的加速作用就减少了。

仅在业务层明确知道数据的访问时长,可以给数据设置合理的过期时间,再设置 Redis 缓存使用 volatile-ttl 策略。

因为只看数据的访问时间,使用 LRU 策略在处理扫描式单次查询操作时,无法解决缓存污染。所谓的扫描式单次查询操作,就是指应用对大量的数据进行一次全体读取,每个数据都会被读取,而且只会被读取一次。此时,因为这些被查询的数据刚刚被访 问过,所以 lru 字段值都很大。

由于 LRU 策略只考虑数据的访问时效,对于只访问一次的数据来说,LRU 策略无法很快将其筛选出来。而 LFU 策略在 LRU 策略基础上进行了优化,在筛选数据时,首先会筛选并淘汰访问次数少的数据,然后针对访问次数相同的数据,再筛选并淘汰访问时间最久远的数据。

LRU 和 LFU 两个策略关注的数据访 问特征各有侧重,LRU 策略更加关注数据的时效性,而 LFU 策略更加关注数据的访问频次。通常情况下,实际应用的负载具有较好的时间局部性,所以 LRU 策略的应用会更加广 泛。但是,在扫描式查询的应用场景中,LFU 策略就可以很好地应对缓存污染问题了,建议优先使用。

旁路缓存

Redis具有高性能访问和数据淘汰机制,非常适合做缓存。

旁路缓存:业务应用使用 Redis 缓存时,就要在应用程序中增加相应的缓存操作代码。读取缓存、读取数据库和更新缓存的操作都需要在应用程序中来完成。如果是无法修改源码的应用场景,就不能使用 Redis 做缓存了。对应的有计算机系统中的 CPU 缓存和 page cache。这两种缓存默认就在应用程序访问内存和磁盘的路径上,我们写的应用程序都能直接使用这两种缓存。

两种模式:只读缓存和读写缓存

只读缓存:所有的数据写请求,会直接发往后端的数据库,在数据库中增删改。最新数据都在数据库中,不会有丢失数据的风险。

读写缓存:所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。

还提供了同步直写和异步写回这两种模式,

同步直写模式侧重于保证数据可靠性,写请求发给缓存的同时,也会发给后端数据库进行处理,等到缓存和数据库都写完数据,才给客户端返回。数据库处理写请求较慢,会增加缓存的响应延迟。

异步写回模式则侧重于提供低延迟访问,所有写请求都先在缓存中处理。等到这些增改的数据要被从缓存中淘汰出来时,缓存将它们写回后端数据库。如果发生了掉电,而它们还没有被写回数据库,就会有丢失的风险(使用 Redis 缓存不会使用,没办法写回数据库)

如果需要对写请求进行加速,我们选择读写缓存; 如果写请求很少,或者是只需要提升读请求的响应速度的话,我们选择只读缓存。

缓存容量大小

结合实际应用的数据总量、热数据的体量,以及成本预算,把缓存空间大小设置在总数据量的 15% 到 30% 这个区间就可以。

缓存穿透、缓存击穿、缓存雪崩

缓存雪崩或击穿时,一旦数据库中的数据被再次写入到缓存后,应用又可以在缓存中快速访问数据了,数据库的压力也会相应地降低下来,而缓存穿透发生时, Redis 缓存和数据库会同时持续承受请求压力。

缓存穿透

场景:查询根本不存在的数据,既不在 Redis 缓存中,也不在数据库中,使得请求直达存储层,多次请求导致其负载过大,甚至宕机。

业务层误操作:缓存中的数据和数据库中的数据被误删除了;恶意攻击:专门访问数据库中没有的数据。

解决方案:

1.缓存空对象或缺省值

缓存层未命中后,仍然将空值存入缓存层。再次访问该数据时,缓存层直接返回空值。

2.布隆过滤器

将所有存在的key提前存入布隆过滤器,在访问缓存层之前,先通过过滤器拦截,若请求的是不存在的key,则直接返回空值。避免从数据库中查询数据。

3.在请求入口的前端进行请求检测

缓存穿透的一个原因是有大量的恶意请求访问不存在的数据,所以,一个有效的应对方案是在请求入口前端,对业务系统接收到的请求进行合法性检测,把恶意的请求(例如请求参数不合理、请求参数是非法值、请求字段不存在)直接过滤掉,不让它们访问后端缓存和数据库。

缓存击穿

场景:一份热点数据,他的访问量非常大。在其缓存失效瞬间,大量请求直达存储层,导致服务崩溃。

解决方案:

1、永不过期

不设置过期时间,就不会出现上述问题,这是“物理”上的不过期。

或者为每个value设置逻辑过期时间,当发现该值逻辑过期时,使用单独的线程重建缓存。

2、加互斥锁

对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。

这个线程访问过后,缓存中的数据将被重建,这时候其他线程就可以直接从缓存中取值。

缓存雪崩

场景:大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

大量数据同时失效,Redis 缓存实例发生故障宕机都会导致缓存雪崩。

解决方案:

1.避免缓冲中大量数据同时过期

设置过期时间时,附加一个随机数,避免大量的key同时过期。

2.构建高可用的redis缓存(集群)

部署多个redis实例,个别节点宕机,依然可以保持服务的整体可用

3.构建多级缓存

增加本地缓存,在存储层前面多加一级屏障,降低请求直达存储层的几率。

4.启用限流和降级措施

对存储层增加限流措施,当请求超出限制时,访问非核心数据时,对其提供降级服务(通常是给页面返回一个空值或错误信息)。当业务应用访问的是核心数据时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。

如何保证缓存和数据库数据的一致性?

如果为了短时间的不⼀致性问题,选择让系统设计变得更加复杂的话,完全没必要。

如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。这两个操作如果无法保证原子性,也就是说,要不都完成,要不都没完成,此时,就会出现数据不一致问题。

对应有两种方案:1.先删除缓存,后更新数据库;2.先更新数据库,后删除缓存 1.不考虑并发,正常情况下,无论谁先谁后都可以保持一致。但要考虑第一步成功,第二步失败的情况。都会对业务造成影响,读到旧值。

重试机制

可以把重试或第二步操作放到消息队列中,或「订阅变更日志」: 消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心) 消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)

2.还要考虑并发引发的一致性问题,即使两步都成功,存在并发也会影响

这两种方案的弊端是当存在并发请求时,很容易出现以下问题:

第一种方案:当请求1执行清除缓存后,还未进行update操作,此时请求2进行查询到了旧数据并写入了redis。

第二种方案:当请求1执行update操作后,还未来得及进行缓存清除,此时请求2查询到并使用了redis中的旧数据。

延迟双删(针对方案一)

延迟双删策略是分布式系统中数据库存储和缓存数据保持一致性的常用策略,但它不是强一致。其实不管哪种方案,都避免不了Redis存在脏数据的问题,只能减轻这个问题,要想彻底解决,得要用到同步锁和对应的业务逻辑层面解决。

先进行缓存清除,再执行update,最后(延迟N秒)再执行缓存清除。

延迟N秒的时间要大于一次写操作的时间,一般为3-5秒。

建议优先使用先更新数据库再删除缓存的方法,原因主要有两个:

  1. 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力;

2.如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

当使用先更新数据库再删除缓存时,也有个地方需要注意,如果业务层要求必须读取一致的数据,那么,我们就需要在更新数据库时,先在 Redis 缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。

如果数据库采用的是主从集群,可以强制让redis从主库中读,避免出现不一致。

Redis如何应对并发访问(原子操作)

Redis 提供了两种方法,分别是加锁和原子操作。

加锁是一种常用的方法,如果加锁操作多,会降低系统的并发访问性能;第二个是,Redis 客户端要加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供加解锁操作。

原子操作

原子操作是另一种提供并发访问控制的方法。原子操作是指执行过程保持原子性的操作, 而且原子操作执行时并不需要再加锁,实现了无锁操作。这样一来,既能保证并发控制, 还能减少对系统并发性能的影响。

并发访问控制对应的操作主要是数据修改操作。当客户端需要修改数据时,基本流程分成 两步:

  1. 客户端先把数据读取到本地,在本地进行修改; 2. 客户端修改完数据后,再写回 Redis。 我们把这个流程叫做“读取 - 修改 - 写回”操作(Read-Modify-Write,简称为 RMW 操 作)。

为了实现并发控制要求的临界区代码互斥执行,Redis 的原子操作采用了两种方法:

  1. 把多个操作在 Redis 中实现成一个操作,也就是单命令操作

  2. 把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本

Redis 是使用单线程来串行处理客户端的请求操作命令的,所以,当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。(比如INCR/DECR 命令可以对数据进行增值 / 减值操作,把这三个操作转变为一个原子操作)

Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。

redis使用lua可以保证指令依次执行而不受其他指令干扰,但是不能保证指令最终必定是原子性的
不能保证原子性的场景:
1.指令语法错误,如上述执行redis.call(‘het’,‘k1’,‘1’),正确的应该是redis.call(‘het’,‘k1’,‘1’,‘2’)
2.语法是正确的,但是类型不对,比如对已经存在的string类型的key,执行hset等
3.服务器挂掉了,比如lua脚本执行了一半,但是服务器挂掉了
会导致Lua脚本中前面的命令zhi'xing正确

单命令原子操作的适用范围较小,并不是所有的 RMW 操作都能转变成单命令的原子操作(例如 INCR/DECR 命令只能在读取数据后做原子增减)。

当我们需要对读取的数据做更多判断,或者是我们对数据的修改不是简单的增减时,单命令操作就不适用了。 而 Redis 的 Lua 脚本可以包含多个操作,这些操作都会以原子性的方式执行,绕开了单命令操作的限制。不过,如果把很多操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的时间增加,同样也会降低 Redis 的并发性能。所以,我给你一个小建议:在编写 Lua 脚本时,你要避免把不需要做并发控制的操作写入脚本中。

分布式锁

场景:修改时,经常需要先将数据读取到内存中,在内存中修改后再存回去。在分布式应用中,可能多个进程同时执行上述操作,而读取和修改不是原子操作,所以会产生冲突。增加分布式锁可以解决此类问题。

基本原理:

同步锁(Java中):在多个线程都能访问到的地方,做一个标记,标识该数据的访问权限。

分布式锁:在多个进程都能访问到的地方(一个共享存储系统),做一个标记,标识该数据的访问权限。

加锁时需要判断锁变量的值,根据锁变量值来判断能否加锁成功;释放锁时需要把锁变量值设置为 0,表明客户端不再持有锁。

实现方式:

1.基于数据库实现分布式锁;分布式环境中的线程连接同一个数据库,利用数据库中的行锁达到互斥访问,性能比较低。

2.基于redis实现分布式锁;

Redis,天生单线程。(为啥天生,人就是这么设计的呗)

即使是多进程,都需要通过Redis,然而Redis天生线程安全。

3.基于zookeeper实现分布式锁;数据在内存中,基于zk的顺序节点,临时节点,Watch机制实现。

Redis实现分布式锁的原则

1.安全属性:独享。在任一时刻,只有一个客户端持有锁。

2.无死锁。即便持有锁的客户端崩溃或者断网,锁依然可以被获取。

3.容错。只要大部分redis节点都活着,客户端就可以获取和释放锁。

单redis实例实现分布式锁:

赋予锁变量一个变量名,把这个变量名作为键值对的键,而锁变量的值,则是键值对的值,这样一来,Redis 就能保存锁变量了,客户端也就可以通过 Redis 的命令操作来实现锁操作。

锁变量要有过期时间且能区分来自不同客户端的操作:通过set命令设置唯一标识和过期时间,使用Lua脚本释放锁(释放锁操作的逻辑包含了读取锁变量、判断值、删除锁变量的多个操作,要保证原子性)

使用 SET 命令带上 NX 选项来实现加锁的原子性(key不存在则创建key),加上 EX/PX 选项,设置其过期时间,再设置唯一标识。

多redis实例实现分布式锁:

考虑到只用一个Redis实例保存变量,如果发生故障,客户端就无法进行锁操作。

分布式锁算法Redlock:让客户端和多个独立的redis实例依次请求加锁,如果半数以上的实例成功完成加锁操作,就认为客户端成功获得分布式锁。

加锁操作:1.客户端获取当前时间;2.客户端按顺序依次向N个redis实例执行加锁操作;3.一旦客户端完成了和所有Redis实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

满足两个条件加锁成功:1.半数以上的实例成功完成加锁操作;2.客户端获取锁的总耗时没有超过锁的有效时间。

释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了

Redis分布式锁有什么缺陷?

Redis 分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。

Redis容易产生的几个问题:

  1. 锁未被释放

  2. B锁被A锁释放了

  3. 数据库事务超时

  4. 锁过期了,业务还没执行完

  5. Redis主从复制的问题

Redis作消息队列

分布式系统组件使用消息队列的三大需求:消息保序、重复消息处理和消息可靠性保证。

redis有两种方式实现消息队列:

Streams是Redis5.0专门针对消息队列场景设计的数据类型,支持消费者组形式的消息读取。

Redis是否适合做消息队列?

很多人认为,要使用消息队列,就应该采用Kafka、RabbitMQ这些专门面向消息队列场景的软件,而Redis更加适合做缓存。

Redis是一个非常轻量级的键值数据库,部署一个Redis实例就是启动一个进程,部署Redis集群就是部署多个Redis实例。而kafka、RabbitMQ部署时,涉及到额外的组件,例如Kafka的运行就需要再部署Zookeer。相比Redis来说,Kafka和RabbitMQ一般被认为是重量级的消息队列。

如果分布式系统中的组件消息通信量不大,redis只需要使用有限的内存空间就能满足消息存储的需求,而且,redis的高性能特性能支持快速的消息读写,也是一个好的解决方案。

Redis的持久化方式?

持久化就是将内存中的数据写入到硬盘里,为了之后重用数据。

1.AOF日志(只追加文件)

AOF持久化的实时性更好,但因为记录的是写操作,宕机后需要一条一条命令执行,比较慢。

数据库的是写前日志,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。AOF是写后日志,先把数据写入内存,再记录日志。

AOF记录的是Redis收到的每一条命令写后的好处:只有命令执行成功,才会记录到日志中,避免记录错误命令;命令先执行不会阻塞当前的写操作。

AOF也有两个潜在的风险:1.刚执行完一个命令没来得及记日至就宕机,数据有丢失的风险,如果redis是作缓存还可以从后端数据库中恢复,如果直接作数据库就无法用日志进行恢复。2.AOF虽然避免了对当前命令的阻塞,但可能给下一个操作带来阻塞风险。

这两个风险和AOF写回磁盘的时机相关,与写回策略相关。

三种写回策略:(在高性能和高可靠性间抉择)

1.同步写回:每次修改都写入,基本不丢数据,但会严重降低redis的速度;

2.每秒写回(折中):每次修改先把日志写到AOF文件的内存缓冲区,每秒钟把缓冲区的内容写入磁盘,几乎不影响redis性能,即使挂掉也只会丢失1s内的数据。

3.操作系统决定何时同步。先把日志写到AOF文件的内存缓冲区,由操作系统决定何时写入。写完缓冲区后就可以继续执行后续命令,性能好;但只要AOF记录没有写回磁盘,一旦宕机对应的数据就丢失。

AOF重写机制

随着接受的写命令越来越多,AOF文件会越来越大,会带来性能问题。一是文件系统本身对文件大小有限制,无法保存过大的文件;二是如果文件太大,再往里面追加命令记录,效率会变低。三是故障恢复时,日志文件过大恢复过程会很慢。

AOF重写机制就是在重写时,Redis根据数据库的现状创建一个新的AOF文件。AOF文件是以追加的方式逐一记录,当一个键值对被多条写命令反复修改时,AOF文件都会记录;但是在重写的时候,只会记录最新的状态,这样一个键值对只会有一条写命令,可以把日志文件变小。

AOF重写不会阻塞主线程,和AOF日志由主线程写回不同,重写过程是由主线程fork创建子进程bgrewriteaof来完成的。(fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程)(为什么使用子进程而不是线程?进程之间是相互隔离的,可以避免使用锁来保证数据的安全性。)

每次重写时,redis会先把主线程的内存拷贝一份给子线程用于重写,里面包含了数据库的最新数据,子线程逐一把拷贝的数据写成操作记入重写日志;然后使用两个日志,一个AOF日志继续把写操作写到缓冲区,保证在重写过程中,新写入的数据不会丢失。一个AOF重写日志,也会把操作写到重写日志的缓冲区,以保证数据库最新状态的记录。

AOF重写为什么不共享使用AOF本身的日志?

如果都用AOF日志,主线程要写,子进程也要写,这两者会竞争文件系统的锁,会对Redis主线程的性能造成影响。

2.快照RDB(默认采⽤)

Redis 默认采⽤的持久化⽅式。和AOF相比RDB记录的是某一时刻的数据而不是操作,宕机后可以快速恢复。

通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本 (Redis 主从结构,主要⽤来提⾼ Redis 性能),还可以将快照留在原地以便重启服务器的时候使⽤。

RDB是全量快照,把内存中的所有数据都记录到磁盘中。redis提供了bgsave命令,创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞。

快照时数据能被修改吗?

可以,redis会借助操作系统提供的写时复制技术,在执行快照的同时,正常处理写操作。

bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入到RDB文件。如果此时主线程对这些数据都是读操作,互不影响;如果主线程要修改一块数据,那么这块数据会被复制一份,生成该数据的副本。bgsave子进程会把这个副本数据写入RDB,同时主线程可以直接修改原来的数据。(fork 会涉及到复制大量链接对象)

既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

多久做一次快照?

如果间隔时间长,某一时刻宕机会丢失很多数据;如果频繁执行全量快照,会带来两方面的开销,一个是频繁将全量数据写入磁盘,会给磁盘带来很大压力;另一方面,bgsave子进程需要通过fork操作从主线程创建出来,fork这个创建过程本身会阻塞主线程。频繁创建会带来很大开销。

考虑增量快照,做了一次全量快照后,后续的快照只对修改的数据进行快照记录。但是需要每次使用额外的元数据信息记录哪些数据被修改了,会带来额外的空间开销。

虽然跟AOF相比,快照的恢复速度快,但快照的频率很难把握。

考虑混合使用AOF日志和RDB快照的方法:在两次快照之间,使用AOF日志记录期间的所有命令操作。既避免了快照频繁执行,AOF日志也只用记录两次快照之间的操作,避免文件过大的重写开销。

redis4.0之后有混合持久化,AOF 重写的时候把内存中的数据以RDB的格式写入到 AOF ⽂件开头。好处是结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF ⾥⾯的 RDB 部分是二进制格式,可读性较差。

混合持久化机制的过程:当进行aof重写时,对于历史数据则以rdb方式重新存储到aof文件中,重写完成后的数据还是以aof方式存储到aof文件中,因此也可以对aof文件进行瘦身。

主从库如何实现数据一致?

redis的高可靠性体现在两个方面:一是数据尽量少丢失,二是服务尽量少中断。前者靠AOF和RDB保证,后者就是通过增加副本冗余量,将一份数据同时保存在多个实例上。即使主库故障了,从库也可以对外提供服务。

主从库之间采用了读写分离的方式来保证数据一致性

读操作:主库、从库都可以接收;

写操作:首先到主库执行,然后,主库将写操作同步给从库。

主从库间如何进行第一次同步?(全量复制)

1.从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。(具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数 来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数,offset为-1表示第一次复制。)

2.主库将所有数据同步给从库,从库收到数据后,在本地完成数据加载。(主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到RDB文件后,会先清空当前数据库,然后加载RDB文件。)

在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer复制缓冲区,记录RDB文件生成后收到的所有写操作。

3.主库会把第二阶段执行过程中新收到的写命令,再发送给从库。(当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。)

主从级联模式分担全量复制时的主库压力

一次全量复制对于主库来说有两个耗时操作:生成RDB文件和传输RDB文件。

如果从库数量很多,而且都要和主库进行全量复制的话,会导致主库忙于fork子进程生成RDB文件,并且传输RDB文件也会占用主库的网络带宽,会给主库的资源使用带来压力。

主从级联模式:将主库生成RDB和传输RDB的压力,以级联的方式分散到从库上。

在部署主从集群的时候,手动选择一个从库用于级联其他的从库,再选择一些从库和刚才选的从库,建立起主从关系。这些从库进行同步时,不再和主库进行交互,只和级联的从库进行写操作同步。

主从库间网络断了怎么办?

一旦主从库完成了全量复制,它们之间会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

网络断了之后,主从库会采用增量复制的方式继续同步,只把网络断连期间主库收到的命令同步给从库。

当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个复制积压缓冲区。 repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。主库只用把 两者之间的命令操作同步给从库就行。

注意:因为 repl_backlog_buffer 是一个环形缓冲区,所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。

可以调整 repl_backlog_size 这个参数,默认是1M。这个参数和所需的缓冲空间大小有关。缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。在实际应用中,考虑 到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即 repl_backlog_size = 缓冲空间大小 * 2。

如果并发请求量非常大,连两倍的缓冲空间都存不下新操作请求的话,此时,主从库数据仍然可能不一致。针对这种情况,一方面,你可以根据 Redis 所在服务器的内存资源再适当增加 repl_backlog_size 值,比如说设置成缓冲空间大小的 4 倍,另一方面,你可以考虑使用切片集群来分担单个主库的请求压力。

哨兵机制:主库挂了,如何不间断服务

哨兵机制的基本流程

哨兵就是运行在特殊模式下的Redis进程,随主从库实例运行而运行,主要负责:监控、选择主库和通知。

监控:哨兵进程在运行时,周期性地给所有的主从库发送ping命令,如果没有响应ping命令,会把它标记为下线状态。如果检测的是主库,不能简单地标记为主观下线,可能存在哨兵误判。(一般发生在集群网络压力较大、网络拥塞,或者主库本身压力较大的情况下)

ping命令是个使用频率极高的网络诊断工具,在Windows、Unix和Linux系统下均适用。它是TCP/IP协议的一部分,用于确定本地主机是否能与另一台主机交换数据报。根据返回的信息,我们可以推断TCP/IP参数设置是否正确以及运行是否正常。

通常会采用多实例组成的哨兵集群一起决策,只有当大多数的哨兵实例都判断主库主观下线,主库才被标记为客观下线。

选主:筛选+打分,检查从库的当前在线状态和之前的网络连接状态,筛选过滤掉一部分不符合要求的从库;然后依次按照优先级(高)、复制进度(和旧主库同步程度最接近)、ID号(小)大小对剩余的从库进行打分,只要有得分最高的出现,就选为新主库。

通知:哨兵把新主库的连接信息发送给其他从库,让他们执行replicaof命令,和新主库建立连接,并进行数据复制。同时,把新主库的连接信息发送给客户端,让他们把请求操作发到新主库上。

哨兵集群

即使有哨兵实例出现故障,其他哨兵还能继续协作完成主从库切换的操作。

哨兵集群之间:基于redis的pub/sub机制(发布订阅),哨兵只要和主库建立连接,就可以在主库上发布消息,比如自己的ip和端口号,其他哨兵可以订阅该消息,获得连接信息。

除了哨兵实例,应用程序也可以发布订阅消息,Redis会以频道的形式,对消息进行分类管理。只有同一个频道,才能进行信息交换。

哨兵和从库之间:由哨兵向主库发送INFO命令,主库接收到命令后,会把从库列表返回给哨兵,获得从库的连接信息,就能和从库建立连接。

哨兵和客户端之间:基于哨兵自身的pub/sub功能(因为哨兵就是运行在特定模式下的redis实例,只是并不服务请求操作),客户端可以从哨兵处订阅消息,哨兵也提供了很多消息订阅频道,不同频道包含了主从库切换过程中的不同关键事件。

判断主库客观下线和由哪个哨兵执行主从切换?

和主库”客观下线“判断类似,是一个投票仲裁的过程。

主库客观下线:一个哨兵判断主库主观下线后,给其他实例发送一个命令,其他实例根据自己和主库的连接情况,做出Y或N的响应。当获得票数达到哨兵配置文件中的quorum时,就可以标记主库客观下线。(包含自己的一张票)

哨兵Leader选举:在判断主库客观下线后,哨兵可以继续给其他哨兵发送命令,表明希望由自己来执行主从切换,让其他哨兵投票。需要满足两个条件:拿到半数以上的赞成票;票数大于等于哨兵配置文件quorum的值。(投票规则:没投过票会投Y,投过Y只能投N)

哨兵选举成功很大程度依赖于选举命令的正常网络传播,如果网络压力较大或有短时堵塞,就可能导致没有一个哨兵能选举成功。

主从库同步可能出现的问题和解决方案

脑裂

脑裂是指在主从集群中,同时存在两个主库都能接收写请求。

在 Redis 的主从切换过程中,如果发生了脑裂,客户端数据就会写入到原主库,如果原主库被降为从库,这些新写入的数据就丢失了。

脑裂发生的原因主要是原主库发生了假故障,我们来总结下假故障的两个原因。1.和主库部署在同一台服务器上的其他程序临时占用了大量资源(例如 CPU 资源),导致主库资源使用受限,短时间内无法响应心跳。其它程序不再使用资源时,主库又恢复正常。 2.主库自身遇到了阻塞的情况,例如,处理 bigkey 或是发生内存 swap,短时间内无法响应心跳,等主库阻塞解除后,又恢复正常的请求处理了。

你可以在主从集群部署时,通过合理地配置参数 min-slaves-to-write (主库能进行数据同步的最少从库数量)和 min-slaves-max-lag(从库给主库发送 ACK 消息的最大延迟),来预防脑裂的发生。

主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则, 主库就不会再接收客户端的请求了。

建议:假设从库有 K 个,可以将 min-slaves-to-write 设置为 K/2+1(如果 K 等于 1,就设为 1),将 min-slaves-max-lag 设置为十几秒(例如 10~ 20s),在这个配置下,如果有一半以上的从库和主库进行的 ACK 消息延迟超过十几秒, 我们就禁止主库接收客户端写请求。 这样一来,我们可以避免脑裂带来数据丢失的情况,而且,也不会因为只有少数几个从库 因为网络阻塞连不上主库,就禁止主库接收请求,增加了系统的鲁棒性。

切片集群

切片集群:指启动多个redis实例组成一个集群,按照一定的规则把收到的数据划分成多份,每一份用一个实例来保存。(多个主库)

如果Redis要缓存的总数据量不是很大,比如5GB数据,一般使用 主从模型 + 哨兵集群保证高可用就可以满足。但如果Redis要缓存的总数据量比较大,或者未来可能会增大,比如20GB、50GB数据,那一个主库就无法满足了。

随着集群规模的增加,实例间的通信量也会增加。通信开销就会成为限制Redis Cluster规模的关键因素。Redis 官方给出了 Redis Cluster 的规模上限,就是一个集群运行 1000 个实例。

如何保存大数据?

纵向扩展:升级单个redis实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的CPU。

横向扩展:增加当前Redis实例的个数,形成切片集群。

纵向简单直接,但会受到硬件和成本的限制,并且使用RDB持久化时,主线程fork子进程可能会阻塞,因为数据量很大,需要的内存也会增加。

横向只需增加Redis的实例个数,不用考虑单个的硬件和成本限制。

在面向百万、千万级别的用户规模时,横向扩展的Redis切片集群是一个非常好的选择。

切片集群要考虑两个问题:

1.数据切片和实例的对应分布关系

Redis Cluster方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射。一个切片集群共有16384个哈希槽,每个键值对根据它的key被映射到一个哈希槽中。

Redis会自动把槽平均分布在集群实例上,也可以手动指定每个实例的哈希槽数。

2.客户端如何定位数据

Redis实例会把自己的哈希槽信息发给和它相连接的其他实例,来完成哈希槽分配信息的扩散。客户端可以在访问任何一个实例时,获得所有的哈希槽信息。

客户端把哈希槽信息缓存到本地。当客户端请求键值对时,会先计算对应的哈希槽,然后给相应的实例发送请求。

重定向机制:考虑实例和哈希槽的关系并不是一成不变的,比如实例新增或删除,为了负载均衡需要重新分配哈希槽。客户端无法感知到哈希槽的新变化,就需要重定向机制。

重定向:客户端给一个实例发送读写操作时,这个实例并没有相应的数据,会重定向到新实例的访问地址。

如果哈希槽1的数据完全从实例1迁移到实例2,客户端给实例1发送请求时,如果对应槽1,实例1会返回MOVED命令,包含新实例2的访问地址。客户端会访问实例2,并更新本地缓存,更新槽1与实例2的对应关系,后续所有命令都发往新实例。

如果槽1中数据比较多,只有一部分从实例1迁移到实例2,发请求时,会收到一条ASK报错信息,包含实例2的访问地址。客户端先给实例2发送一个ASKING命令(让这个实例允许客户端发送接下来的命令),再向2发送get命令,读取数据。ASK命令不会更新客户端缓存的哈希槽分配信息,只是让客户端能给新实例发送一次请求。

数据倾斜

数据量倾斜:集群实例上的数据分布不均衡。

原因:1.bigkey导致倾斜(bigkey的value值很大(String类型),或者是bigkey保存了大量集合元素(集合类型),会导致这个实例的数据量增加);2.Slot分配不均衡;3.Hash Tag 导致倾斜。

Hash Tag 是指加在键值对 key 中的一对花括号{}。这对括号会把 key 的一部分括起来, 客户端在计算 key 的 CRC16 值时,只对 Hash Tag 花括号中的 key 内容进行计算。使用 Hash Tag 的好处是,如果不同 key 的 Hash Tag 内容都是一样的,那么,这些 key 对应的数据会被映射到同一个 Slot 中,同时会被分配到同一个实例上。

数据访问倾斜:每个集群实例上的数据量相差不大,但是某个实例上的数据是热点数据,被访问得非常频繁。

原因:实例上存在热点数据,导致大量访问请求集中到了热点数据所在的实例上。

解决:热点数据多副本,只能针对只读的热点数据,因为读写情况要保证多副本间的数据一致性会带来开销。

如果已经发生了数据倾斜,我们可以通过数据迁移来缓解数据倾斜的影响。在构建切片集群时,尽量使用大 小配置相同的实例(例如实例内存配置保持相同),这样可以避免因实例资源不均衡而在 不同实例上分配不同数量的 Slot

渐进式哈希

百度安全验证

切片集群方案:Codis

Redis 官方提供的切片集群方案 Redis Cluster, Redis Cluster 方案正式发布前,业界 已经广泛使用了 Codis。

Codis 集群中包含了 4 类关键组件。

codis server:这是进行了二次开发的 Redis 实例,其中增加了额外的数据结构,支持数据迁移操作,主要负责处理具体的数据读写请求。

codis proxy:接收客户端请求,并把请求转发给 codis server。

Zookeeper 集群:保存集群元数据,例如数据位置信息和 codis proxy 信息。

codis dashboard 和 codis fe:共同组成了集群管理工具。其中,codis dashboard 负责执行集群管理工作,包括增删 codis server、codis proxy 和进行数据迁移。而 codis fe 负责提供 dashboard 的 Web 操作界面,便于我们直接在 Web 界面上进行集群管理。

Pika如何基于SSD实现大容量Redis

应用 Redis 时,随着业务数据的增加,就需要 Redis 能保存更多的数据。

你可能会想到使用 Redis 切片集群,把数据分散保存到多个实例上。但是这样做的话,会有一个问题,如果要保存的数据总量很大,但是每个实例保存的数据量较小的话,就会导致集群的实例规模增加,这会让集群的运维管理变得复杂,增加开销。

你可能又会说,我们可以通过增加 Redis 单实例的内存容量,形成大内存实例,每个实例可以保存更多的数据,这样一来,在保存相同的数据总量时,所需要的大内存实例的个数就会减少,就可以节省开销

大内存 Redis 实例的潜在问题 Redis 使用内存保存数据,内存容量增加后,就会带来两方面的潜在问题,分别是,内存快照 RDB 生成和恢复效率低,以及主从节点全量同步时长增加、缓冲区易溢出。

1实例内存容量大,RDB 文件也会相应增大,那么,RDB 文件生成时的 fork 时长就会增加,
这就会导致 Redis 实例阻塞。而且,RDB 文件增大后,使用 RDB 进行恢复的时长也会增
加,会导致 Redis 较长时间无法对外提供服务。

2.主从节点间的同步的第一步就是要做全量同步。全量同步是主节点生成 RDB 文件,并传给从节点,从节点再进行加载。试想一下,如果 RDB 文件很大,肯定会导致全量同步的时长增加,效率不高,而且还可能会导致复制缓冲区溢出。一旦缓冲区溢出了,主从节点间就会又开始全量同步,影响业务应用的正常使用。如果我们增加复制缓冲区的容量,这又会消耗宝贵的内存资源。此外,如果主库发生了故障,进行主从切换后,其他从库都需要和新主库进行一次全量同步。如果 RDB 文件很大,也会导致主从切换的过程耗时增加,同样会影响业务的可用性。

基于 SSD(固态硬盘) 给 Redis 单实例进行扩容的技术方案 Pika。跟 Redis 相比,Pika 的好处非常明显:既支持 Redis 操作接口,又能支持保存大容量的数据。如果你原来就在应用 Redis,现在想进行扩容,那么,Pika 无疑是一个很好的选择,无论是代码迁移还是运维管理,Pika 基本不需要额外的工作量。

Pika 键值数据库的整体架构中包括了五部分,分别是网络框架、Pika 线程模块、Nemo 存储模块、RocksDB 和 binlog 机制。

网络框架主要负责底层网络请求的接收和发送。Pika 的网络框架是对操作系统底层 的网络函数进行了封装。

Pika 线程模块采用了多线程模型来具体处理客户端请求,包括一个请求分发线程 (DispatchThread)、一组工作线程(WorkerThread)以及一个线程池 (ThreadPool)。

Nemo 模块实现了 Pika 和 Redis 的数据类型兼容,把 Redis 的集合类型转换成单值的键值对。(因为RocksDB 只提供了单值的键值对类型)(Set集合的key和元素member值,都被嵌入到了Pika单值键值对的键当中)

RocksDB(持久化键值数据库) 提供的基于 SSD 保存数据的功能。它使得 Pika 可以不用大容量的内存,就能保存更多数据,还避免了使用内存快照。RocksDB 会先用 Memtable 缓存数据,再将数据快速写入SSD,即使数据量再大,所有数据也都能保存到 SSD 中。使用两小块内存空间(Memtable1 和Memtable2)来交替缓存写入的数据,当有数据要写入 RocksDB 时,RocksDB 会先把数据写入到 Memtable1。等到 Memtable1 写满后,RocksDB 再把数据以文件的形式,快速写入底层的 SSD。同时,RocksDB 会使用 Memtable2 来代替 Memtable1,缓存新写入的数据。

Pika 使用 binlog 机制记录写命令,用于主从节点的命令同步,避免了刚刚所说的大内存实例在主从同步过程 中的潜在问题。binlog 是保存在 SSD上的文件,文件大小不像缓冲区,会受到内存容量的较多限制。

不过,Pika 毕竟是把数据保存到了 SSD 上,数据访问要读写 SSD,所以,读写性能要弱于 Redis。针对这一点,我给你提供两个降低读写 SSD 对 Pika 的性能影响的小建议:

  1. 利用 Pika 的多线程模型,增加线程数量,提升 Pika 的并发请求处理能力; 2. 为 Pika 配置高配的 SSD,提升 SSD 自身的访问性能。

Redis单线程模型

Redis 是单线程,主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程。但Redis的其他功能,比如持久化、异步删除、集群数据同步等,是由额外的线程执行的。

既然是单线程,那怎么监听⼤量的客户端连接呢?

Redis 通过IO多路复⽤程序来监听来⾃客户端的⼤量连接(或者说是监听多个 socket),该机制允许内核中同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,会触发相应的事件,放进事件队列里,交给redis线程处理。

多路复用I/O就是我们说的 select,poll,epoll 等操作,复用的好处就在于 单个进程 就可以同时处理 多个 网络连接的I/O,能实现这种功能的原理就是 select、poll、epoll 等函数会不断的 轮询 它们所负责的所有 socket ,当某个 socket 有数据到达了,就通知用户进程。

这样的好处⾮常明显: I/O 多路复⽤技术的使⽤让 Redis 不需要额外创建多余的线程来监听客户端的⼤量连接,降低了资源的消耗。

单线程Redis为什么这么快?

通常来说,单线程的处理能力要比多线程差很多,但是Redis却能使用单线程模型达到每秒数十万级别的处理能力,这是Redis多方面设计选择的一个综合结果。

一方面,redis的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表。另一方面,redis采用了多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。

Redis6.0之前为什么不使用多线程?

  1. 单线程编程容易并且更容易维护;

  2. Redis 的性能瓶颈不在 CPU ,主要在内存和⽹络;

  3. 避免多线程开发的并发控制问题,多线程会存在死锁、线程上下⽂切换等问题,甚⾄会影响性能。

Redis6.0 引⼊多线程主要是为了提高网络IO读写性能,因为这个算是 Redis 中的⼀个性能瓶颈。

虽然,Redis6.0 引⼊了多线程,但是 Redis 的多线程只是在⽹络数据的读写这类耗时操作上使⽤了, 读写命令仍然是单线程顺序执⾏。

Redis6.0新特性

多线程:单个主线程处理网络请求的速度跟不上底层网络硬件的速度。Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。

多线程默认关闭,可以设置 io-thread-do-reads 配置项为 yes,表示启用多线程。设置的线程个数要小于 Redis 实例所在机器的 CPU 核个数。如果你在实际应用中,发现 Redis 实例的 CPU 开销不大,吞吐量却没有提升,可以考虑使 用 Redis 6.0 的多线程机制,加速网络处理,进而提升实例的吞吐量。

实现服务端协助的客户端缓存

也称为跟踪(Tracking)功能。有了这个功能,业务应用中的 Redis 客户端就可以把读取的数据缓存在业务应用本地了,应用就可以直接在本地快速读取数据了。

如果数据被修改或失效,通过两种模式通知客户端对缓存的数据做失效处理。

更细粒度的权限控制

在 Redis 6.0 版本之前,要想实现实例的安全访问,只能通过设置密码来控制,例如,客 户端连接实例前需要输入密码。

6.0 版本支持创建不同用户来使用 Redis。还支持以用户为粒度设置命令操作的访问权限。

启用 RESP 3 协议

Redis 6.0 实现了 RESP 3 通信协议,而之前都是使用的 RESP 2。在 RESP 2 中,客户端 和服务器端的通信内容都是以字节数组形式进行编码的,客户端需要根据操作的命令或是 数据类型自行对传输的数据进行解码,增加了客户端开发复杂度。

而 RESP 3 直接支持多种数据类型的区分编码,包括空值、浮点数、布尔值、有序的字典 集合、无序的集合等。区分编码,就是指直接通过不同的开头字符,区分不同的数据类型。

事务

Redis事务提供了⼀种将多个命令请求打包的功能。然后, 再按顺序执⾏打包的所有命令,并且不会被中途打断。

事务的执行过程包含三个步骤,Redis 提供了 MULTI、EXEC 两个命令来完成这三个步 骤。

1.客户端使用MULTI命令显式地表示一个事务的开启。
2.客户端把事务中本身要执行的增删改操作发送给服务器端。Redis只是把这些命令暂存到一个命令队列中,不会立即执行。
3.客户端向服务器端发送提交事务的命令EXEC,当服务器端收到 EXEC 命令后,才会实际执行命令队列中的所有命令。

Redis 提供了 DISCARD 命令,但是,这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。

Redis 是不⽀持 roll back 的,因⽽不满⾜原⼦性的,⽽且不满⾜持久性。

redis通过使用 WATCH 机制保证隔离性。

WATCH 机制

WATCH 机制的作用是,在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此 时,如果没有并发修改事务数据的操作了,事务就能正常执行,隔离性也得到了保证。

原子性

命令入队时就报错,会放弃事务执行,保证原子性;

命令入队时没报错,实际执行时报错,不保证原子性

EXEC 命令执行时实例故障,如果开启了 AOF 日志,可以保证原子性。(要使用 redis-check-aof 工具检查 AOF 日志文件,这个工具可以把已完成的事务操作从 AOF 文件中去除。)

持久性:不管 Redis 采用什么持久化模式,事务的持久性属性是得不到保证的

如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也是不能保证持久化的。 如果 Redis 采用了 AOF 模式,因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况,所以,事务的持久性属性也还是得不到保证。

排查和解决Redis变慢的方法

1.查看Redis响应延迟,看的是绝对值,当发现 Redis 命令的执行时间突然就增长到了几秒,可以认为变慢了

2.基于当前环境下的Redis基线性能判断,看相对值,观察到的 Redis 运行时延迟是其基线性能的 2 倍及以上,就可以认定 Redis 变慢了。

3.慢查询命令

慢查询命令,就是指在 Redis 中执行速度慢的命令,这会导致 Redis 延迟增加。通过 Redis 日志,或者是 latency monitor 工具,查询变慢的请求,根据请求对应的具体命令以及官方文档,确认下是否采用了复杂度高的慢查询命令。

用其他高效命令代替。比如说,如果你需要返回一个 SET 中的所有成员时,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量数据,造成 线程阻塞。

当你需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、 SUNION、SINTER 这些命令,以免拖慢 Redis 实例。

因为 KEYS 命令需要遍历存储的键值对,所以操作延时高。所以,KEYS 命令一般不被建议用于生产环境中。

(Redis 提供的 SCAN 命令,以及针对集合类型数据提供的 SSCAN、HSCAN 等,可以根据执行时设定的数量参数,返回指定数量的数据,这就可以避免像 KEYS 命令一样同时返回所有匹配的数据,不会导致 Redis 变慢。)

4.过期key操作

如果频繁使用带有相同时间参数的expireat命令设置过期key,会导致在定期扫描策略中,同一秒内有大量的key同时过期,Redis 就会一直删除以释放内存空间。

如果一批 key 的确是同时过期,你还可以在 EXPIREAT 和 EXPIRE 的过期时间参数上,加上一个一定大小范围内的随机数,这样,既保 证了 key 在一个邻近时间范围内被删除,又避免了同时过期造成的压力。

5.当写回策略配置为 everysec 和 always 时,Redis 需要调用 fsync 把日志写回磁盘。避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。

6.Redis 实例的内存使用是否过大?发生 swap 了吗?如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。同时,要避免出现 Redis 和其他内存需求大的应用共享机器的情况。

7.在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。

影响Redis性能的五大潜在因素

Redis内部的阻塞式操作

Redis实例在运行时交互的对象和交互时发生的操作:

客户端:网络IO(多路复用机制)、第一个阻塞点:集合全量查询和聚合操作,第二个阻塞点:bigkey删除操作(删除的本质是释放键值对占用的内存空间,释放时操作系统会把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配,这个过程回阻塞当前释放内存的应用程序),第三个阻塞点:清空数据库(涉及到删除和释放所有的键值对)

磁盘:生成RDB快照和AOF日志重写设计为采用子进程的方式执行,不会阻塞。第四个阻塞点:AOF日志同步写,redis直接记录AOF日志时,会根据不同的写回策略对数据做落盘保存。一个同步写磁盘的操作耗时大约是1-2ms,如果有大量的写操作需要记录在AOF日志中,并同步写回的话,就会阻塞主线程。

主从节点:主库在复制的过程中,创建和传输RDB文件都是由子进程完成的;清空数据库是第三个阻塞点,第五个阻塞点:加载RDB文件(从库在清空数据库后需要把RDB加载到内存,RDB文件越大,加载过程越慢)

切片集群:当部署Redis切片集群时,每个Redis实例上分配的哈希槽信息需要在不同实例间传递,同时当需要进行负载均衡或者有实例删减时,数据会在不同的实例间进行迁移。

考虑到哈希槽的信息量不大,而数据迁移是渐进式执行的,所以这两类操作对redis主线程的阻塞风险不大。

异步线程机制(4.0以后):后台线程redis会启动一些子线程,把一些任务交给子线程在后台完成,可以避免阻塞主线程。

能被异步执行的操作,一定不能是redis主线程的关键路径上的操作。关键路径上的操作是指客户端把请求发送给redis后,等着redis返回数据结果的操作。

对于Redis来说,读操作是典型的关键路径操作。因为客户端发送读操作后,需要等待读取的数据返回,以便进行后续的数据处理。

bigkey删除清空数据库AOF日志同步写,都不会返回具体的数据结果给实例,可以用异步线程机制。

集合全量查询和聚合操作从库加载RDB文件都属于关键路径上的操作,必须让主线程来执行。

两个小建议:集合全量查询和聚合操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算;

从库加载 RDB 文件:把主库的数据量大小控制在 2~4GB 左右,以保证 RDB 文件能以较快的速度加载。

CPU核和NUMA架构的影响

多核 CPU 和 NUMA 架构已经成为了目前服务器的主流配置。

多核CPU架构

一个CPU处理器一般有多个运行核心,就是物理核,每个物理核有自己的一级、二级缓存L1L2,共享三级缓存L3。L1,L2只有kb,L3几十Mb,能让应用程序缓存更多数据,避免直接访问内存。每个物理核有两个逻辑核,逻辑核共享L1、L2。

对Redis性能的影响

在多核 CPU 架构下,Redis 如果在不同的核上运行,就需要频繁地进行上下文切换。这个过程会增加 Redis 的执行时间。每调度一次,一些请求就会受到运行时信息、指令和数据重新加载过程的影响,这就会导致某些请求的延迟明显高于其他请求。客户端也会观察到较高的尾延迟了。

Redis运行时,把实例和某个核绑定,这样,就能重复利用核上的 L1、L2 缓存,可以降低响应延迟。

NUMA架构(多CPU架构)

多个CPU架构中,不同处理器(CPU Socket)通过总线连接。如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(NUMA 架构)。

对Redis性能的影响

为了提升 Redis 的网络性能,有时会把网络中断处理程序和 CPU 核绑定(可以避免网络中断处理程序在不同核上来回调度执行)。在这种情况下,如果服务器使用的是 NUMA 架构,Redis 实例一旦被调度到和中断处理程序不在同一个 CPU Socket,就要跨CPU Socket访问网络数据,这就会降低 Redis 的性能。(做过测试,和访问 CPU Socket本地内存相比,跨CPU Socket的内存访问延迟增加了18%)

所以,我建议你把 Redis 实例和网络中断处理程序绑在同一个 CPU Socket 下的不同核上, 这样可以提升 Redis 的运行性能。

绑核的风险和解决方案

Redis 除了主线程以外,还有用于 RDB 生成和 AOF 重写的子进程。此外,还有后台线程。 当我们把 Redis 实例绑到一个 CPU 逻辑核上时,就会导致子进程、后台线程和 Redis 主线程竞争 CPU 资源,一旦子进程或后台线程占用 CPU 时,主线程就会被阻塞,导致 Redis 请求延迟增加。

两种解决方案:一个 Redis 实例对应绑一个物理核,让主线程、子进程、后台线程 共享使用 2 个逻辑核,可以在一定程度上缓解 CPU 资源竞争。优化 Redis 源码:修改 Redis 源码,把子进程和后台线程绑到不同的 CPU 核上。

Redis内存碎片

Redis 是内存数据库,内存利用率的高低直接关系到 Redis 运行效率的高低。为了让用户 能监控到实时的内存使用情况,Redis 自身提供了 INFO 命令,可以用来查询内存使用的详细信息。mem_fragmentation_ratio 表示的就是 Redis 当前的内存碎片率,实际分配的/实际使用的。

大于1小于 1.5是合理的,大于 1.5 。这表明内存碎片率已经超过了 50%,需要降低内存碎片率了。

(如果 小于 1,就表明,操作系统分配给 Redis 的内存 空间已经小于 Redis 所申请的空间大小了,此时,运行 Redis 实例的服务器上的内存已经 不够用了,可能已经发生 swap 了。这样一来,Redis 的读写性能也会受到影响,因为 Redis 实例需要在磁盘上的 swap 分区中读写数据,速度较慢。)

内存碎片自动清理,可以避免因为碎片导致 Redis 的内存实际利用率降低,提升成本收益率。自动内存碎片清理机制在控制碎片清理启停的时机上,既考虑了碎片的空间占比、对 Redis 内存使用效率的影响,还考虑了清理机制本身的 CPU 时间占比、对 Redis 性能的影响。而且,清理机制还提供了 4 个参数,让我们可以根据实际应用中的数据量需求和性能要求灵活使用。

内存碎片自动清理涉及内存拷贝,这对 Redis 而言,是 个潜在的风险。如果你在实践过程中遇到 Redis 性能变慢,记得通过日志看下是否正在进 行碎片清理。

Redis缓冲区

缓冲区就是用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。

如果往里面写入数据的速度持续地大于从里面读取数据的速度,可能会导致缓冲区溢出。

缓冲区溢出,本质上无非就是三个原因:命令数据发送过快过大;命令数据处理较慢;缓冲区空间过小。

缓冲区溢出导致网络连接关闭:普通客户端、订阅客户端,以及从节点客户端,它们使用的缓冲区,本质上都是 Redis 客户端和服务器端之间,或是主从节点之间为了传输命 令数据而维护的。这些缓冲区一旦发生溢出,处理机制都是直接把客户端和服务器端的连接,或是主从节点间的连接关闭。网络连接关闭造成的直接影响,就是业务程序无法 读写 Redis,或者是主从节点全量同步失败,需要重新执行。

缓冲区溢出导致命令数据丢失:主节点上的复制积压缓冲区属于环形缓冲区,一旦发生 溢出,新写入的命令数据就会覆盖旧的命令数据,导致旧命令数据的丢失,进而导致主 从节点重新进行全量复制。

客户端输入和输出缓冲区

为了避免客户端和服务器端的请求发送和处理速度不匹配,服务器端给每个连接的客户端设置了一个输入和输出缓冲区。输入缓冲区会把客户端发送的请求暂存起来,Redis主线程再从输入缓冲区中读取命令,进行处理。当Redis主线程处理完数据后,会把结果写入到输出缓冲区,再通过输出缓冲区返回给客户端。

输入缓冲区:

使用 CLIENT LIST 命令,要查看和服务器端相连的每个客户端对输入缓冲区的使用情况。

Redis 并没有提供参数让我们调节客户端输入缓冲区的大小。

避免客户端写入 bigkey,以及避免 Redis 主线程阻塞。

输出缓冲区:

MONITOR 命令主要用在调试环境中,用来监测 Redis 执行,输出结果会持续占用输出缓冲区,不要在线上生产环境中持续使用 MONITOR。

避免 bigkey 操作返回大量数据结果; 避免在线上环境中持续使用 MONITOR 命令。 使用 client-output-buffer-limit 设置合理的缓冲区大小上限,或是缓冲区连续写入时间和写入量上限。

主从集群中的缓冲区

主从集群间的数据复制包括全量复制和增量复制两种。全量复制是同步所有数据,而增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。无论在哪种形式的复制 中,为了保证主从节点的数据一致,都会用到缓冲区。

复制缓冲区:

在全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写 命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从 节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。

可以控制主节点保存的数据量大小;并设置合理的复制缓冲区大小;控制从节点的数量,来避免主节点中复制缓冲区占用过多内存的问题。

复制积压缓冲区:

主节点在把接收到的写命令同步给从节点时,同时会把这些写命令写入复制积压缓冲区。 一旦从节点发生网络闪断,再次和主节点恢复连接后,从节点就会从复制积压缓冲区中, 读取断连期间主节点接收到的写命令,进而进行增量同步。

调整复制积压缓冲区的大小,也就是设置 repl_backlog_size 这个参数的值。

秒杀场景

秒杀场景有 2 个负载特征,分别是 瞬时高并发请求读多写少。Redis 良好的高并发处理能力,以及高效的键值对读写特性,正好可以满足秒杀场景的需求。

秒杀中Redis参与的两个环节:库存查验,库存扣减。依靠redis的支持高并发保证库存查验和库存扣减原子性执行(采用原子操作Lua脚本或分布式锁)

秒杀前:用户会不断刷新商品详情页,这会导致详情页的瞬时请求量剧增。把商品详情页的页面元素静态化,然后使用 CDN 或是浏览器把这些静态化的元素缓存起来,就减轻了服务器端的压力。

请求拦截和流控。在秒杀系统的接入层,对恶意请求进行拦截,避免对系统的恶意攻 击,例如使用黑名单禁止恶意 IP 进行访问。如果 Redis 实例的访问压力过大,为了避免实例崩溃,我们也需要在接入层进行限流,控制进入秒杀系统的请求数量。

秒杀中:库存查验、库存扣减和订单处理,最大的并发压力都在第一步的库存查验操作上。订单处理会涉及支付、商品出库、物流等多个关联操作,这些操作本身涉及数据库中的多 张数据表,要保证处理的事务性,需要在数据库中完成。而且,订单处理时的请求压力已经不大了,数据库可以支撑这些订单处理请求。

对于库存扣减:一旦请求查到有库存,就意味着发送该请求的用户获得了商品的购买资格,用户就会下单了,要保证库存查验和库存扣减原子性。如果放在数据库执行,需要库存量在缓存中同步更新,和redis同步带来额外的开销;数据库处理速度较慢,可能会更新库存量不及时,造成超售。

秒杀后:可能还会有部分用户刷新商品详情页,尝试等待有其他用户退单。而已经成 功下单的用户会刷新订单详情,跟踪订单的进展。用户量会下降很多,服务器端一般都能支撑。

注意:1.Redis 中保存的库存信息其实是数据库的缓存,为了避免缓存击穿问题,我们不要给库存信息设置过期时间。2.如果数据库没能成功处理订单,可以增加订单重试功能,保证订单最终能被成功处理。

场景题

1.

内存不足的风险:Redis fork 一个 bgsave 子进程进行 RDB 写入,如果主线程再接收到写 操作,就会采用写时复制。写时复制需要给写操作的数据分配新的内存空间。本问题中写 的比例为 80%,那么,在持久化过程中,为了保存 80% 写操作涉及的数据,写时复制机制会在实例内存中,为这些数据再分配新内存空间,分配的内存量相当于整个实例数据量 的 80%,大约是 1.6GB,这样一来,整个系统内存的使用量就接近饱和了。此时,如果实 例还有大量的新 key 写入或 key 修改,云主机内存很快就会被吃光。如果云主机开启了 Swap 机制,就会有一部分数据被换到磁盘上,当访问磁盘上的这部分数据时,性能会急 剧下降。如果云主机没有开启 Swap,会直接触发 OOM,整个 Redis 实例会面临被系统 kill 掉的风险。

主线程和子进程竞争使用 CPU 的风险:生成 RDB 的子进程需要 CPU 核运行,主线程本身也需要 CPU 核运行,而且,如果 Redis 还启用了后台线程,此时,主线程、子进程和后台线程都会竞争 CPU 资源。由于云主机只有 2 核 CPU,这就会影响到主线程处理请求的速度。

Redis的部署方式

redis 有4种部署方式:单机,主从,哨兵,集群

Redis的监控命令

最基本的监控命令:INFO 命令

Redis 本身提供的 INFO 命令会返回丰富的实例运行监控信息,这个命令是 Redis 监控工 具的基础。 INFO 命令在使用时,可以带一个参数 section,这个参数的取值有好几种,相应的, INFO 命令也会返回不同类型的监控信息。我把 INFO 命令的返回信息分成 5 大类,其中,有的类别当中又包含了不同的监控内容,如下表所示:

3 种用来监控 Redis 实时运行状态的 运维工具,分别是 Redis-exporter、redis-stat 和 Redis Live。

Redis的使用规范

Redis的未来

这几年呢,新型非易失存储(Non-Volatile Memory,NVM)器件发展得非常快。NVM 器件具有容量大、性能快、能持久化保存数据的特性,这些刚好就是 Redis 追求的目标。 同时,NVM 器件像 DRAM (电脑内存)一样,可以让软件以字节粒度进行寻址访问,所以,在实际应用中,NVM 可以作为内存来使用,我们称为 NVM 内存。

Redis 发展的下一步,就可以基于 NVM 内存来实现大容量实例,或者是实现快速持久化数据和恢复

Redis 在涉及持久化操作时的问题:
RDB 文件创建时的 fork 操作会阻塞主线程; 
AOF 文件记录日志时,需要在数据可靠性和写性能之间取得平衡; 
使用 RDB 或 AOF 恢复数据时,恢复效率受 RDB 和 AOF 大小的限制

有了持久化内存后,还需要 Redis 主从集群吗?

答案:持久化内存虽然可以快速恢复数据,但是,除了提供主从故障切换以外,主从集群 还可以实现读写分离。所以,我们可以通过增加从实例,让多个从实例共同分担大量的读 请求,这样可以提升 Redis 的读性能。而提升读性能并不是持久化内存能提供的,所以, 如果业务层对读性能有高要求时,我们还是需要主从集群的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值