Redis 通讯协议(RESP)

Redis 通讯协议(Redis Serialization Protocol,RESP)是 Redis 服务端与客户端之间进行通信的协议。它是一种二进制安全的文本协议,设计简洁且易于实现。RESP 主要用于支持客户端和服务器之间的请求响应交互。

RESP 的主要特点:

  1. 简单性:协议的设计非常简单,易于理解和实现。
  2. 可读性:虽然是一个二进制协议,但它的许多部分都是可读的文本,这使得调试变得更加容易。
  3. 支持多种数据类型:RESP 支持字符串、整数、数组等多种数据类型。
  4. 高效:RESP 的设计考虑了性能,它使用前缀来区分不同的数据类型,减少了数据传输的开销。

RESP 的数据类型:

  • 简单字符串:以 “+” 开头,例如 +OK\r\n
  • 错误:以 “-” 开头,例如 -Error message\r\n
  • 整数:以 “:” 开头,例如 :1000\r\n
  • 批量字符串:以 “$” 开头,后跟字符串的长度,例如 $5\r\nhello\r\n
  • 数组:以 “*” 开头,后跟数组中元素的数量,例如 *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

Redis 的架构模式

单机版

图片

特点:简单。

问题:内存容量有限、处理能力有限、无法高可用。

主从复制

图片

Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步给从服务器,从而一直保证主从服务器的数据相同。

特点:master/slave 角色、master/slave 数据相同、降低 master 读压力在转交从库。

问题:无法保证高可用、没有解决 master 写的压力。

哨兵

图片

Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。其中三个特性:

监控(Monitoring):Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。

提醒(Notification):当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。

自动故障迁移(Automatic failover):当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。

特点:保证高可用、监控各个节点、自动故障迁移。

缺点:主从模式,切换需要时间丢数据、没有解决 master 写的压力。

集群(proxy 型):

图片

Twemproxy 是Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器;Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。

特点:

  1. 多种 hash 算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins。
  2. 支持失败节点自动删除。
  3. 后端 Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个 Redis 一致。

缺点:增加了新的 proxy,需要维护其高可用。Failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预。

集群(直连型):

图片

从Redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

特点:

  • 无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
  • 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
  • 可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
  • 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本。
  • 实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。

缺点:

  • 资源隔离性较差,容易出现相互影响的情况。
  • 数据通过异步复制,不保证数据的强一致性。

Redis 常用命令

  • 列出所有key,*表示区配所有

    Keys pattern 
  • 设置过期时间(单位秒)

    Expire
  • 查看剩下多少时间,返回负数则key失效,key不存在了

    TTL
  • 对 key 的值做加加操作,并返回新的值。注意 incr 一个不是 int 的 value 会返回错误,incr 一个不存在的 key,则设置 key 为 1

    Incr # 同incrby类似
  • 对 key 的值做的是减减操作,decr 一个不存在 key,则设置 key 为-1

    Decr
  • 重命名

    Rename
  • 返回数据类型

    Type
  • 把age 移动到1库

    move age 1
  • 随机返回一个key

    Randomkey

Redis 做异步队列

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

缺点:在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

能不能生产一次消费多次呢?

使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

Redis 连接

安装服务器后,可以运行 redis 安装时提供的 Redis 客户端,也可以打开命令提示符并使用以下命令:

redis-cli

Redis 的主要特点

  • 读写性能优异, Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s。
  • 支持数据持久化,支持 AOF 和 RDB 两种持久化方式。
  • 支持事务,Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持 string 类型的 value 外还支持 hash、set、zset、list 等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

Redis 的复制功能

Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

Redis 和 RDBMS的区别

  • Redis 是 NoSQL 数据库,而 RDBMS 是 SQL 数据库。
  • Redis 遵循键值结构,而 RDBMS 遵循表结构。
  • Redis 非常快,而 RDBMS 相对较慢。
  • Redis 将所有数据集存储在主存储器中,而 RDBMS 将其数据集存储在辅助存储器中。
  • Redis 通常用于存储小型和常用文件,而 RDBMS 用于存储大文件。
  • Redis 仅为 Linux,BSD,Mac OS X,Solaris 提供官方支持。它目前没有为 Windows 提供官方支持,而 RDBMS 提供对两者的支持。

Redis 不同于其它KV存储DB的原因

  • Redis发展方向不同与其他键值数据库,它能包含很多复杂数据类型,对这些数据类型操作都是原子的。Redis数据类型与基本数据结构强相关,直接暴露给程序员,没有增加抽象层。

  • Redis是一个基于内存的,能够持久化到硬盘的数据库,因此为了实现高速读写,数据集大小不能超过内存。内存数据库另一个优点是,内存数据库相对于硬盘数据库非常容易操作复杂数据结构,因此Redis的可以做很多事情,内部复杂性低。与此同时两款磁盘存储格式(RDB和AOF)不需要支持随机访问,因此他们是紧凑的,而且总是以追加形式生成(甚至AOF日志轮换也是一个追加操作,因为新版本是由内存中的副本生成)。比起基于磁盘的数据存储, Redis 用来处理重要数据时需要确保数据集及时落盘刷新。

Redis 内存使用情况

举几个例子(所有数据基于64位实例)

  • 一个空实例大约占用3M内存
  • 1百万简单字符串键值对大约占用85M内存
  • 1百万哈希表键值对,每个对象有5个属性,大约占用160M内存

为了测试你的用例,使用redis-benchmark工具生成随机数据集,使用INFO memory命令检查使用内存空间。

存储相同的键,64位系统比32位系统使用更多的内存,键值很小情况下更明显。这是因为64位系统指针占用8字节。但是64位系统优点是可以配置更多内存(校对注:32位操作系统支持的内存最多为2的32次方,就是4G),因此为了运行大型Redis服务器,64位系统尤佳。另一种方案是使用分片。

Redis 内存中的数据集

过去为了允许数据集超过RAM大小,Redis开发人员尝试使用虚拟内存和其他系统,数据服务由内存提供,磁盘用于存储数据。现在没有计划为Redis创建磁盘后端,毕竟Redis大部分特性都是基于其当前架构设计的。如果数据集过大,需要划分数据集到多个Redis实例上

同时使用 Redis 和磁盘数据库

