【Redis】Redis 常见问题
Redis 介绍
说说什么是 Redis?
Redis 是一个开源(BSD 许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,HyperLogLogs 等数据类型。内置复制、Lua 脚本、LRU 收回、事务,以及不同级别磁盘持久化功能,同时通过 Redis Sentinel 提供高可用,通过 Redis Cluster 提供自动分区。根据月度排行网站 DB-Engines 的数据,Redis 是最流行的键值对存储数据库。
Redis 全称为:Remote Dictionary Server(远程数据服务),是一个基于内存且支持持久化的高性能 key-value 数据库。具备以下三个基本特征:
- 多数据类型
- 持久化机制
- 主从同步
Redis 有什么优点和缺点?
Redis 优点
- 读写性能优异, Redis 能读的速度是 110000 次 /s,写的速度是 81000 次 /s。
- 支持数据持久化,支持 AOF 和 RDB 两种持久化方式。
- 支持事务,Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。
- 数据结构丰富,除了支持 string 类型的 value 外还支持 hash、set、zset、list 等数据结构。
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
Redis 缺点
- 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
- Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP 才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了系统的可用性。
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
Redis 和 Memcached 的区别有哪些?
- Redis 和 Memcache 都是将数据存放在内存中,都是内存数据库。不过 Memcache 还可用于缓存其他东西,例如图片、视频等等。
- Memcache 仅支持 key-value 结构的数据类型,Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,hash 等数据结构的存储。
- 虚拟内存– Redis 当物理内存用完时,可以将一些很久没用到的 value 交换到磁盘;分布式–设定 Memcache 集群,利用 magent 做一主多从;Redis 可以做一主多从。都可以一主一从。
- 存储数据安全– Memcache 挂掉后,数据没了;Redis 可以定期保存到磁盘(持久化)
- Memcache 的单个 value 最大 1m , Redis 的单个 value 最大 512m 。
- 灾难恢复– Memcache 挂掉后,数据不可恢复;Redis 数据丢失后可以通过 aof 恢复
- Redis 原生就支持集群模式, Redis3.0 版本中,官方便能支持 Cluster 模式了, Memcached 没有原生的集群模式,需要依赖客户端来实现,然后往集群中分片写入数据。
- Memcached 网络 IO 模型是多线程,非阻塞 IO 复用的网络模型,原型上接近于 nignx 。而 Redis 使用单线程的 IO 复用模型,自己封装了一个简单的 AeEvent 事件处理框架,主要实现类 epoll,kqueue 和 select ,更接近于 Apache 早期的模式。
对比:
- Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
- Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
- Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
- Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.
- Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
- Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
- Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
总结:Redis 性能更好,功能更多,支持的数据格式更多,支持的高可用解决方案更多。
Redis 数据结构
底层数据结构:https://zhuanlan.zhihu.com/p/358366217
Redis 的数据类型有哪些?分别在哪些场景下使用比较合适?
Redis 主要有以下几种数据类型:
- String:这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。,一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
- Hash:这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 hash 里的某个字段。
- List:List 是有序列表,这个可以玩儿出很多花样。比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
- Set:是无序集合,自动去重。直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 Redis 进行全局的 set 去重。可以基于 set 进行交集、并集、差集的操作,比如交集可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁
- Zset:排序的 set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。可以用来做排行榜功能。
一般文章都是以 Redis 只有 5 种数据类型,还有 Bitmaps、HyperLogLogs、Streams 等。
Redis 有哪些使用场景?
缓存数据
Redis 提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在 Redis 用在缓存的场合非常多。
排行榜
很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis 提供的有序集合Zset数据结构能实现各种复杂的排行榜应用。
计数器
如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给 + 1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。
分布式会话
分布式系统中存储 Session,使得多台Web服务器都能共享 Session。
集群模式下,在应用不多的情况下一般使用容器自带的 session 复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以 Redis 等内存数据库为中心的 session 服务,session 不再由容器管理,而是由 session 服务及内存数据库管理。
分布式锁
分布式锁实现方案,常见有三种:数据库,Redis、ZooKeepr。Redis 就是其中之一。
如全局 ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用 Redis 的 setnx 功能来编写分布式的锁,如果设置返回 1 说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
社交网络
点赞、踩、关注 / 被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis 提供的哈希Hash、Set集合等数据结构能很方便地实现这些功能。
最新列表
Redis 列表结构,LPUSH 可以在列表头部插入一个内容 ID 作为关键字,LTRIM 可用来限制列表的数量,这样列表永远为 N 个 ID,无需查询最新的列表,直接根据 ID 去到对应的内容页即可。
消息系统
消息队列主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis 提供了发布 / 订阅及阻塞队列功能,能实现一个简单的消息队列系统 。但 Redis 不是一个专业的消息队列。建议使用其他消息队列:Kafka、RocketMQ、RabbitMQ 等。
Bigkey 问题
操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
一方面Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
什么时候会释放大量内存呢?其实就是在删除大量键值对数据的时候,最典型的就是删除包含了大量元素的集合,也称为 bigkey 删除。
bigkey 删除操作就是 Redis 的第二个阻塞点
- 4.0之后,异步新线程删除
- 之前,SCAN
Redis 线程模型
Redis 是单线程的吗?
这里的单线程指的是 Redis 网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程。
为什么 Redis 单线程模型也能效率这么高?
可以从下面 5 个方面来回答:
- C 语言实现,效率高
- 纯内存操作,不涉及IO操作,十分节省时间
- 基于非阻塞的 IO 复用模型机制
- 单线程的话就能避免多线程的频繁上下文切换问题
- 丰富的数据结构(全称采用 Hash 结构,读取速度非常快,对数据存储进行了一些优化,比如亚索表,跳表等)
Redis 为什么设计成单线程的?
- 绝大部分请求是纯粹的内存操作(非常快速)
- 采用单线程,避免了不必要的上下文切换和竞争条件
- 非阻塞 IO,内部采用 epoll,epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,避免 IO 代价。
请说说 Redis 的线程模型?
Redis 内部使用文件事件处理器 file event handler
,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket ,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
来看客户端与 Redis 的一次通信过程:
- 首先,Redis 服务端进程初始化的时候,会将 server socket 的
AE_READABLE
事件与连接应答处理器关联。 - 客户端 socket01 向 Redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个
AE_READABLE
事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的AE_READABLE
事件与命令请求处理器关联。 - 假设此时客户端发送了一个
set key value
请求,此时 Redis 中的 socket01 会产生AE_READABLE
事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的AE_READABLE
事件,由于前面 socket01 的AE_READABLE
事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的key value
并在自己内存中完成key value
的设置。操作完成后,它会将 socket01 的AE_WRITABLE
事件与命令回复处理器关联。 - 如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个
AE_WRITABLE
事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的AE_WRITABLE
事件与命令回复处理器的关联。 - 这样便完成了一次通信。
简单介绍下select poll epoll的区别,select和poll本质上没啥区别,就是文件描述符数量的限制,select根据不同的系统,文件描述符限制为1024或者2048,poll没有数量限制。他两都是把文件描述符集合保存在用户态,每次把集合传入内核态,内核态返回ready的文件描述符。
epoll是通过epoll_create和epoll_ctl和epoll_await三个系统调用完成的,每当接入一个文件描述符,通过ctl添加到内核维护的红黑树中,通过事件机制,当数据ready后,从红黑树移动到链表,通过await获取链表中准备好数据的fd,程序去处理。
Redis 是单线程的,如何提高多核 CPU 的利用率?
CPU 不太可能是 Redis 的瓶颈,一般内存和网络才有可能是。例如使用 Redis 的管道(pipelining)在 liunx 系统上运行可以达到 500K 的 RPS (requests per second) ,因此,如果您的应用程序主要使用 O (N) 或者 O (log (N)) 的 命令,他们几乎不需要使用什么 CPU。
然而,为了最大限度的使用 CPU,可以在同一个服务器部署多个 Redis 的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的,所以,如果你想使用多个 CPU,你可以考虑一下分片(shard) 。
在 Redis 的客户端类库里面,比如 RB(Ruby 的客户端)和 PRedis(最常用的 PHP 客户端之一),能够使用一致性哈希(consistent hashing)来处理多个 Redis 实例。
Redis 没有使用多线程?为什么不使用多线程?Redis6.0 之后为何引入了多线程?
虽然说 Redis 是单线程模型,但是, 实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主处理之外的其他线程来 “异步处理”。
Redis 6 终于支持多线程了,告别单线程了吗?
Redis 6 新增的IO多线程其实指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程。
大体上来说,Redis 6.0 之前主要还是单线程处理。
Redis 6 加入多线程,但跟 Memcached 这种从 IO处理到数据访问多线程的实现模式有些差异。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。整体的设计大体如下:
Redis6.0 之前为什么不使用多线程?
我觉得主要原因有下面 3 个:
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 Redis 配置文件 redis.conf
:
1 | io-threads-do-reads yes |
开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 Redis 配置文件 redis.conf
:
1 | io-threads 4 # 官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 |
什么是 Redis Pipelining ?
Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是, 相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis 可以做很多内部复杂性很强的事情。
同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
Redis 持久化
Redis 有几种持久化方式?
Redis 提供了两种方式,实现数据的持久化到硬盘。
- 【全量】RDB 持久化,是指在指定的时间间隔内将内存中的数据集快照写入磁盘。实际操作过程是,fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
- 【增量】AOF 持久化,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
说说 RDB 的优缺点
优点
- 灵活设置备份频率和周期。你可能打算每个小时归档一次最近 24 小时的数据,同时还要每天归档一次最近 30 天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
- 非常适合冷备份,对于灾难恢复而言,RDB 是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。推荐,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说 Amazon 的 S3 云服务上去,在国内可以是阿里云的 OSS 分布式存储上。
- 性能最大化。对于 Redis 的服务进程而言,在开始持久化时,它唯一需要做的只是 fork 出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行 IO 操作了。也就是说,RDB 对 Redis 对外提供的读写服务,影响非常小,可以让 Redis 保持高性能。
- 恢复更快。相比于 AOF 机制,RDB 的恢复速度更更快,更适合恢复数据,特别是在数据集非常大的情况。
缺点
- 如果想保证数据的高可用性,即最大限度的避免数据丢失,那么 RDB 将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。所以,RDB 实际场景下,需要和 AOF 一起使用。
- 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是 1 秒钟。所以,RDB 建议在业务低估,例如在半夜执行。
说说 AOF 的优缺点
优点
- 该机制可以带来更高的数据安全性,即数据持久性。Redis 中提供了 3 种同步策略,即每秒同步、每修改 (执行一个命令) 同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。
- 由于该机制对日志文件的写入操作采用的是 append 模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。因为以 append-only 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。另外,如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在 Redis 下一次启动之前,我们可以通过 Redis-check-aof 工具来帮助我们解决数据一致性的问题。
- 如果 AOF 日志过大,Redis 可以自动启用 rewrite 机制。即使出现后台重写操作,也不会影响客户端的读写。因为在 rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。
- AOF 包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
缺点
- 对于相同数量的数据集而言,AOF 文件通常要大于 RDB 文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- 根据同步策略的不同,AOF 在运行效率上往往会慢于 RDB 。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和 RDB 一样高效。
- 以前 AOF 发生过 bug ,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志 /merge/ 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug 。不过 AOF 就是为了避免 rewrite 过程导致的 bug ,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
两种持久化方式该如何选择?
RDB 的 bgsave 做镜像全量持久化,AOF 做增量持久化。因为 bgsave 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要 AOF 来配合使用。在 Redis 实例重启时,会使用 bgsave 持久化文件重新构建内存,再使用 AOF 重放近期的操作指令来实现完整恢复重启之前的状态。
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化,但并不推荐这种方式:因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的问题。
那如果突然机器掉电会怎样?
取决于 AOF 日志 sync 属性的配置,如果不要求性能,在每条写指令时都 sync 一下磁盘,就不会丢失数据。但是在高性能的要求下每次都 sync 是不现实的,一般都使用定时 sync ,比如 1 秒 1 次,这个时候最多就会丢失 1 秒的数据。实际上,极端情况下最多丢失 2 秒的数据。因为 AOF 线程负责每秒执行一次 fsync 操作,操作完成后,记录最后同步时间。主线程负责对比上次同步时间,如果超过 2 秒,阻塞等待成功。
bgsave 的原理是什么?
fork 和 cow 。fork 是指 Redis 通过创建子进程来进行 bgsave
操作。cow 指的是 copy on write
,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。这里 bgsave
操作后,会产生 RDB 快照文件。
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
bgsave
:Redis会在后台(Background)异步进行快照操作, 快照同时还可以响应客户端请求。可以通过 lastsave
命令获取最后一次成功执行快照的时间
Redis 内存
Redis 内存调整与查看方式?
查看 Redis 最大占用内存
配置文件 redis.conf
的 maxmemory
参数,maxmemory
是bytes
字节类型,注意转换。
Redis 默认内存多少可以用?
如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。
一般生产上你如何配置?
一般推荐Redis设置内存为最大物理内存的四分之三。
如何修改 Redis 内存设置
- 修改配置文件
redis.conf
的maxmemory
参数,如:maxmemory 104857600
(代表100MB) - 通过命令修改
config set maxmemory 1024
:设置最大内存config get maxmemory
:查看当前配置中的最大内存
什么命令查看 Redis 内存使用情况?
info memory
Redis 打满内存 OOM
1 | 127.0.0.1:6379> config get maxmemory |
没有加上过期时间就会导致数据写满maxmemory
,报OOM错误。为了避免类似情况,引出后文的内存淘汰策略
Redis 数据过期判断原理?
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key (键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
Redis 有几种数据 “过期删除” 策略?
Redis 过期策略是:定期删除 + 惰性删除。
Redis 的过期删除策略,就是指当 Redis 中缓存的 key 过期了,Redis 如何处理。Redis 提供了 3 种数据过期策略:
- 立即删除:key一旦过期立刻被删除,能节省内存空间。缺点是对CPU不友好,用处理器性能换取存储空间。
- 惰性删除(被动删除):当数据过期时不立即删除,而是等到有客户端请求访问该key时再删除,并通知客户端该key不存在。缺点是对内存不友好,大量不常用的key过期后无法回收内存,会占用大量内存资源。
- 定期删除(主动删除):是上述两种策略的折中。由于惰性删除策略无法保证冷数据被及时删除,所以 Redis 会定期每隔一段时间主动淘汰一批已过期的 key,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。其周期性轮询Redis库中的时效性数据,利用随机抽取的策略,利用过期数据占比的方式控制删除频度
定期删除策略
Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略:
- 从过期字典中随机 20 个 key;
- 删除这 20 个 key 中已经过期的 key;
- 如果过期的 key 比率超过 1/4,那就重复步骤 1;
Redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?假如 Redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。
惰性删除策略
所谓惰性策略就是在客户端访问这个key的时候,Redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。
定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被Redis给删除掉。这就是所谓的惰性删除,即当你主动去查过期的key时,如果发现key过期了就立即进行删除,不返回任何东西。
总结:定期删除是集中处理,惰性删除是零散处理。
Redis 中,同时使用了定期删除和惰性删除策略,定期删除没有随机抽到的过期key将在客户端访问时惰性删除。但该方案仍然会有一些不常被访问的key残留在内存中,解决方案是使用数据淘汰策略。
Redis 有哪几种数据 “淘汰” 策略?
当内存即将达到限制,才会执行数据淘汰策略,算是对上述删除策略的兜底。Redis 一共8种数据淘汰策略(达到某种条件后回收key):
noeviction
:不会驱逐任何key。当内存限制达到时仍想增加数据时返回错误allkeys-lru
:加入键的时候,如果过限,首先通过LRU算法驱逐所有键中最久没有使用的键(LRU)volatile-lru
:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键(LRU)allkeys-lfu
:加入键的时候如果过限,从所有键中驱逐使用频率最少的键(LFU)volatile-lfu
:加入键的时候如果过限,从所有配置了过期时间的键中驱逐使用频率最少的键(LFU)allkeys-random
:加入键的时候如果过限,从所有key随机删除。volatile-random
:加入键的时候如果过限,从过期键的集合中随机删除volatile-ttl
:加入键的时候如果过限,从过期键的集合优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
Redis 默认配置的是noeviction
策略。开发常用的策略是 allkeys-lru
。
上述几种策略可分为以下两个维度:
- 对所有键都驱逐
- 只对设置了过期时间的键驱逐
四个方面:
- 随机删除
- LRU
- LFU
- TTL
如何配置淘汰策略
- 命令
config set maxmemory-policy noeviction
config get maxmemory
- 配置文件 - 配置文件
Redis.conf
的maxmemory-policy
参数
Redis 大 Key 问题
Redis 一个字符串类型的值能存储最大容量是多少?
512M
缓存命中率表示什么?
通常来说,缓存命中率越高,缓存的收益越高,应用的性能也就越好。
- 缓存命中: 可以从缓存中获取到需要的数据
- 缓存不命中:缓存中无法获取所需数据,需要再次查询数据库或者其他数据存储载体。
缓存命中率 = 缓存中获取数据次数 / 获取数据总次数
如何提高 Redis 命中率?
提供缓存命中率,通常有如下方式:
- 缓存预加载
- 增加缓存存储量
- 调整缓存存储数据类型
- 提升缓存更新频次
怎么优化 Redis 的内存占用?
- redisObject 对象
- 缩减键值对象
- 共享对象池
- 字符串优化
- 编码优化
- 控制 key 的数量
对 Redis 进行性能优化,有些什么建议?
- Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件。
- Master 调用
BGREWRITEAOF
重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。 - 尽量避免在压力很大的主库上增加过多的从库。
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3… 。
- Redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用 keys
指令可以扫出指定模式的 key 列表。
追问:如果这个 Redis 正在给线上的业务提供服务,那使用 keys
指令会有什么问题?
回答 Redis 关键的一个特性:Redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan
指令,scan
指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
Redis 事务
什么是 Redis 事务?
可以一次性执行多条命令,本质上是一组命令的集合。一个事务中的所有命令都会序列化,然后按顺序地串行化执行,而不会被插入其他命令 。
Redis 的事务相关命令有:
- DISCARD:取消事务,放弃执行事务块中的所有命令
- EXEC:执行事务块中的命令
- MULTI:标记一个事务的开始
- UNWATCH:取消 WATCH 命令对所有 key 的监视
- WATCH key [key…]:监视一个(或多个)key,如果在事务之前执行这个(或者这些)key 被其他命令所改动,那么事务将会被打断。
Redis 事务的注意点有哪些?
- 不支持回滚,如果事务中有错误的操作,无法回滚到处理前的状态,需要开发者处理。
- 在执行完当前事务内所有指令前,不会同时执行其他客户端的请求。
为什么 Redis 事务不支持回滚?
Redis 事务不支持回滚,如果遇到问题,会继续执行余下的命令。这一点和关系型数据库不太一致。这样处理的原因有:
- 只有语法错误,Redis 才会执行失败,例如错误类型的赋值, 这就是说从程序层面完全可以捕获以及解决这些问题
- 支持回滚需要增加很多工作,不支持的情况下,Redis 可以保持简单、速度快的特性
Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。
Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。
如何使用 Redis 实现分布式锁?
关于Redis分布式锁的详细介绍见【Redis】Redis 分布式锁。
使用 Redis 实现分布式锁的思路:
1、setnx(String key,String value)
- 若返回 1,说明设置成功,获取到锁;
- 若返回 0,说明设置失败,已经有了这个 key,说明其它线程持有锁,重试。
2、expire(String key, int seconds)
获取到锁(返回 1)后,还需要用设置生存期,如果在多少秒内没有完成,比如发生机器故障、网络故障等,键值对过期,释放锁,实现高可用。
3、del(String key)
完成业务后需要释放锁。释放锁有 2 种方式:del 删除 key,或者 expire 将有效期设置为 0(马上过期)。在执行业务过程中,如果发生异常,不能继续往下执行,也应该马上释放锁。
如果项目中 Redis 是多机部署的,那么可以尝试使用 Redisson 实现分布式锁,这是 Redis 官方提供的 Java 组件。
Redis 分布式锁
分布式锁的实现条件?
- 互斥性,和单体应用一样,要保证任意时刻,只能有一个客户端持有锁
- 可靠性,要保证系统的稳定性,不能产生死锁
- 一致性,要保证锁只能由加锁人解锁,不能产生 A 的加锁被 B 用户解锁的情况
Redis 和 ZooKeeper 实现的分布式锁有什么区别?
- 高性能:Redis(AP),但其无法保证主从机器上的缓存数据是一致的,可能主机刚保存了某个锁,还未同步给从机,自己就宕机了。在哨兵机制选举出了另一台主机后,其内并不存在该锁,故此前加的分布式锁失效,但其能保证高性能,而不像ZooKeeper一样主从同步时服务无法访问。
- 可靠性:ZooKeeper(CP),能够保证数据的一致性,主机收到的加锁消息会在同步给所有从机后再一起添加到缓存中,此时即可以保证分布式锁数据高度一致,但是缺点是同步期间服务无法访问,性能降低。
二者区别:
- 实现方式的不同,Redis 实现方式为插入一条占位数据key,而 ZK 为注册一个临时节点。
- 遇到宕机情况时,Redis 需要等到过期时间到了后自动释放锁,而 ZK 因为是临时节点,在宕机时候已经是删除了节点去释放锁。
- Redis 在没抢占到锁的情况下一般会去自旋获取锁(阻塞等待锁释放),比较浪费性能,而 ZK 是通过注册监听器的方式获取锁,性能而言优于 Redis。
- 对于性能要求很高的建议使用 Redis 来实现,否则,建议使用 ZooKeeper 来实现。
如何使用 Redis 实现分布式限流?
限流的目的是通过对并发访问 / 请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。Redis 限流的实现方式有 3 种,分别是:
- 基于 Redis 的
setnx
的操作,给指定的 key 设置过期时间; - 基于 Redis 的数据结构
Zset
,将请求打造成一个Zset
数组; - 基于 Redis 的令牌桶算法,输出速率大于输入速率,就要限流。
- 基于 Redis 的信号量机制,当信号量值为0,则拒绝其他的请求,进行限量。详细介绍见【Redis】Redis 分布式锁。
如何使用 Redis 实现消息队列?
Redis 的 List
数据结构常用来作为异步消息队列使用,使用 rpush/lpush
操作入队列,使用 lpop
和 rpop
来出队列。rpush
和 lpop
结合 或者 lpush
和 rpop
结合。
客户端是通过队列的 pop 操作来获取消息,然后进行处理。处理完了再接着获取消息,再进行处理。如此循环往复,这便是作为队列消费者的客户端的生命周期。
Redis 高可用:主从复制、哨兵机制、集群
Redis 高可用方案有哪些?
主从复制、哨兵机制、集群
Redis 单副本
Redis 单副本,采用单个 Redis 节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。
Redis 多副本(主从)
Redis 多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略。
Redis Sentinel(哨兵)
Redis Sentinel 是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel 集群和 Redis 数据集群。
其中 Redis Sentinel 集群是由若干 Sentinel 节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel 的节点数量要满足 2n+1(n>=1)的奇数个。
Redis Cluster
Redis Cluster 是社区版推出的 Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。
Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。
Redis 自研
Redis 自研的高可用解决方案,主要体现在配置中心、故障探测和 failover 的处理机制上,通常需要根据企业业务的实际线上环境来定制化。
Redis 的同步机制是什么?
Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。
加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
主从复制原理
slave启动成功连接到master后会发送一个sync
命令。master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个RDB数据文件到slave,以完成一次完全同步。
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:master继续将新的所有收集到的修改命令依次传给slave(只传送新增的修改命令),完成同步
在Redis2.8版本后,主从断线后恢复的情况下实现增量复制。
说说 Redis Cluster 哈希槽的概念?
Redis Cluster 没有使用一致性 hash ,而是引入了哈希槽的概念。
Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。因为最大是 16384 个哈希槽,所以考虑 Redis 集群中的每个节点都能分配到一个哈希槽,所以最多支持 16384 个 Redis 节点。
为什么是 16384 呢?主要考虑集群内的网络带宽,而 16384 刚好是 2K 字节大小。
Redis 的哨兵有什么功能?
哨兵是 Redis 集群架构中非常重要的一个组件,主要功能如下:
- 集群监控,负责监控 Redis Master 和 Slave 进程是否正常工作;
- 消息通知,如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员;
- 故障转移,如果 Master node 挂掉了,会自动转移到 Slave node 上;
- 配置中心,如果故障转移发生了,通知 Client 客户端新的 Master 地址。
Redis 哨兵和集群的区别是什么?
Redis 的哨兵作用是管理多个 Redis 服务器,提供了监控、提醒以及自动的故障转移的功能。哨兵可以保证当主服务器挂了后,可以从从服务器选择一台当主服务器,把别的从服务器转移到读新的主机。Redis 哨兵的主要功能有:
Redis 的集群的功能是为了解决单机 Redis 容量有限的问题,将数据按一定的规则分配到多台机器,对内存的每秒访问不受限于单台服务器,可受益于分布式集群高扩展性。
Redis Cluster 的主从复制模型是怎样的?
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有 N-1 个复制节点。所以,Redis Cluster 可以说是 Redis Sentinel 带分片的加强版。也可以说:
- Redis Sentinel 着眼于高可用,在 master 宕机时会自动将 slave 提升为 master ,继续提供服务。
- Redis Cluster 着眼于扩展性,在单个 Redis 内存不足时,使用 Cluster 进行分片存储。
Redis 集群模式的工作原理能说一下么?
- 自动将数据进行分片,每个 master 上放一部分数据
- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的
在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是 加 1w 的端口号,比如 16379。16379 端口号是用来进行节点间通信的,也就是 Cluster Bus 的东西,Cluster Bus 的通信,用来进行故障检测、配置更新、故障转移授权。Cluster Bus 用了另外一种二进制的协议,Gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
节点间的内部通信机制
集群元数据的维护有两种方式:集中式、Gossip 协议。Redis Cluster 节点间采用 Gossip 协议进行通信。
集中式是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 storm。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于ZooKeeper(分布式协调的中间件)对所有元数据进行存储维护。
Redis 维护集群元数据采用另一个方式, Gossip 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
- 集中式的好处在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;不好在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
- Gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
- 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号 + 10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 ping 消息,同时其它几个节点接收到 ping 之后返回 pong。
- 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。
Gossip 协议
Gossip 协议包含多种消息,包含 ping,pong,meet,fail 等等。
-
meet
:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。1
redis-trib.rb add-node
其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。
-
ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。
-
pong:返回 ping 和 meet,包含自己的状态和其它信息,也用于信息广播和更新。
-
fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。
ping 消息深入
ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。
每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 cluster_node_timeout / 2
,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeou
t 可以调节,如果调得比较大,那么会降低 ping 的频率。
每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含 总节点数减 2 个其它节点的信息。
在集群模式下,Redis 的 key 是如何寻址的?
使用 Hash Slot 算法,算出来当前 key 的 Hash Slot 值,找到对应的服务器存储
分布式寻址都有哪些算法?
分布式寻址算法:
- hash 算法(大量缓存重建)
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
- Redis cluster 的 Hash Slot 算法
了解一致性 hash 算法吗?
hash 算法:来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。
一致性 hash 算法:
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针 “行走”,遇到的第一个 master 节点就是 key 所在位置。
在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
然而,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点,这样就实现了数据的均匀分布,负载均衡。
Redis Cluster 的 hash slot 算法,如何动态增加和删除一个节点?
Redis Cluster 有固定的 **16384 **个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。
Redis Cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag 来实现。
任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。
Redis Cluster 的高可用与主备切换原理
Redis Cluster 的高可用的原理,几乎跟哨兵是类似的。
判断节点宕机
如果一个节点认为另外一个节点宕机,那么就是 pfail,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是 fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown。在 cluster-node-timeout
内,某个节点一直没有返回 pong,那么就被认为 pfail。
如果一个节点认为某个节点 pfail 了,那么会在 gossip ping 消息中,ping 给其他节点,如果超过半数的节点都认为 pfail 了,那么就会变成 fail。
从节点过滤
对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。
检查每个 slave node 与 master node 断开连接的时间,如果超过了 cluster-node-timeout * cluster-slave-validity-factor
,那么就没有资格切换成 master。
从节点选举
每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。
从节点执行主备切换,从节点切换为主节点。
与哨兵比较
整个流程跟哨兵相比,非常类似,所以说,Redis Cluster 功能强大,直接集成了 Replication 和 Sentinel 的功能。
集群模式是否默认含有哨兵?以后详细复习
请说说你们生产环境中的 Redis 是怎么部署的?
- Redis Cluster ,10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 qps 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求每秒。
- 机器是什么配置?32G 内存 + 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10G 内存,一般线上生产环境,Redis 的内存尽量不要超过 10G,超过 10G 可能会有问题。那么,5 台机器对外提供读写,一共有 50G 内存。
- 因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
- 你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb 。100 条数据是 1mb ,10 万条数据是 1G 。常驻内存的是 200 万条商品数据,占用内存是 20G ,仅仅不到总内存的 50% 。目前高峰期每秒就是 3500 左右的请求量。
- 其实大型的公司,会有基础架构的 Team 负责缓存集群的运维。
了解什么是 Redis 的雪崩、穿透和击穿?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?
缓存雪崩
视频介绍:https://www.bilibili.com/video/BV1Y7411G7Pf?spm_id_from=333.999.0.0
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了,这就是缓存雪崩(还有key集中过期导致请求全都访问数据库导致雪崩,解决方案是设置过期时间时加上随机值)。
缓存雪崩的事前事中事后的解决方案如下:
- 事前:Redis 高可用,主从 + 哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免 MySQL 被打死。
- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 Redis,如果 ehcache 和 Redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 Redis 中。
限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。
针对 Redis 服务不可用的情况:
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
- 限流,避免同时处理大量的请求。
针对热点缓存失效的情况:
- 设置不同的失效时间比如随机设置缓存的失效时间。
- 设置缓存永不失效。
缓存穿透
对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都 “视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
有哪些解决办法?
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
缓存无效 key
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:SET key value EX 10086
。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
布隆过滤器
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个 “人”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
加入布隆过滤器之后的缓存处理流程图如下:
但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
为什么会出现误判的情况呢?我们还要从布隆过滤器的原理来说:
我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
- 对给定元素再次进行相同的哈希计算;
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方式:
- 可以将热点数据设置为永远不过期;
- 或者基于 Redis or Zookeeper 实现分布式互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
如何保证缓存与数据库的双写一致性?
- 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
- 增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。
Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?
多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。 Redis 自己就有天然解决这个问题的 CAS 类的乐观锁方案(WATCH)。
杂项
熟悉 Redis 的哪些客户端?
Jedis
是我们最熟悉和最常用的客户端。轻量,简洁,便于集成和改造。
Jedis 多个线程使用一个连接的时候线程不安全。可以使用连接池,为每个请求创建不同的连接,基于 Apache common pool 实现。跟数据库一样,可以设置最大连接数等参数。Jedis 中有多种连接池的子类
Jedis 有 4 种工作模式:单节点、分片、哨兵、集群。3 种请求模式:Client、Pipeline、事务。
- Client 模式就是客户端发送一个命令,阻塞等待服务端执行,然后读取返回结果。
- Pipeline 模式是一次性发送多个命令,最后一次取回所有的返回结果,这种模式通过减少网络的往返时间和 io 读写次数,大幅度提高通信性能。
- 事务模式(Transaction 模式),即开启 Redis 的事务管理,事务模式开启后,所有的命令(除了 exec,discard,multi 和 watch)到达服务端以后不会立即执行,会进入一个等待队列。
Lettuce
与 Jedis 相比,Lettuce 则完全克服了其线程不安全的缺点:Lettuce 是一个可伸缩的线程安全的 Redis 客户端,支持同步、异步和响应式模式(Reactive)。多个线程可以共享一个连接实例,而不必担心多线程并发问题。
Lettuce 是 Spring Boot 2.x 默认的客户端,替换了 Jedis。集成之后我们不需要单独使用它,直接调用 Spring 的 RedisTemplate 操作,连接和创建和关闭也不需要我们操心。
异步调用基于 Netty 框架构建,支持 Redis 的高级功能,如 Pipeline、发布订阅,事务、Sentinel,集群,支持连接池。异步的结果使用 RedisFuture 包装,提供了大量回调的方法。
Redisson
是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid),提供了分布式和可扩展的 Java 数据结构。
特点:
- 基于 Netty 实现,采用非阻塞 IO,性能高
- 支持异步请求
- 支持连接池、pipeline、LUA Scripting、Redis Sentinel、Redis Cluster 不支持事务,官方建议以 LUA Scripting 代替事务
- 主从、哨兵、集群都支持。Spring 也可以配置和注入 RedissonClient。
实现分布式锁:在 Redisson 里面提供了更加简单的分布式锁的实现。
你知道有哪些 Redis 分区实现方案?
Redis 分区方案,主要分成两种类型:
- 客户端分区,就是在客户端就已经决定数据会被存储到哪个 Redis 节点或者从哪个 Redis 节点读取。大多数客户端已经实现了客户端分区。案例:Redis Cluster 和客户端分区。
- 代理分区,意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些 Redis 实例,然后根据 Redis 的响应结果返回给客户端。案例:Twemproxy 和 Codis 。
查询路由 (Query routing) 的意思,是客户端随机地请求任意一个 Redis 实例,然后由 Redis 将请求转发给正确的 Redis 节点。Redis Cluster 实现了一种混合形式的查询路由,但并不是直接将请求从一个 Redis 节点转发到另一个 Redis 节点,而是在客户端的帮助下直接 Redirect 到正确的 Redis 节点。
Redis 分布式锁相关问题
- Redis除了拿来做缓存,你还见过基于Redis的什么用法?
答:可以做简易的购物车,交友关系,微信朋友圈点赞,微信公众号订阅。还能做发布订阅,消息中间件,数据库
-
Redis做分布式锁的时候有需要注意的问题?
-
如果是Redis是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
-
集群模式下,比如主从模式,有没有什么问题呢?
-
那你简单的介绍一下Redlock吧?你简历上写Redisson,你谈谈。
-
Redis分布式锁如何续期?看门狗知道吗?