一、redis是什么
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、分布式、可选持久性的键值对存储数据库。
从2015年6月开始,Redis的开发由Redis Labs赞助,而2013年5月至2015年6月期间,其开发由Pivotal赞助。在2013年5月之前,其开发由VMware赞助。根据月度排行网站DB-Engines.com的数据,Redis是最流行的键值对存储数据库。
二、redis可以用来干什么
- 缓存
- 计数器
- 排行榜
- 社交网络
- 消息队列
- 分布式锁
以一个电商项目的用户服务为例:
- Token存储:用户登录成功之后,使用Redis存储Token
- 登录失败次数计数:使用Redis计数,登录失败超过一定次数,锁定账号
- 地址缓存:对省市区数据的缓存
- 分布式锁:分布式环境下登录、注册等操作加分布式锁
三、redis的5种数据类型
1. String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。value其实不仅是String,也可以是数字。string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。string 类型是 Redis 最基本的数据类型
注意事项:: string 类型的值最大能存储 512MB。
常用命令: get、set、incr、decr、mget等。
使用场景: 缓存功能、计数、共享Session、限速
2. Hash(哈希)
Hash 是一个键值(key => value)对集合。Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
注意事项: 每个 hash 可以存储 232 -1 键值对(40多亿)。
常用命令: hget、hset、hgetall 等。
使用场景: 缓存用户信息、缓存对象
3. List(列表)
List 其实就是链表(Redis用双端链表实现 List)。List 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。
注意事项: 列表最多可存储 2^32 - 1 元素 (4294967295, 每个列表可存储40多亿)
常用命令: rpush 、lpush、rpop、 lpop、 lrange、 lindex
使用场景: 消息队列、文章列表
4. Set(集合)
Set 是string类型的无序集合。集合是通过hashtable实现的,概念和数学中的集合基本类似,可以交集,并集,差集等等,Set中的元素是没有顺序的。所以添加,删除,查找的复杂度都是O(1)。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
注意事项: 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)
常用命令: sadd、spop、smembers、sunion 等
使用场景: 标签(tag)、共同关注、点赞、收藏 等
5. zset(sorted set:有序集合)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的, 但分数(score)却可以重复。有序集合是通过两种数据结构实现:
- 压缩列表(ziplist) : ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。它能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关
- 跳跃表(zSkiplist) : 跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这是采用跳跃表的主要原因。跳跃表的复杂度是O(log(n))。
使用场景: 用户点赞统计、用户排序
各数据类型应用场景
| 类型 | 简介 | 特性 | 场景 |
|---|---|---|---|
| String(字符串) | 二进制安全 | 可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M | --- |
| Hash(字典) | 键值对集合,即编程语言中的Map类型 | 适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去) | 存储、读取、修改用户属性 |
| List(列表) | 链表(双向链表) | 增删快,提供了操作某一段元素的API | 1、最新消息排行等功能(比如朋友圈的时间线) 2、消息队列 |
| Set(集合) | 哈希表实现,元素不重复 | 1、添加、删除、查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作 | 1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐 |
| Sorted Set(有序集合) | 将Set中的元素增加一个权重参数score,元素按score有序排列 | 数据插入集合时,已经进行天然排序 | 1、排行榜 2、带权重的消息队列 |
四、redis的4种特殊类型
1. 发布订阅 PUB/SUB
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。但如果是先发布消息,再订阅频道,不会收到订阅之前就发布到该频道的消息。
redis有两种订阅模式
1. 基于频道(Channel)的发布/订阅, 订阅给定的一个或多个频道:
subscribe channel [channel ... ]
实现原理: 底层通过字典实现。pubsub_channels 是一个字典类型,保存订阅频道的信息:字典的key为订阅的频道, 字典的value是一个链表, 链表中保存了所有订阅该频道的客户端
频道订阅: 订阅频道时先检查字段内部是否存在;不存在则为当前频道创建一个字典且创建一个链表存储客户端id;否则直接将客户端id插入到链表中
取消频道订阅: 取消时将客户端id从对应的链表中删除;如果删除之后链表已经是空链表了,则将会把这个频道从字典中删除
发布消息: 首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端
2. 基于模式(pattern)的发布/订阅,订阅一个或多个符合给定模式的频道
psubscribe pattern1 [pattern...]
说明:每个模式以 * 作为匹配符;例如 cn* 匹配所有以cn开头的频道:cn.java、cn.csdn
实现原理: 底层是pubsubPattern节点的链表
模式订阅: 新增一个pubsub_pattern数据结构添加到链表的最后尾部,同时保存客户端ID
取消模式订阅: 从当前的链表pubsub_pattern结构中删除需要取消的pubsubPattern结构
PUB/SUB使用注意
- 客户端需要及时消费和处理消息
客户端订阅了channel之后,如果接收消息不及时,可能导致DCS实例消息堆积,当达到消息堆积阈值(默认值为32MB),或者达到某种程度(默认8MB)一段时间(默认为1分钟)后,服务器端会自动断开该客户端连接,避免导致内部内存耗尽
- 客户端需要支持重连
当连接断开之后,客户端需要使用subscribe或者psubscribe重新进行订阅,否则无法继续接收消息
- 不建议用于消息可靠性要求高的场景中
Redis的pubsub不是一种可靠的消息系统。当出现客户端连接退出,或者极端情况下服务端发生主备切换时,未消费的消息会被丢弃
典型使用场景
2. Redis HyperLogLog
Redis 在 2.8.9 版本添加了 HyperLogLog 结构
Redis HyperLogLog 是用来做基数统计的算法
HyperLogLog 的优点: 占用内存小
在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 264 个不同元素的基数。这和计算基数时元素越多耗费内存就越多的集合形成鲜明对比。
HyperLogLog的缺点: 有一定误差
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。HyperLogLog算法一开始就是为了大数据量的统计而发明的,所以很适合那种数据量很大,然后又没要求不能有一点误差的计算,HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,不过这对于页面用户访问量是没影响的,因为这种统计可能是访问量非常巨大,但是又没必要做到绝对准确,访问量对准确率要求没那么高,但是性能存储方面要求就比较高了,而HyperLogLog正好符合这种要求,不会占用太多存储空间,同时性能不错
3. Redis Geo
Redis GEO 用做存储地理位置信息,并对存储的信息进行操作。通过geo相关的命令,可以很容易在redis中存储和使用经纬度坐标信息。Redis中提供的Geo命令有如下几个:
- geoadd:添加经纬度坐标和对应地理位置名称。
- geopos:获取地理位置的经纬度坐标。
- geodist:计算两个地理位置的距离。
- georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
- georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
- geohash:计算一个或者多个经纬度坐标点的geohash值。
4. Redis Bitmaps (位图)
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。由于bit是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
Bitmap不属于Redis的基本数据类型,而是基于String类型进行的位操作。而Redis中字符串的最大长度是 512M,所以 BitMap 的 offset 值也是有上限的,其最大值是:
8 * 1024 * 1024 * 512 = 2^32
六、Redis事务
Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行
一个事务从开始到执行会经历以下三个阶段: 开始事务--->命令入队--->执行事务
Redis事务的注意点:
- Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
- Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。
Redis 事务为什么不支持回滚:
- 如果执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。
- 这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。
七、深入了解Redis
redis为什么快
Redis的速度⾮常的快,单机的Redis就可以⽀撑每秒十几万的并发, 主要原因如下:
- 完全基于内存操作
- 使⽤单线程,避免了线程切换和竞态产生的消耗
- 基于⾮阻塞的IO多路复⽤机制
- C语⾔实现,优化过的数据结构,基于⼏种基础的数据结构,redis做了⼤量的优化,性能极⾼
Redis的IO多路复用
简单理解就是:一个服务端进程可以同时处理多个套接字描述符
Linux 对文件的操作实际上就是通过文件描述符(fd),通过一个进程监听多个文件描述符,一旦某个文件描述符准备就绪,就会通知对应的程序响应并处理,这种通知的方式优势在于在单个时间内能够处理更多的链接。
在 Redis 单线中,允许在内核中同时存在多个监听文件描述符,内核会去监听在这上面的链接请求,一但有请求就会交给 Redis 线程处理,从而实现一个 Redis 线程可以处理多个 IO 流。Linux 中的 I/O 多路复用机制是指一个线程处理多个 IO 流,也就是我们通常说的 select/epoll。
select
select 是一个函数,它支持最大的连接数是 1024 或者 2048,因为在 select 函数需要传入 fd_set 参数,这个 fd_set 的长度取决于操作系统的位数 1024 或者 2048, 其中 fd_set 是一个 bitmap,当数据没有到缓冲区那么就是 0, 反之到了缓冲区就是 1。select 函数的功能是将 fd_set 遍历,判断标识位是否存在变化,若发生变化就发起中断处理。
select 函数监视的文件描述符分 3 类,分别是 writefds、readfds、和 exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符。
select 具有良好的跨平台支持,其缺点在于:
1.单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024。
2.select并没有通知用户态哪一个socket用户有数据,仍需要O(n)的遍历
poll
poll 改变了文件描述符集合的描述方式,使用了 pollfd 结构而不是 select 的 fd_set 结构,使得 poll 支持的文件描述符集合限制远大于 select 的 1024。
epoll
epoll 的首次提出是在 Linux 2.6 内核中,他是为了解决 select 的缺点。它定义了 epoll_event 结构体来处理,解决 select 存在最大链接数的限制。epoll 不会遍历所有的文件描述符(fd),epoll 会将准备就绪的文件描述符维护在一块指定的空间内,每次从其中取出已经准备就绪的文件描述符进行处理,大大提高了性能。
回调机制
为了当请求到达时会通知 Redis 线程, select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。当 select/epoll 监测到有文件描述符请求到达时,会发出对应的事件处理,这些事件会被放到一个事件处理队列中,然后 Redis 会对事件进行处理。通过对队列进行轮询,可以提高 CPU 利用率。同时,Redis 在处理事件时,会调用其相应的事件处理函数,实现基于事件的回调。最终使得请求能够第一时间及时响应,再一步提升 Redis 的相应性能。
Redis6.0使用多线程是怎么回事
Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。
这样做的⽬的是因为Redis的性能瓶颈在于⽹络IO⽽⾮CPU,使⽤多线程能提升IO读写的效率,从⽽整体提⾼Redis的性能。
Redis SCAN命令
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
SCAN 返回一个包含两个元素的数组, 第一个元素是用于进行下一次迭代的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回 0 表示迭代已结束。
相比于keys命令,scan命令有两个比较明显的优势
- scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。
- scan命令提供了count参数,可以控制每次返回结果的最大条数。
scan命令注意点
- Scan Count 参数限制的是遍历的 bucket 数,而不是限制的返回的元素个数, ****由于不同 bucket 中的元素个数不同,其中满足条件的个数也不同,所以每次 Scan 返回元素也不一定相同
- 在使用scan命令的时候,如果需要迭代的遍历,需要每次调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程。
- Count 越大,Scan 总耗时越短,但是单次耗时越大,即阻塞Redis 时间边长, 推荐 Count 大小为 1W左右, 当 Count = Redis Key 总数时,Scan 和 Keys 效果一致
- Scan 采用逆二进制迭代法来计算游标,主要为了兼容Rehash的情况. 即客户端需要做去重处理。
Redis持久化
Redis持久化⽅案分为RDB和AOF两种。
RDB
RDB持久化方案是按照指定时间间隔对你的数据集生成的时间点快照(point-to-time snapshot)。它以紧缩的二进制文件保存Redis数据库某一时刻所有数据对象的内存快照,可用于Redis的数据备份、转移与恢复。到目前为止,仍是官方的默认支持方案。
- RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
- RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的,所以即使Redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库的状态。
手动触发分别对应save和bgsave命令
- save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用
- bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短
需要注意的是,如果父进程内存占用过大,fork过程会比较耗时,在这个过程中父进程无法对外提供服务;另外,需要综合考虑计算机内存使用量,fork子进程后会占用双倍的内存资源,需要确保内存够用。通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork以操作的耗时。
以下场景会自动触发RDB持久化
- 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
- 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
- 执行debug reload命令重新加载Redis时,也会自动触发save操作
- 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
RDB优点
- 只有一个紧凑的二进制文件 dump.rdb,非常适合备份、全量复制的场景。
- 容灾性好,可以把RDB文件拷贝道远程机器或者文件系统中,用于容灾恢复。
- 恢复速度快,RDB恢复数据的速度远远快于AOF的方式
RDB缺点
- 实时性低,RDB 是间隔一段时间进行持久化,没法做到实时持久化/秒级持久化。如果在这一间隔事件发生故障,数据会丢失。
- 存在兼容问题,Redis演进过程存在多个格式的RDB版本,存在老版本Redis无法兼容新版本RDB的问题
AOF(append only file)持久化
AOF是Append Only File的缩写,它是Redis的完全持久化策略,从1.1版本开始支持;这里的file存储的是引起Redis数据修改的命令集合(比如:set/hset/del等),这些集合按照Redis Server的处理顺序追加到文件中。当重启Redis时,Redis就可以从头读取AOF中的指令并重放,进而恢复关闭前的数据状态。
AOF的工作流程操作:命令追加(append)、文件写入与同步(fsync)、文件重写(rewrite)、重启加载(load)
1)所有的写入命令会追加到aof_buf(缓冲区)中。
2)AOF缓冲区根据对应的策略向硬盘做同步操作。
3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。
AOF优点
- 实时性好,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
- 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
AOF缺点
- AOF 文件比 RDB 文件大,且 恢复速度慢。
- 数据集大的时候,比 RDB 启动效率低。
RDB和AOF如何选择
一般来说, 如果想达到足以媲美数据库的数据安全性,应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
- 如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
- 如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。
Redis数据恢复
当Redis发生了故障,可以从RDB或者AOF中恢复数据。恢复的过程也很简单,把RDB或者AOF文件拷贝到Redis的数据目录下,如果使用AOF恢复,配置文件开启AOF,然后启动redis-server即可。
Redis 启动时加载数据的流程 (4.0版本之前的方式)
- AOF持久化开启且存在AOF文件时,优先加载AOF文件
- AOF关闭或者AOF文件不存在时,加载RDB文件。(AOF和RDB只会加载一个)
- 加载AOF/RDB文件成功后,Redis启动成功。
- AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。
Redis4.0 的混合持久化
- Redis 4.0版本之前,重启 Redis 时,我们很少使用 RDB 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
- Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
- 于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
Redis主从复制
精品文章:
概述: 主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是 单向 的,只能由主节点到从节点。
在2.8版本之前只有全量复制,而2.8版本后有全量和增量复制
Redis 主从复制支持 主从同步 和 从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。
主从复制的作用
- 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 (实际上是一种服务的冗余)。
- 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
- 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。
主从有三种常见的拓扑结构
一主一从
一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。
一主多从
一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。
树状主从
树状主从结构(又称为树状拓扑结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。
主从数据同步的方式
Redis在2.8及以上版本使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制。
全量复制
一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。
全量复制的完整运行流程如下:
第一阶段是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。
具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。
第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。
具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。
第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
部分复制
部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施, 使用psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点 (master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向 主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。
先看两个概念: replication buffer 和 repl_backlog_buffer
repl_backlog_buffer:它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。如果从库断开时间太久,repl_backlog_buffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。而在repl_backlog_buffer中找主从差异的数据后,如何发给从库呢?这就用到了replication buffer。
replication buffer:Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个 内存buffer进行数据交互,客户端是一个client,从库也是一个client,我们每个client连上Redis后,Redis都会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。
如果在网络断开期间,repl_backlog_size环形缓冲区写满之后,从库是会丢失掉那部分被覆盖掉的数据,还是直接进行全量复制呢?
对于这个问题来说,有两个关键点:
- 一个从库如果和主库断连时间过长,造成它在主库repl_backlog_buffer的slave_repl_offset位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。
- 每个从库会记录自己的slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过psync命令把自己记录的slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。
为什么不持久化的主服务器自动重启非常危险呢?
在进行主从复制设置时,强烈建议在主服务器上开启持久化,当不能这么做时,比如考虑到延迟的问题,应该将实例配置为避免自动重启。
为了更好的理解这个问题,看下面这个失败的例子,其中主服务器和从服务器中数据库都被删除了。
- 我们设置节点A为主服务器,关闭持久化,节点B和C从节点A复制数据。
- 这时出现了一个崩溃,但Redis具有自动重启系统,重启了进程,因为关闭了持久化,节点重启后只有一个空的数据集。
- 节点B和C从节点A进行复制,现在节点A是空的,所以节点B和C上的复制数据也会被删除。
- 当在高可用系统中使用Redis Sentinel,关闭了主服务器的持久化,并且允许自动重启,这种情况是很危险的。比如主服务器可能在很短的时间就完成了重启,以至于Sentinel都无法检测到这次失败,那么上面说的这种失败的情况就发生了。
如果数据比较重要,并且在使用主从复制时关闭了主服务器持久化功能的场景中,都应该禁止实例自动重启。
为什么主从全量复制使用RDB而不使用AOF?
1、RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量复制的成本最低。
2、假设要使用AOF做全量复制,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。
为什么还有无磁盘复制模式?
Redis 默认是磁盘复制,但是如果使用比较低速的磁盘,这种操作会给主服务器带来较大的压力。Redis从2.8.18版本开始尝试支持无磁盘的复制。使用这种设置时,子进程直接将RDB通过网络发送给从服务器,不使用磁盘作为中间存储。
无磁盘复制模式:master创建一个新进程直接dump RDB到slave的socket,不经过主进程,不经过硬盘。适用于disk较慢,并且网络较快的时候。
使用repl-diskless-sync配置参数来启动无磁盘复制。
使用repl-diskless-sync-delay 参数来配置传输开始的延迟时间;master等待一个repl-diskless-sync-delay的秒数,如果没slave来的话,就直接传,后来的得排队等了; 否则就可以一起传。
为什么还会有从库的从库的设计?
通过分析主从库间第一次数据同步的过程,你可以看到,一次全量复制中,对于主库来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件。
如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量复制。fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。此外,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。那么,有没有好的解决方法可以分担主库压力呢?
其实是有的,这就是“主 - 从 - 从”模式。该模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
简单来说,我们在部署主从集群的时候,可以手动选择一个从库(比如选择内存资源配置较高的从库),用于级联其他的从库。然后,我们可以再选择一些从库(例如三分之一的从库),在这些从库上执行如下命令,让它们和刚才所选的从库,建立起主从关系
replicaof 所选从库的IP 6379
这样一来,这些从库就会知道,在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力,如下图所示:
读写分离及其中的问题
在主从复制基础上实现的读写分离,可以实现Redis的读负载均衡:由主节点提供写服务,由一个或多个从节点提供读服务(多个从节点既可以提高数据冗余程度,也可以最大化读负载能力);在读负载较大的应用场景下,可以大大提高Redis服务器的并发量。下面介绍在使用Redis读写分离时,需要注意的问题。
- 延迟与不一致问题
前面已经讲到,由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。
在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。
- 数据过期问题
在单机版Redis中,存在两种删除策略:
- 惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
- 定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。
Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。
- 故障切换问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
Redis Sentinel 哨兵模式
Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移。
下图是一个典型的哨兵集群监控的逻辑图:
Redis Sentinel 由两部分组成,哨兵节点和数据节点
- 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据,对数据节点进行监控。
- 数据节点: 主节点和从节点都是数据节点;
在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下面是官方对于哨兵功能的描述:
- 监控(Monitoring) :哨兵会不断地检查主节点和从节点是否运作正常。
- 自动故障转移(Automatic failover) :当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
- 配置提供者(Configuration provider) :客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
- 通知(Notification) :哨兵可以将故障转移的结果发送给客户端。
其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。
哨兵集群的组建
哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。
在主从集群中,主库上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的。在下图中,哨兵 1 把自己的 IP(172.16.19.3)和端口(26579)发布到__sentinel__:hello频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。然后,哨兵 2、3 可以和哨兵 1 建立网络连接。
通过这个方式,哨兵 2 和 3 也可以建立网络连接,这样一来,哨兵集群就形成了。它们相互间可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。
哨兵模式实现原理
哨兵模式是通过哨兵节点完成对数据节点的监控、下线、故障转移。
定时监控
Sentinel通过三个定时监控任务完成对各个节点发现和监控
- 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构
- 每隔2秒,每个Sentinel节点会向Redis数据节点的
__sentinel__:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息 - 每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达
主观下线和客观下线
- 主观下线就是哨兵节点认为某个节点有问题
每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过 down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。
- 客观下线就是超过一定数量的哨兵节点认为主节点有问题
当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is- master-down-by-addr命令向其他Sentinel节点询问对主节点的判断,当超过 个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定
领导者Sentinel节点选举
Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移的工作。Redis使用了Raft算法实现领导者选举。
故障转移
领导者选举出的Sentinel节点负责故障转移
- 在从节点列表中选出一个节点作为新的主节点,这一步是相对复杂一些的一步
- Sentinel领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点
- Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点
- Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点
领导者Sentinel节点选举流程 (Raft算法)
- 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观 下线时候,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令, 要求将自己设置为领导者。
- 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝。
- 如果该Sentinel节点发现自己的票数已经大于等于max(quorum, num(sentinels)/2+1),那么它将成为领导者。
- 如果此过程没有选举出领导者,将进入下一次选举。
新的主节点是怎样被挑选出来的
选出新的主节点,大概分为这么几步:
- 过滤:“不健康”(主观下线、断线)、5秒内没有回复过Sentinel节 点ping响应、与主节点失联超过down-after-milliseconds*10秒。
- 选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
- 选择复制偏移量最大的从节点(复制的最完整),如果存在则返 回,不存在则继续。
- 选择runid最小的从节点。
Redis集群模式
Redis 主从存在高可用和分布式的问题,哨兵解决了高可用的问题,而集群就是终极方案,一举解决高可用和分布式问题。
- 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。
- 高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
集群中数据如何分区
分布式的存储中,要把数据集按照分区规则映射到多个节点,常见的数据分区规则三种:
- 方案一:节点取余分区
节点取余分区,非常好理解,使用特定的数据,比如Redis的键,或者用户ID之类,对响应的hash值取余:hash(key)%N,来确定数据映射到哪一个节点上。
不过该方案最大的问题是,当节点数量变化时,如扩容或收缩节点,数据节点映射关 系需要重新计算,会导致数据的重新迁移。
- 方案二:一致性哈希分区
将整个 Hash 值空间组织成一个虚拟的圆环,然后将缓存节点的 IP 地址或者主机名做 Hash 取值后,放置在这个圆环上。当我们需要确定某一个 Key 需 要存取到哪个节点上的时候,先对这个 Key 做同样的 Hash 取值,确定在环上的位置,然后按照顺时针方向在环上“行走”,遇到的第一个缓存节点就是要访问的节点。这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中 相邻的节点,对其他节点无影响。
但它还是存在问题:
- 缓存节点在圆环上分布不平均,会造成部分缓存节点的压力较大
- 当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成力。
- 方案三:虚拟槽分区 (Redis 集群使用的便是该方案)
这个方案 一致性哈希分区的基础上,引入了 虚拟节点 的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为 槽(slot) 。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。
在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);
- 槽 0-3 位于 node1;4-7 位于 node2;以此类推....
如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4,数据在其他节点的分布仍然较为均衡。
Redis集群的原理
集群创建: 数据分区是在集群创建的时候完成的。
设置节点: Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。每个节点需要开启配置cluster-enabled yes,让Redis运行在集群模式下。
节点握手: 是指一批运行在集群模式下的节点通过Gossip协议彼此通信, 达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命 令:cluster meet{ip}{port}。完成节点握手之后,一个个的Redis节点就组成了一个多节点的集群。
分配槽(slot) : Redis集群把所有的数据映射到16384个槽中。每个节点对应若干个槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过 cluster addslots命令为节点分配槽。
故障转移: Redis集群的故障转移和哨兵的故障转移类似,但是Redis集群中所有的节点都要承担状态维护的任务。
故障发现: Redis集群内节点通过ping/pong消息实现节点通信,集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong 消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。
故障恢复: 故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它 的从节点中选出一个替换它,从而保证集群的高可用。
- 资格检查, 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。
- 准备选举时间, 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该 时间后才能执行后续流程。
- 发起选举, 当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程。
- 选举投票, 持有槽的主节点处理故障选举消息。投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节 点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。
- 替换主节点, 当从节点收集到足够的选票之后,触发替换主节点操作。
Redis集群伸缩
Redis集群提供了灵活的节点扩容和收缩方案,可以在不影响集群对外服务的情况下,为集群添加节点进行扩容也可以下线部分节点进行缩容。
其实,集群扩容和缩容的关键点,就在于槽和节点的对应关系,扩容和缩容就是将一部分槽和数据迁移给新节点。
例如下面一个集群,每个节点对应若干个槽,每个槽对应一定的数据,如果希望加入1个节点希望实现集群扩容时,需要通过相关命令把一部分槽和内容迁移给新节点。
缩容也是类似,先把槽和数据迁移到其它节点,再把对应的节点下线。
Redis异步消息队列
- 使用list作为队列,lpush生产消息,rpop消费消息
这种方式,消费者死循环rpop从队列中消费消息。但是这样,即使队列里没有消息,也会进行rpop,会导致RedisCPU的消耗
可以通过让消费者休眠的方式的方式来处理,但是这样又会又消息的延迟问题。
- 使用list作为队列,lpush生产消息,brpop消费消息
brpop是rpop的阻塞版本,list为空的时候,它会一直阻塞,直到list中有值或者超时。
这种方式只能实现一对一的消息队列。
- 使用Redis的pub/sub来进行消息的发布/订阅
发布/订阅模式可以1:N的消息发布/订阅。发布者将消息发布到指定的频道频道(channel),订阅相应频道的客户端都能收到消息。
但是这种方式不是可靠的,它不保证订阅者一定能收到消息,也不进行消息的存储。
所以,一般的异步队列的实现还是交给专业的消息队列。
Redis延时队列
使用zset,利用score排序实现。
可以使用 zset这个结构,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 ....命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可 。
Redis用于缓存相关问题
缓存击穿
一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。
- 加锁更新,⽐如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写⼊缓存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。
- 将过期时间组合写在value中,通过异步的⽅式不断的刷新过期时间,防⽌此类现象。
缓存穿透
缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样. 缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义
缓存空值/默认值
- 一种方式是在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库
- 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除
- 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。 例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。 这时候可以利用消息队列或者其它异步方式清理缓存中的空对象
布隆过滤器
布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。
布隆过滤器可以用来判断某个元素是否在集合内,具有运行快速,内存占用小的特点,它是一个保存了很长的二级制向量,同时结合 Hash 函数实现的。而高效插入和查询的代价就是,它是一个基于概率的数据结构,只能告诉我们一个元素绝对不在集合内,对于存在集合内有一定的误判率。
如何选择哈希函数个数和布隆过滤器长度
- 如果布隆过滤器的长度太小,所有的 bit 位很快就会被用完,此时任何查询都会返回“可能存在”;
- 如果布隆过滤器的长度太大,那么误判的概率会很小,但是内存空间浪费严重。
- 类似的,哈希函数的个数越多,则布隆过滤器的 bit 位被占用的速度越快;哈希函数的个数越少,则误判的概率又会上升。因此,布隆过滤器的长度和哈希函数的个数需要根据业务场景来权衡。
布隆过滤器基本操作
布隆过滤器有两个基本指令,bf.add 添加元素,bf.exists 查询元素是否存在,
它的用法和 set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一个元素,如果想要一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需要用到 bf.mexists 指令。
布隆过滤器特点
只要返回数据不存在,则肯定不存在
返回数据存在,但只能是大概率存在
布隆过滤器优点
- 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快
布隆过滤器缺点
- 随着数据的增加,误判率会增加;还有无法判断数据一定存在
- 还有一个重要缺点,无法删除数据
缓存雪崩
某⼀时刻发⽣⼤规模的缓存失效的情况,例如缓存服务宕机、大量key在同一时间过期,这样的后果就是⼤量的请求进来直接打到DB上,可能导致整个系统的崩溃,称为雪崩。
如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了
提高缓存可用性
- 集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
- 多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
- 过期时间
- 均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。
- 热点数据永不过期。
熔断降级
- 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
- 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。
如何保证缓存和数据库数据的⼀致性?
根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。
1. 选择合适的缓存策略
- 删除缓存而不是更新缓存
当一个线程对缓存的key进行写操作的时候,如果其它线程进来读数据库的时候,读到的就是脏数据,产生了数据不一致问题。
相比较而言,删除缓存的速度比更新缓存的速度快很多,所用时间相对也少很多,读脏数据的概率也小很多。
- 先更数据,后删缓存
更新数据,耗时可能在删除缓存的百倍以上。在缓存中不存在对应的key,数据库又没有完成更新的时候,如果有线程进来读取数据,并写入到缓存,那么在更新成功之后,这个key就是一个脏数据。
先删缓存,再更数据库,缓存中key不存在的时间的时间更长,有更大的概率会产生脏数据。
2. 缓存不一致处理
- 消息队列保证key被删除
可以引入消息队列,把要删除的key或者删除失败的key丢尽消息队列,利用消息队列的重试机制,重试删除对应的key。缺点是对业务代码有一定的侵入性。
- 数据库订阅+消息队列保证key被删除
可以用一个服务(比如阿里的 canal)去监听数据库的binlog,获取需要操作的数据。
然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除操作。
这种方式降低了对业务的侵入,但其实整个系统的复杂度是提升的,适合基建完善的大厂。
- 延时双删防止脏数据
还有一种情况,是在缓存不存在的时候,写入了脏数据,这种情况在先删缓存,再更数据库的缓存更新策略下发生的比较多,解决方案是延时双删。
简单说,就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。
- 设置缓存过期时间兜底
给缓存设置一个合理的过期时间,即使发生了缓存数据不一致的问题,它也不会永远不一致下去,缓存过期的时候,自然又会恢复一致。
如何保证本地缓存和分布式缓存的一致?
- 采用Redis本身的Pub/Sub机制,分布式集群的所有节点订阅删除本地缓存频道,删除Redis缓存的节点,同事发布删除本地缓存消息,订阅者们订阅到消息后,删除对应的本地key。但是Redis的发布订阅不是可靠的,不能保证一定删除成功。
- 引入专业的消息队列,比如RocketMQ,保证消息的可靠性,但是增加了系统的复杂度。
- 设置适当的过期时间兜底,本地缓存可以设置相对短一些的过期时间。
怎么处理热key?
所谓的热key,就是访问频率比较的key。
假如Redis集群部署,热key可能会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过 Redis本身能够承受的OPS。
怎么处理热 key
最关键的是对热点key的监控,可以从这些端来监控热点key
- 客户端: 客户端其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录。
- 代理端: 像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行收集统计。
- Redis服务端: 使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。
只要监控到了热key,对热key的处理就简单了
- 把热key打散到不同的服务器,降低压⼒
- 加⼊⼆级缓存,提前加载热key数据到内存中,如果redis宕机,⾛内存查询
热点key重建?问题?解决?
开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现,可能就会出现比较大的问题:
- 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
要解决这个问题也不是很复杂,解决问题的要点在于:
- 减少重建缓存的次数。
- 数据尽可能一致。
- 较少的潜在危险。
所以一般采用如下方式:
- 互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
- 永远不过期 “永远不过期”包含两层意思:
- 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
- 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
大key问题
单个简单的key存储的value很大,size超过10KB
hash, set,zset,list 中存储过多的元素(以万为单位)
什么是大key,举例
- 一个STRING类型的Key,它的值为5MB(数据过大)
- 一个LIST类型的Key,它的列表数量为20000个(列表数量过多)
- 一个ZSET类型的Key,它的成员数量为10000个(成员数量过多)
- 一个HASH格式的Key,它的成员数量虽然只有1000个但这些成员的value总大小为100MB(成员体积过大)
大key会造成什么问题呢?
- 客户端耗时增加,甚至超时
- 对大key进行IO操作时,会严重占用带宽和CPU
- 造成Redis集群中数据倾斜
- 主动删除、被动删等,可能会导致阻塞
如何找到大key?
- bigkeys命令:使用bigkeys命令以遍历的方式分析Redis实例中的所有Key,并返回整体统计信息与每个数据类型中Top1的大Key。
bigkeys对问题的排查非常方便,但是在使用它时候也有几点需要注意:
- 建议在从节点执行,因为--bigkeys也是通过scan完成的。
- 建议在节点本机执行,这样可以减少网络开销。
- 如果没有从节点,可以使用
--i参数,例如(--i 0.1 代表100毫秒执行一次)。--bigkeys只能计算每种数据结构的top1
Redis 4.0开始提供memory usage命令可以计算每个键值的字节数(自身、以及相关指针开销)
- redis-rdb-tools:redis-rdb-tools是由Python写的用来分析Redis的rdb快照文件用的工具,它可以把rdb快照文件生成json文件或者生成报表用来分析Redis的使用详情。
如何处理大key?
- 删除大key
当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
- 压缩和拆分key
当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
缓存预热怎么做?
所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:
- 直接写个缓存刷新页面或者接口,上线时手动操作
- 数据量不大,可以在项目启动的时候自动进行加载
- 定时任务刷新缓存.
Redis分布式锁
Redis是分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
- setnx命令
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
- 锁超时释放
所以在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。
- set指令
Redis 2.8版本加入了 set 指令的扩展参数,使得 setnx 和expire 指令可以一起执行。
上面这个指令就是 setnx 和 expire 组合在一起的原子指令,这个就算是比较完善的分布式锁了。
Redis pipeline 管道
Redis 提供三种将客户端多条命令打包发送给服务端执行的方式
Pipelining(管道) Redis 管道是三者之中最简单的,当客户端需要执行多条 redis 命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time) 对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis 服务端。
Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。
Pipelining的优势
在性能方面, Pipelining 有下面两个优势
- 节省了RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数
- 减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。
Redis运维
Redis报内存不足怎么处理?
- 修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存
- 也可以通过命令set maxmemory动态设置内存上限
- 修改内存淘汰策略,及时释放内存空间
- 使用 Redis 集群模式,进行横向扩容。
Redis的过期数据回收策略有哪些?
- 惰性删除
惰性删除指的是当我们查询key的时候才对key进⾏检测,如果已经达到过期时间,则删除。显然,他有⼀个缺点就是如果这些过期的key没有被访问,那么他就⼀直⽆法被删除,⽽且⼀直占⽤内存。
- 定期删除
定期删除指的是Redis每隔⼀段时间对数据库做⼀次检查,删除⾥⾯的过期key。由于不可能对所有key去做轮询来删除,所以Redis会每次随机取⼀些key去做检查和删除。
Redis有哪些内存溢出控制/内存淘汰策略?
Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略,Redis支持六种策略
- noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返 回客户端错误信息,此时Redis只响应读操作。
- volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
- allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。
- allkeys-random:随机删除所有键,直到腾出足够空间为止。
- volatile-random:随机删除过期键,直到腾出足够空间为止。
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
Redis阻塞排查
API或数据结构使用不合理
通常Redis执行命令速度非常快,但是不合理地使用命令,可能会导致执行速度很慢,导致阻塞,对于高并发的场景,应该尽量避免在大对象上执行算法复杂度超过O(n)的命令。
对慢查询的处理分为两步
发现慢查询: slowlog get{n}命令可以获取最近的n条慢查询命令;
发现慢查询后,可以从两个方向去优化慢查询:
1)修改为低算法复杂度的命令,如hgetall改为hmget等,禁用keys、sort等命 令
2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。
CPU饱和的问题
- 单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis单核CPU使用率跑到接近100%
- 针对这种情况,处理步骤一般如下
- 判断当前Redis并发量是否已经达到极限,可以使用统计命令redis-cli-h{ip}-p{port}--stat获取当前 Redis使用情况
- 如果Redis的请求几万+,那么大概就是Redis的OPS已经到了极限,应该做集群化水品扩展来分摊OPS压力
- 如果只有几百几千,那么就得排查命令和内存的使用
持久化相关的阻塞
对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。
- fork阻塞: fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。
- AOF刷盘阻塞: 当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等 待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了 数据安全性它会阻塞直到后台线程执行fsync操作完成。
- HugePage写操作阻塞: 对于开启Transparent HugePages的 操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。
Redis常见性能问题和解决方案
- Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
- 如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
- 为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
- 尽量避免在压力较大的主库上增加从库。
- Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
- 为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。
Redis和Lua脚本的使用了解吗?
Redis的事务功能比较简单,平时的开发中,可以利用Lua脚本来增强Redis的命令。
Lua脚本能给开发人员带来这些好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。