同时使用Redis和磁盘数据库是个好想法,一个通用的设计方案是,在非常频繁的写小的数据时采用Redis(并且你需要使用Redis数据结构给你的问题建立高效模型),以及将大数据存储到SQL数据库或者最终一致性磁盘数据库中。

降低 Redis 内存使用率

如果可以的话使用Redis 32位实例。另外,还要善于使用哈希表,列表,有序集合和整数集,因为在特殊情况下Redis使用这些数据类型可以更紧凑存储一些元素。可以在内存优化页面获取更多信息。

Redis 内存不足时的处理

Redis要么被Linux内核OOM杀掉,抛出错误崩溃,要么开始变得卡顿。随着现代操作系统malloc方法通常都不返回NULL,而是服务器开始交换,因此Redis性能降低,因此你可能会观察到一些错误现象。

INFO命令返回Redis使用内存总量,因此你可以编写脚本监控Redis服务器内存临界值。

Redis内置保护措施允许用户在配置文件中使用maxmemory选项,设置Redis最大占用内存。如果达到此限制,Redis将开始返回错误给写命令(但是将继续接受只读命令),或者当最大内存限制达到时也可以配置为键淘汰,在这种情况下Redis作为缓存使用。

Linux 后台保存失败报 fork 错误

精简回答:echo 1 > /proc/sys/vm/overcommit_memory。

详细回答:Redis后台保存模式依赖现代操作系统的写时拷贝技术。Redis fork(创建一个子进程)是父进程精确拷贝。子进程存储数据到磁盘并且最终退出。从理论上讲,子进程应该和父进程使用同样多内存,作为父进程副本,但是得益于多数现代操作系统实现的写时复制技术,父进程和子进程共享内存页。内存页在父进程或子进程改变时将被复制。当子进程保存时,理论上所有页面都可能改变,Linux无法提前告知子进程需要多少内存,因此如果overcommit_memory设置为0,fork将会失败除非有足够的空闲RAM真正复制父进程内存页。结果是,如果你有3G Redis数据集,只有2G可用内存将会失败。

overcommit_memory设置为1,意味着Linux 使用更乐观方式fork,这确实是你所期望的Redis。

“理解虚拟机内存 ”是红帽经典文章,可以了解Linux虚拟内存怎么工作,overcommitmemory和overcommitratio的替代品。这篇文章校正了proc(5)用户手册对overcommit_memory1和2配置正确含义。

Redis 磁盘快照

Redis磁盘快照是原子操作,当服务器不在执行命令时,Redis后台保存进程总是被创建,因此每个命令在RAM中是原子的,并且在磁盘快照过程也是原子的。

Redis 利用多CPU/核

CPU基本不可能成为的Redis的瓶颈,因为通常Redis受限于内存或网络。例如使用Pipelining,Redis运行在普通的Linux系统上,每秒可以处理50万请求,所以如果你的应用程序主要使用O(N) 或者 O(log(N))命令,几乎不会使用太多的CPU。

然而为了最大限度利用CPU,你可以在一台机器上启动多个Redis实例,并把它们设置为不同服务器。某些时候单个机器是不够的,所以如果你想使用多个CPU,你可以提前考虑使用分片。

关于使用多Redis实例,可以在Partitioning page找到更多的信息。

单个 Redis 实例可以存储的键

Redis最大可以处理2^32键,实践测试每个实例最少可以处理2.5亿键。

哈希表、列表、集合和有序集合可以包含的元素

每个哈希表、列表、集合和有序集合可以容纳2^32元素。

换句话说,Redis极限容量就是系统可用内存。

主从实例拥有不同数量键原因

如果使用有生存周期的键,这就是正常现象。这就导致主从实例键的数量不一致原因。

  • 主实例在第一次与从实例同步时生成RDB文件。
  • RDB文件不包含已经过期的键,但是已经过期的键仍然在内存中。
  • 尽管这些键从逻辑上说已经过期失效,但是还在Redis主实例内存中,他们并不被识别为存在的,当增量或访问这些键时这些键会被回收。尽管从逻辑上说这些键不是数据集一部分,但是INFO和DBSIZE命令结果包含这些信息。
  • 当从实例读取主实例生成的RDB文件时,过期键不会被载入。

为很多键设置过期属性,通常为用户提供了在从实例上存储更少键,但是实际上实例内容没有逻辑区别。

Redis缓存和DB数据的一致性

  1. 写操作时:先更新数据库,再清除缓存。
  2. 读操作:读取缓存,存在则直接返回,不存在则读取数据库,之后更新到缓存。

图片

为什么是删除缓存,而不是更新缓存呢?

更新缓存会有并发问题,可能会导致缓存与数据库数据不一致,这对大多数业务场景来说是不能接受的。如线程1和2都是写操作,线程1先完成数据库写操作,然后线程2完成了数据库和缓存的写操作,之后线程1完成缓存写操作,那么此时缓存和数据库的数据就不一致了。

图片

为什么不是先删除缓存,再更新数据库呢?

先删除缓存、再更新数据库容易造成读写请求并发问题,可能造成数据不一致。另外,先删除缓存,由于缓存中数据缺失,加剧数据库的请求压力,可能会增大缓存穿透出现的概率。

数据不一致的场景是:线程1删除缓存后,线程2读取到数据库旧值,之后更新旧值到缓存中,之后线程1更新数据库,造成缓存不一致。如下图所示:

图片

因为写数据库往往慢于读请求,所以此问题出现的概率还是相对较大的。

如果使用此方案,业界也提出了“延迟双删”的方案解决不一致问题,即在更新数据库后,再操作一次删除缓存。为了保证第二次删除缓存的时间点在读请求更新缓存之后,这个延迟时间的经验值通常应稍大于业务中读请求的耗时。延迟的实现可以在代码中 sleep 或采用延迟队列。显而易见的是,无论这个值如何预估,都很难和读请求的完成时间点准确衔接,这也是延迟双删被诟病的主要原因。延迟双删的流程如下图所示:

图片

那当前这个方案在并发读写的时候会有数据不一致的问题吗?

当前方案出现数据不一致问题的概率很小,出现的条件极其严苛。如果需要强一致性,也是需要加分布式锁的,但是这样的话,方案的复杂性就会大大增加了。

类比上述读写并发的场景,线程1读请求,此时缓存刚好失效了,就从数据库中读取了旧值,然后线程2更新数据库并操作清除了缓存,之后线程1更新旧值到缓存中。如下图所示:

图片

由上图可知,出现数据不一致问题的条件包括:

  1. 线程1读数据时,无缓存。
  2. 线程1读请求、线程2写请求并发。
  3. 线程1更新缓存比线程2更新数据库+删除缓存加起来耗时都长。

可见,出现该问题的条件还是比较苛刻的,尤其是第3个条件,一般情况下更新数据库都是比更新缓存要慢的,除非刚好线程1到缓存服务刚好出现网络抖动,才会出现该问题。

还有一点,需要指出:上述谈到的数据不一致都是缓存与数据库中的数据可能由于并发等问题导致的长时间不一致,避免该问题,即达成了缓存与数据库数据的最终一致性。

如果需要缓存与数据库数据的强一致性,必然要把操作数据库和缓存放置到同一事务中,操作资源时也需要加分布式锁避免并发读写造成的不一致,这也会导致方案复杂度的上升和请求性能的下降。

(1)更新数据库成功、删除缓存失败了,怎么处理?

  • 重试机制:在更新数据库成功但删除缓存失败时,可以尝试重新删除缓存。重试可以立即进行,也可以延时进行。
  • 补偿事务:如果重试失败,可以记录错误日志,并启动补偿事务,确保数据最终一致性。
  • 异步删除:可以考虑使用消息队列等异步机制来确保缓存的最终一致性。

(2)先更新数据库再清除缓存的方案,适用于以下哪些场景?

  1. 读多写少:这种场景下,缓存的主要作用是减轻数据库的读压力。由于写操作相对较少,因此即使写操作需要先更新数据库再清除缓存,对性能的影响也相对较小。

  2. 对数据一致性要求较高:这种方案可以确保数据库中的数据是最新的,因为每次写操作都会先更新数据库。这有助于减少缓存和数据库之间的不一致性。

  3. 缓存重建成本较低:如果重建缓存的开销较小,那么即使缓存被频繁清除,也不会对系统性能造成太大影响。

然而,这种方案不适合秒杀场景,原因如下:

  • 高并发写操作:秒杀场景通常涉及大量的并发写操作,如果每个写操作都需要先更新数据库再清除缓存,可能会导致数据库成为瓶颈,影响系统的性能和可用性。
  • 数据一致性和实时性要求:秒杀场景要求高数据一致性和实时性,先更新数据库再清除缓存的方案可能会导致用户看到旧数据,影响用户体验。

(3)缓存应用还有哪些策略?分别适用于什么场景?

  1. 最近最少使用(LRU)策略:这种策略会优先淘汰最久未被使用的缓存项。适用于那些访问模式具有时间局部性的场景,即最近被访问的数据在未来一段时间内很可能再次被访问。
  2. 先进先出(FIFO)策略:按照缓存项进入缓存的顺序来淘汰数据,先进入的先被淘汰。适用于那些对数据的新旧程度有明确要求的场景。
  3. 最不经常使用(LFU)策略:这种策略会淘汰访问频率最低的缓存项。适用于那些访问频率能够准确反映数据重要性的场景。
  4. 随机替换策略:随机选择缓存项进行替换。适用于那些没有明显访问模式或访问模式难以预测的场景。
  5. 写回策略:当数据在缓存中被修改时,不立即写入后端存储,而是等到缓存项被替换时才写入。适用于那些写操作频繁且对数据一致性要求不是特别高的场景。
  6. 写透策略:当数据在缓存中被修改时,立即写入后端存储。适用于对数据一致性要求较高的场景。
  7. 基于容量的缓存策略:根据缓存的最大容量来决定替换策略,例如固定容量或动态容量调整。适用于需要控制缓存占用资源的场景。
  8. 基于时间的缓存策略:根据缓存项的存活时间来决定替换,例如设置过期时间。适用于那些数据时效性较强的场景。
  9. 分片缓存策略:将缓存分为多个部分,每个部分使用不同的替换策略。适用于复杂的多类型数据缓存场景。
  10. 分布式缓存策略:在多个节点上分布缓存,提高缓存可用性和性能。适用于大规模、高并发的应用场景。

(4)生产环境中MySQL等数据库一般都会使用主备模式,应用程序会写主库读从库。如果采用写数据库清缓存+读数据库更新缓存的方案,读请求可能因为从库没同步最新的数据而更新旧数据到缓存中,造成缓存不一致,这种情况如何解决?

  • 延迟双删策略:在更新数据库后,延迟一段时间再删除缓存。这段时间应该大于主从同步的时间。
  • 读取主库:在更新缓存时,直接从主库读取数据,而不是从从库读取。
  • 缓存时间设置:合理设置缓存的过期时间,以减少不一致性的影响。
  • 版本号或时间戳:在数据中添加版本号或时间戳,用于检测数据是否是最新的。

(5)先删除缓存再更新数据库的方案适用于哪些场景?

  1. 数据一致性要求高的场景:这种方案可以减少数据不一致的风险,因为当数据库更新后,即使缓存中不存在对应的数据,也可以保证下次访问时从数据库中获取最新数据。
  2. 写操作频繁的场景:在写操作频繁的情况下,如果采用先更新数据库再删除缓存的方案,可能会因为缓存删除操作延迟导致数据不一致。先删除缓存可以减少这种风险。
  3. 缓存穿透问题较为严重的场景:缓存穿透是指查询一个不存在的数据,由于缓存中没有,所以直接查询数据库,这会导致数据库压力增大。先删除缓存可以减少缓存穿透的情况,因为即使请求了不存在的数据,由于缓存已经被删除,所以会直接返回空或错误信息,而不会继续查询数据库。
  4. 缓存数据不是非常关键的场景:如果缓存的数据允许一定时间的不一致性,或者数据不是非常关键,可以先删除缓存,后续的读取操作会从数据库中获取最新数据。
  5. 读操作远大于写操作的场景:在读取操作远多于写入操作的情况下,先删除缓存可以减少因为写入导致的缓存更新问题,因为大部分操作都是读取,即使缓存中没有数据,也不会对性能造成太大影响。
  6. 分布式系统中缓存同步困难的场景:在分布式系统中,缓存同步可能比较困难,先删除缓存可以简化同步逻辑,因为不需要担心缓存中的数据是否是最新的。

Redis 的数据类型、使用场景、实现方式

Redis 提供了以下五种基本数据类型:

1、String(字符串)

(1)使用场景:

  • 存储单个值,如用户信息、商品详情等简单文本或二进制数据。
  • 作为计数器,实现递增/递减操作,如统计网页访问次数、库存数量等。
  • 作为分布式锁的实现基础,利用其原子性操作保障并发安全。
  • 共享Session信息,多个服务都从同一Redis查询Session信息,判断用户是否登录。

(2)实现方式:

Redis 中的 String 采用简单动态字符串(SDS)实现,SDS 不仅可以保存文本数据,还可以保存二进制数据,拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。根据保存对象长短,编码方式(encoding)不同,如下图所示:

图片

2、Hash(哈希)

(1)使用场景:

  • 存储对象或结构化的数据,如用户资料、商品属性等,通过 field-value 形式组织,便于快速访问和更新单个属性。
  • 用作缓存,将关系型数据库中的一行记录完整地存储为一个哈希。

(2)实现方式:

  • Redis Hash 采用压缩列表(ziplist)或哈希表(hashtable)两种底层数据结构实现。当哈希元素数量较少且元素值较小(默认512个)的情况下,使用压缩列表以节约空间;随着元素数量增长或值大小超过阈值,自动转换为哈希表以保证操作性能。
  • 压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。缺点:插入大元素容易引发后面元素的prevlen字段的连锁更新、占用空间调整等。压缩列表结构如下图所示:

图片

  • 在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。listpack结构如下图:

图片

3、List(列表)

(1)使用场景:

  • 实现队列或栈功能,如任务队列、消息队列等,通过 LPUSH/RPOP 或 RPUSH/LPOP 操作实现先进先出(FIFO)或后进先出(LIFO)。
  • 时间序列数据的存储,如用户浏览历史、日志记录等,利用列表的有序性。

(2)实现方式:

  • Redis List 可以选择使用双端链表(linked list)或压缩列表(ziplist)作为底层实现。对于元素数量较少或元素值较小的列表(默认512个),使用压缩列表以节省内存;当元素数量增长或值大小超过设定阈值时,自动转为双端链表以保证快速地插入和删除操作。
  • 在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。如下图所示:

图片

4、Set(集合)

(1)使用场景:

  • 存储唯一值集合,如标签系统中的用户标签、社交网络中的关注关系等。
  • 实现交集、并集、差集等集合运算,如共同关注、推荐好友等。

(2)实现方式:

  • Redis Set 使用整数集合(int set)或哈希表(hashtable)实现。当集合元素全部为整数且范围合适时,使用整数集合以节省空间;否则使用哈希表来保证唯一性和快速增删查操作。

5、ZSet(有序集合)

(1)使用场景:

  • 排行榜系统,如用户积分排名、热门文章等,根据分数(score)对成员(member)进行排序。
  • 时间窗口内事件计数,如最近活跃用户、按时间排序的日志记录等,score 作为时间戳或其他排序依据。
  • 地理位置服务,利用经纬度作为 score,对地点进行范围查询和排序。

(2)实现方式:

  • Redis ZSet 采用跳跃表(skiplist)和字典(dict)相结合的方式实现。跳跃表提供有序性,通过多层索引结构实现快速的范围查询和排序;字典则用于确保成员的唯一性。每个成员在跳跃表和字典中各有一份拷贝,以兼顾排序和唯一性检查。
  • 跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:

图片

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

实现方式总体如图所示:

图片

Redis中的Zset(Sorted Set)使用跳表(Skip List)而不是平衡树(如AVL树、红黑树)、二叉树或B+树来实现,主要基于以下几个原因:

  1. 简单性:跳表的结构相对简单,实现起来容易,不容易出错。相比之下,平衡树(如AVL树、红黑树)的实现和维护要复杂得多,需要频繁地进行树的旋转等操作来保持平衡。

  2. 范围查询高效:跳表支持高效的范围查询。在跳表中,可以快速地定位到范围的起始节点,然后顺序遍历得到结果。而平衡树的范围查询通常需要中序遍历,效率较低。

  3. 并发友好:跳表在进行插入和删除操作时,不需要复杂的树结构调整,因此在并发环境下,跳表更容易实现无锁或乐观锁,从而提高并发性能。

  4. 内存友好:跳表的空间开销比平衡树小,特别是对于内存数据库如Redis来说,内存的使用效率是非常重要的。

  5. 扩展性强:跳表的层数是动态的,可以根据数据量的大小自动调整,因此在数据量动态变化的情况下,跳表的性能表现更加稳定。

  6. 概率均衡:跳表通过随机化来决定节点是否升级(增加层),这使得跳表在保持高效的同时,也能够在一定程度上避免最坏情况的发生。

  7. 缓存友好:跳表的结构更加适合缓存机制,因为其结构简单,缓存效率更高。

尽管跳表在某些方面(如内存利用率)可能不如B+树等结构,但在Redis这样的内存数据库中,其综合性能和实现复杂度使得跳表成为实现Sorted Set的一个非常合适的选择。

其他数据类型

1、Bitmaps(位图)

(1)使用场景:

  • 适用于需要对大量二进制位进行高效操作的场景,如用户签到、活跃用户统计等。
  • 空间高效的存储布尔状态或标志位,如用户是否已读某条消息、某个商品是否有库存等。

(2)实现方式:

  • Bitmaps 在 Redis 中实际上是 String 类型的一种特殊应用。通常将一个大整数看作一系列连续的位,每个位代表一个独立的状态。Redis 提供了一系列针对位的操作命令,如 SETBIT、GETBIT、BITCOUNT 等,使得用户能够以极低的空间成本(每个位仅占用 1 bit)对大量二进制位进行快速的设置、获取与统计。

2、HyperLogLog(基数估计算法)

(1)使用场景:

  • 用于近似统计大量唯一元素的数量,如网站独立访客数、唯一用户 ID 计数等,无需精确计数但对内存占用敏感的场景。

(2)实现方式:

  • HyperLogLog 是一种概率算法,能在小内存空间内(通常几十到几百字节)提供对大规模数据集基数(不重复元素数量)的估算。Redis 将 HyperLogLog 算法实现为一种数据类型,提供 PFADD、PFCOUNT 等命令,允许用户向 HyperLogLog 结构添加元素,并获取估算的基数,误差率通常在 0.81% 以内,精确计数时不能使用。

3、Geo(地理位置)

(1)使用场景:

  • 适用于需要对带有地理位置信息的数据进行距离查询、范围查询、附近点查找等操作的场景,如地图服务、LBS 应用、社交网络中基于位置的好友推荐等。

(2)实现方式:

  • Redis 的 Geo 数据类型基于 ZSet 实现,通过将经纬度信息转换为分值,结合专用的地理空间操作命令(如 GEOADD、GEODIST、GEORADIUS、GEORADIUSBYMEMBER),实现了对地理位置数据的高效管理和查询。用户可以轻松地添加、删除地理位置信息,以及根据给定坐标或距离范围进行查询。

4、Streams(流)

(1)使用场景:

  • 适用于处理持续追加的事件序列,如消息队列、活动日志、物联网设备数据流等,支持持久化存储、多消费者消费、消息分片等复杂需求。

(2)实现方式:

  • Redis Streams 是一种逻辑上类似于发布-订阅模式,但具有更强持久化、多消费组、消息回溯等特点的数据类型。每个 Stream 由一系列带有唯一 ID 和元数据(时间戳、字段值等)的消息组成。Redis 提供 XADD、XREAD、XGROUP 等命令,支持客户端以高并发、低延迟的方式生成、消费和管理数据流。

5、RedisObject(自定义数据类型)

(1)使用场景:

  • RedisObject 是一种更为抽象和灵活的数据类型概念,它涵盖了 Redis 中所有基本数据类型以及特殊数据类型的实现。在实际应用中,开发者并不直接操作 RedisObject,而是通过使用 Redis 提供的各种数据类型命令与之交互。RedisObject 作为 Redis 内部的核心数据结构,为不同数据类型的特性和操作提供了统一的封装和管理。
  • 在高级开发和定制化需求中,RedisObject 的存在为扩展 Redis 功能、实现特定业务逻辑或集成第三方模块提供了可能。例如,通过编写自定义 Redis 模块,开发者可以创建新的数据类型,结合 Redis 的高性能特性实现特定领域的高效数据存储和处理解决方案。

(2)实现方式:

RedisObject 结构包含了以下几个关键部分:

  1. type:表示该对象的实际类型,如 STRING、LIST、HASH、SET、ZSET、BITMAPS、HYPERLOGLOG、GEO 或自定义类型。
  2. encoding:表示对象内部数据的编码方式,如整数、压缩列表、双端链表、哈希表、跳跃表、整数集合、字典等。不同的数据类型和使用场景下,Redis 会根据数据量、元素分布等因素动态选择最合适的编码,以平衡内存占用和操作性能。
  3. ptr:指向实际数据存储区域的指针。根据 type 和 encoding,该指针可能指向不同类型和编码的具体数据结构。
  4. 其他属性和元数据,如过期时间(TTL)、引用计数(用于复制、AOF/RDB 持久化等场景)等。

RedisObject 的设计使得 Redis 能够以高度统一且灵活的方式处理各种数据类型。当客户端执行相关命令时,Redis 会根据命令参数和目标对象的类型与编码,调用相应的内部函数进行操作。这种设计不仅简化了 Redis 内部的代码组织和维护,也使得 Redis 能够在不牺牲性能的前提下,轻松支持新的数据类型和功能扩展。

ZSet 不使用 二叉树/红黑树/B+树原因

尽管 B+ 树是一种常用于数据库系统中实现有序数据存储的数据结构,具有良好的查询性能和较高的空间利用率,但 Redis 在设计 ZSet 时选择使用跳跃表而非 B+ 树,主要原因可能包括:

  • 范围查询性能:

跳跃表的层次化索引结构使得它能够快速进行范围查询,无需像 B+ 树那样需要从根节点逐层遍历到叶子节点。尤其是在处理大量范围查询的场景(如排行榜分页展示、地理坐标范围检索)时,跳跃表的优势更为明显。

  • 内存使用:

可以有效地控制跳表的索引层级,来控制内存的消耗,Redis是直接操作内存的并不需要磁盘IO,而MySQL需要去读取IO,所以MySQL要使用B+树的方式减少磁盘IO(B+树的原理是 叶子节点存储数据,非叶子节点存储索引,每次读取磁盘页时就会读取一整个节点,每个叶子节点还有指向前后节点的指针,为的是最大限度地降低磁盘的IO)。

二叉树/红黑树层级较高,内存占用大;B+ 树的空间利用率较高,但其节点间复杂的指针结构可能导致内存分配不连续,从而增加内存占用。

  • 简单性与实现成本:

跳跃表是一种相对简单的数据结构,其逻辑清晰、易于实现和维护。相比之下,B+ 树的实现较为复杂,涉及到节点分裂、合并等操作,对于追求简洁高效的 Redis 来说,跳跃表是更轻量级的选择。

  • 高效插入与删除:

跳跃表支持在 O(logN) 时间复杂度内完成插入、删除和查找操作,与 B+ 树相当。但在实际应用中,跳跃表的插入和删除操作通常能更快地完成,因为它不需要像 B+ 树那样频繁调整节点之间的指针关系。

综上所述,Redis 选择跳跃表而非 B+ 树来实现 ZSet,主要是出于良好范围查询性能、内存使用效率、简单性、以及高效操作的考虑。这种设计选择在实践中已被证明能够很好地满足 Redis 应用场景的需求,特别是在高并发、低延迟的环境中表现出色。

使用 Redis 延迟队列来自动取消未支付订单

延迟队列的实现方式多样,比如RocketMQ、RabbitMQ等消息队列本身就支持延迟队列的功能。

Redis实现延迟队列的基本思路:

Redis的Sorted Set数据结构天然适合实现延迟队列。可以将任务ID作为成员(member),任务的执行时间戳作为分数(score)。这样,通过ZADD命令可以轻松地按照执行时间将任务插入到集合中。而ZRangeByScore或ZRemRangeByScore命令则可以在合适的时机取出或删除已到期的任务。如下图:

图片

实现步骤主要包括:

1、任务入队:将任务详情序列化后存储,并以其执行时间戳作为score,通过ZADD命令加入到Sorted Set中。

2、任务出队:使用ZRANGEBYSCORE命令,配合WITHSCORES选项,获取当前时间戳之前的所有任务,并通过分数(score)判断哪些任务已经到期,然后进行处理。

3、周期性检查:通过启动一个额外的定时任务周期性检查并处理已到期的任务。

通过额外的定时任务检查还是挺麻烦的,是否可以使用Redis的Keyspace Notifications,订阅key过期事件来做?

答案是不可以。因为Redis的key过期事件并不能保证key过期的时刻能够及时发出通知事件,甚至不能保证key过期能发出事件。原因是,Redis删除过期key的时机是:客户端访问该key时Redis服务端发现过期或者Redis后台任务检测到这个key过期。如果一直不访问这个key,那有可能长期不能发现key过期,也就不会产生key过期的事件了。设置的key过期精确度如此不可控,这对于大部分使用延迟队列的业务场景应该是不可接受的。

实现生产可用的延迟队列还需要关注什么?

按照上述的思路去具体实现一个延迟队列的话,还需要关注以下几点,这样才能打造出一个生产环境可用的好方案。

1、首先是性能。如果底层只采用一个Sorted Set,数据量大的时候,比如同时有几百万人下单,这些数据被存储到同一个Sorted Set,就容易引发性能瓶颈。可以采用指定数量的Sorted Set来解决此问题,这样生产和消费延迟消息的并发处理效率会提升。

2、其次是原子操作。在消费消息的时候可能涉及查询和删除的两步操作,有可能还涉及数据库等其他操作,如果部分处理失败,可能会造成消息丢失或者重复处理的问题。需要采用重试机制和幂等处理机制来应对。

3、最后是简单易用的封装。要实现好延迟队列,不是一件轻松的事儿。可设计上报延迟消息、到期回调处理两个接口,简化延迟队列的接入成本。可以参考Redission等封装实现,使用Sorted Set、消息Pub/Sub、Stream等结合实现完善的延迟队列。

Redis系列面试题整理

项目中有用过Redis么,用在哪里?

我们项目中有用的,主要用了3个场景:

1.缓存相关的场景,我们是做在线教育的,内容模板会有很多课程相关,这些数据在DB单表有600W;如果走mysql查询会很比较慢,用户体验感比较差,并发也上不去。所以我们做了些接口缓存、课程内容的对象缓存。提升了性能的同时,还解决了本地缓存不一致问题。

2.同时,我们由于它是分布式的,并且可以设置过期时间,也会用来保存用户token。因为token也是有过期时间的,用Redis来保存刚好满足。

3.我们还用它去基于日期+incr自增指令实现了一个分布式ID。因为我们的课程ID比较大,需要分库分表,数据库自增满足不了我们需求。

你们缓存的单个key是多大的?会去控制key的大小么?

我们的key因为有压缩,所以我们的key一般会控制在10K之内,比较大的key我们是不会放入Redis的。

因为如果Key过大,会由于带宽、网络等原因导致指令返回速度过慢,Redis又是单线程的,会阻塞其他指令执行。

什么是缓存雪崩、缓存穿透、缓存击穿?你怎么解决?

其实,缓存雪崩、穿透以及击穿,最终导致的问题都是我的请求绕过了我们的缓存中间件,直接打到了DB的集群,导致DB的压力过大或者造成DB崩溃的现象。只不过场景稍微有些不一样。

缓存雪崩

缓存雪崩主要有大量的key同时打到DB,那么产生这种场景有2种主要原因。

1.缓存中间件服务不可用,缓存失效,导致所有的请求全部打到DB,那么这种解决方案就是尽可能保证缓存中间件的高可用,比如Redis缓存,那么就采用Redis的cluster集群,以及提前做好缓存组件报警机制,杜绝宕机。

2.缓存中大部分key同时过期,比如Redis中key同时过期,对于这样的情况,可以在失效时间上增加一个1到5分钟的随机值。或者对热数据,进行访问的时候,自动续期。

缓存穿透 

缓存穿透是指缓存和数据库中都没有的数据,但是用户一直请求不存在的数据!这时的用户很可能就是攻击者,恶意搞你们公司的,攻击会导致数据库压力过大。

1.我们可以在缓存之外再加一层性能更好的过滤方案,比如布隆过滤器或者布谷鸟过滤器。如果数据不存在,直接在过滤器进行过滤掉,不会打到Redis跟DB。

2.如果发现是恶意攻击,可以对IP进行处理,加入IP黑名单。

缓存击穿

单个key过期的时候有大量并发,我们可以采用dcl锁等方式,但是我觉得如果要采用锁来保证,那么就没有必要用到缓存。

所以,在我看来,您提出来的这个问题,有点过于放大了它带来的影响。

首先,在一个成熟的系统里面,对于比较重要的热点数据,必然会有一个专门缓存系统来维护,同时它的过期时间的维护必然和其他业务的key会有一定的差别。而且非常重要的场景,我们还会设计多级缓存系统。

其次,即便是触发了缓存雪崩,数据库本身的容灾能力也并没有那么脆弱,数据库的主从、双主、读写分离这些策略都能够很好的缓解并发流量。

最后,数据库本身也有最大连接数的限制,超过限制的请求会被拒绝,再结合熔断机制,也能够很好的保护数据库系统,最多就是造成部分用户体验不好。

有遇到过缓存跟DB的数据不一致么,怎么产生的?怎么解决?

遇到过的,主要是因为操作DB跟redis这2个操作,不是原子性操作,所以在并发的时候,可能会导致数据不一致。其实就是并发导致的原子性问题。在拿到数据准备放入Redis的時候,另外的线程对DB数据进行了改动。

解决方案:

最优最好的方案:给Redis的缓存数据加上过期时间。达到最终一致性,就算不一致,在过期时间到了后,就能一致。

当然网上也有说通过锁、延迟双删;但是我觉得都不太适合我们的应用场景的。因为我们Redis本来就是为了提升性能。如果再加上一些消耗性能的方式来保证强一致性,那么你干脆不要用Redis。

设置过期时间会从Redis删除,你知道怎么删的吗?

删除的话,Redis主要有2种方式:

1.惰性删除 当我去获取这个key的时候,判断有没有过期,过期了就删除。

2.定期过期 Redis会有个事件驱动机制,每隔一段时间会去扫描我的key里面是否有过期的数据,如果有,我就去删除。

隔多久可以根据config文件配置,扫描也不会扫描所有的,而是分批去扫描。

还知道Redis的其他的一些数据类型的使用场景么?

我们工作中,刚才我讲的缓存、自增ID、保存token其实都是基于String做的。

但是由于Redis性能比DB高,所以根据不同的缓存场景,我可以用不同的数据类型来支撑,比如hash我就可以用来做对象型的数据缓存,比如我们之前课程会有浏览数、购买数 这些数量可以用hash来存储。

还有一些集合类的缓存,可以用list、set存储。如果我的数据有分数排行的,可以用zset做排行缓存。

Redis2.8.9后引入了基于HyperLogLog算法的HyperLogLog数据类型。可以做一些统计类的数据,比如UV,这个的好处就是内存会占用很小很小。但是会有一定的误差,也只能做统计。

Redis3.2后,也加入了GEO的数据类型。可以做一些查询附近地址等功能。可以存储经纬度,然后可以拿到彼此的相差距离,比如搜索饭店,哪个离我最近,有多远。

Redis5.0后又引入了Stream,可以做类似mq的发布订阅功能,并且支持ack回执。

你刚才讲用Redis做缓存,那么性能肯定比Mysql要高,Redis是怎么设计的?

1.Redis是基于内存操作的,节省了跟磁盘的实时IO,但是后台还是会异步进行磁盘备份。所以数据一致性不强,会有数据丢失。

2.由于是基于内存操作,所以单次执行非常快,瓶颈不在cpu。设计的是单线程指令执行。减少了执行指令时候的线程切换。

3.Redis本身就是一个k-v保存结构。查询数据的时间接近O(1),只有当数据发生冲突的时候才会遍历链表,跟HashMap很类似。

4.Redis底层的数据结构支持,思想就是空间换时间,比如SDS、跳表。

5.网络模型采用的是多路复用。大大减少了网络IO的时间消耗。

跳表用在哪里?跳表的思路是什么

跳表主要用在zset的有序数据集合类型。

跳表的思路跟Mysql的b+树差不多,主要是我的数据会保存一个随机的层级,每个层级会跟相同层级的数据建立一个双向链表。

找数据的时候,会从最外层开始,然后数据是有序的,查询过程跟二分法一样。

核心思想就是用空间来换取时间,提升查询性能。

你说k-v跟HashMap很类似,解决冲突用的是链表,那为什么Redis用头插,而HashMap用的是尾插?

HashMap用尾插,是因为在并发的场景下,可能会导致链表的死链。

但是Redis是单线程执行,所以不会有并发导致死链,头插法比尾插速度要快很多。所以用头插法即可。

那Redis有没有像HashMap一样的扩容机制?

Redis肯定也有扩容机制,因为如果没有扩容的话,会导致链表越来越长,从而降低查询性能。

只不过Redis的扩容机制跟HashMap有点不一样,Redis会有2个hashTable,第二个table只有在扩容的时候使用,当第一个table的容量达到一定量,这个量正常是已有的数据是table大小的时候就会扩容,但是当有在进行持久化的时候,使用量是table容量的5倍的时候扩容。

扩容也不会一下子都扩容完成,因为一下子把所有的数据从第一个table移到到第二个table耗时太长。

所以会采用渐进式rehash,分批次的将数据迁移到第二个table。然后把第一个table变量指向新table。第二个table赋空。

既然是基于内存的,你有遇到过内存满的情况么,怎么处理的?

有遇到过的,内存满了的话,会报OOM错误,数据能读不能写了,因为没有足够的内存去接收新的数据。

至于解决方案:

1.首先想到的肯定是去加内存,包括加大单机的内存,或者横向扩容。

当然,如果你允许一些老的、或者冷门的数据删除的话,你也可以在config文件设置淘汰策略。

默认官方提供8种淘汰策略:有3种算法(LRU、LFU、random),然后每种算法的淘汰范围都可以设置淘汰范围 allkeys还是volatile 是淘汰所有的数据还是只淘汰设置了过期时间的。再加上novication不淘汰、根据volatile-ttl过期时间淘汰。

  • volatile-lru:从已设置过期时间的数据中挑选最近最少使用的数据淘汰
  • volatile-lfu:从已设置过期时间的数据中挑选使用次数最少的数据淘汰
  • volatile-ttl:从已设置过期时间的数据中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据中任意选择数据淘汰
  • allkeys-lru:从所有数据中挑选最近最少使用的数据淘汰
  • allkeys-lfu:从所有数据中挑选使用次数最少的数据淘汰
  • allkeys-random:从所有数据中任意选择数据淘汰
  • noeviction(驱逐):禁止驱逐数据,新写入操作会报错

我记得Redis中有个淘汰池的概念,它是干嘛的?

首先,淘汰是根据淘汰算法去找到需要淘汰的数据,比如lru就是找到最久未使用的,LFU是找到最少使用的。

但是如果去遍历所有的key,那么就会导致要精确的查出来要淘汰的数据会很慢,从而阻塞数据的添加。

所以Redis中采用的是取样淘汰,取样数量可以设置,从取样的数据中找到最适合淘汰的。这样就大大的影响了淘汰的精确度。

所以,加了一个淘汰池的概念,默认是16,如果淘汰池没满,那么取样出来适合淘汰的数据,假如淘汰池。

如果满了,跟里面的数据做比较,如果比淘汰池里面的数据更适合淘汰。就顶掉之前的。

然后淘汰的时候,就从淘汰池中末尾淘汰。

所以,在我看来,淘汰池的作用就是去提升我们淘汰的精确度。

基于内存操作,怎么去保证我宕机的时候,数据不会丢失?

虽然是基于内存操作,但是持久化还是会做的,Redis重启的时候,都会从我们的磁盘加载数据,然后再基于内存操作。

数据也会持久化到磁盘,只不过实时性不是特别高,同步方式主要有2种,RDB与AOF。

RDB默认的持久化方式,触发时机可以通过save指令来配置。同时shutdown、flushall、save/bgsave指令也会触发。

AOF默认不开启,但是开启了就会默认用aof来加载数据。

AOF是指令追加的形式来刷新到磁盘,但是如果每次都要保证刷到磁盘的话,就每次都需要跟磁盘IO,所以Redis会提供一些选择方案:

比如默认每秒刷到磁盘,或者每次刷到磁盘,或者交给操作系统,方案不一样,数据一致性跟性能就会有不同的影响。

AOF指令追加,那么文件越来越大怎么办?

AOF文件会有重写的过程,重写就是当我的文件过大的时候,我会触发重写,这个过大到底多少才大,是可以配置的。

重写其实就是压缩的过程,会以RDB的文件格式来压缩现有的AOF。所以也是AOF+RDB的混合模式。

你们公司用的Redis集群是什么?

我们用的Redis cluster集群。

你们用cluster集群是基于什么考虑的?

cluster是一个去中心化多主多从的集群架构。

cluster集群除了能实现主从自动切换以外,cluster还可以做到数据分片,我可以把我的数据灵活的分散到不同的主,然后主对应的从会读取主的数据。

那数据怎么知道我要往哪个主写?

cluster里面会有个虚拟槽的概念,其实就是0-16383的一个数值。

然后我们的key会通过一个hash算法,然后取模16384得到一个0到16383的值。

这个虚拟槽又会对应到不同的机器,并且虚拟槽跟机器的对应关系是能手动维护的,这样我就知道这个key会放在哪台机器呢。

并且整个集群维护会非常灵活,一些热点的key我都可以单独部署一台主。

但是这些虚拟槽必须要分配完成,就是每个槽都必须能找到对应的实例,不然集群不可用。

你说key是根据一个hash算法得到虚拟槽的,那我希望一个订单相关的key都到一台机器,比如订单的统计数据、订单的基本信息等等,是不同的key,会分散到不同的机器,查询就会查询多台机器,怎么解决?

作者也想到了这个具体的业务问题,所以,当你希望相关的数据放在同一个key的话,你可以在key上加{}.这样hash的算法只会去根据{}里面的值来进行虚拟槽的计算了。

cluster里面的slave是怎么保证跟master一致的?

它的流程是这样的:

全量同步

1.slave会每隔一段时间发起同步指令,这个同步指令会包含我之前同步的数据的偏移量以及我对应的masterId。

2.master收到指令后,会去判断传过来的masterId是否跟自己的ID一致,如果不一致,那么说明之前没有同步过,master会通过bgsave生成一个rdb文件给到slave,slave会清空自己的数据然后基于rdb文件去加载数据。

但是由于bgsave是不会阻塞主线程的,所以master会有新的指令,这些新的指令会暂存到master的一个内存空间,这个内存空间 每个slave独立。等slave rdb文件加载完后,把新的指令给到slave执行。

这个就是我们经常说的全量同步。

增量同步

但是有一种场景,我的slave可能只是断开了一段时间,只有一部分数据没跟master同步。所以会有个增量过程。

master收到指令后,如果masterId跟自己的ID一致,那么会根据数据的偏移量去一个内存缓存中找相差的数据,这个缓存叫做积压缓存,大小可以设置,跟mysql的redolog很类似,是覆盖写。如果能找到相差的数据,那么就同步给slave。

这个就是我们讲的增量同步。

但是因为覆盖写,所以可能断开太久,这个相差的数据找不到了,也会触发全量同步。

总结下:master如果根据slave的参数,能找到相差的数据,就把相差的数据同步给slave.如果找不到,那么就触发全量同步。

指令同步

同时,master的指令也会异步同步给slave。

你们项目中用的是哪个客户端?

Redis对不同的语言都支持不同的客户端,因为我们是基于Springboot开发,所以我们的客户端主要是Springboot自带的letuce。

也用过Redisson,因为它对于我们的业务做了很好的封装,用起来也比较简单。比如锁啊这些。

假如Redis线上突然阻塞了,你会怎么排查?

指令阻塞其他指令

因为Redis执行指令是单线程的,因为单次执行速度会非常快,但是如果你让单次执行变慢了,那么也会阻塞后续的指令执行。

哪些情况会让指令变慢:

1.指令获取的数据很多,比如大数据量下执行keys、hgetall、smembers等指令。我们可以通过查看Redis的慢查找到问题,不要去执行慢查操作。

2.大Key,我单次查询的的数据过大,也会导致单次执行变慢。所以我们需要拆分大key。

CPU扛不住了

因为Redis处理命令只用到一个cpu,所以当cpu超过我们负荷的时候,也会导致阻塞。

1.Redis是否有牺牲cpu去换取内存的一些配置,比如数据类型底层数据结构的配置。有些数据类型是去追求极致空间,所以会牺牲CPU。

2.如果也没有,那就是需要加机器了。

持久化阻塞

1.虽然说Redis的持久化是异步做的,但是fork子进程的时候就慢,那么也会导致问题。

2.AOF刷盘的时候,由于磁盘压力,导致操作时间够久,主线程为了数据一致性会阻塞指令。

其他原因

1.其他的程序跟Redis部署在一台机器,导致cpu之间的相互影响。

2.网络问题 包括网络是否稳定,是否有延迟、带宽是否足够。

3.客户端连接数是否达到了Redis的最大连接数。

什么是Redis?

Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。

传统数据库遵循 ACID 规则。而 Nosql(Not Only SQL 的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称) 一般为分布式而分布式一般遵循 CAP 定理。

Github 源码:https://github.com/antirez/redis。

Redis 官网:https://redis.io/。

与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value 数据库。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

Redis支持的数据类型?

Redis 可以存储键和不同类型的值之间的映射。键的类型只能为字符串,值常见有五种数据类型:字符串、列表、集合、散列表、有序集合。

String字符串

格式: set key value

string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。

string类型是Redis最基本的数据类型,一个键最大能存储512MB。

Hash(哈希)

格式: hmset name key1 value1 key2 value2

Redis hash 是一个键值(key=>value)对集合。

Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

List(列表)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

格式: lpush name value

在 key 对应 list 的头部添加字符串元素。

格式: rpush name value

在 key 对应 list 的尾部添加字符串元素。

格式: lrem name index

key 对应 list 中删除 count 个和 value 相同的元素。

格式: llen name

返回 key 对应 list 的长度。

Set(集合)

格式: sadd name value

Redis的Set是string类型的无序集合。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

zset(sorted set:有序集合)

格式: zadd name score value

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部