Advertisement

Redis 相关知识点(持续更新)

阅读量:

一、什么是NoSQL

要介绍Redis前必须要先介绍下NoSQL,这两者间密不可分。什么是NoSQL?

NoSQL也就是(not only SQL),它不仅涵盖了传统的SQL技术。非关系数据库在处理高并发任务时表现出色,在很多场景中都能提供更好的性能。此外,NoSQL在数据分析和数据挖掘方面同样具有显著的优势。Redis和MongoDB作为当前较为流行的非关系型数据库之一。

二、Redis和MySQL的区别

两者在外在表现上的差异主要体现在性能上的差异程度: Redis在单位时间内执行读/写的速率往往比MySQL快几倍到十几倍。

之所以两者有这么大性能差异主要因为两方面:

  1. MySQL的数据持久化采用磁盘作为存储介质而非内存,在读取与写入性能上更具优势。
  2. MySQL的数据存储与查询机制相对复杂,在设计过程中需要考虑索引、数据库范式以及其他优化措施以提高性能;相比之下,在实现上 Redis 的机制更为简单直接。需要注意的是,在处理大量并发客户端连接时 Redis 使用的是 非阻塞 IO(IO 多路复用) 方式:通过将多个输入输出通道同时工作以减少操作系统的切换开销;同时通过多路复用 IO 方式减少了 I/O 等待时间。

虽然 Redis 具备诸多优势与特点,在分布式系统设计中展现出显著的应用价值与可行性[1] ,但就其局限性而言,并不能完全取代主流的关系型数据库如 MySQL 等技术方案[2] 。具体而言,在基于内存的 Redis 数据库架构中存在明显缺陷:其一是在断电状态下会导致全部数据丢失;其二相较于使用磁盘存储的数据库系统而言,在内存使用成本方面具有明显劣势;此外虽然Redis实现了事务管理功能[3] ,但在面对复杂的应用场景时仍显力有未逮;综合来看,在MySQL 与 Redis 的合理搭配下才能满足现代应用对数据库性能的需求

举例来说,在进行秒杀活动的商品秒杀过程中,在遇到大量并发请求的时候,MYSQL必须在短时间内处理成千上万的SQL指令,这种情况下很容易导致数据库出现性能瓶颈,也就是我们常说的‘罢工’现象.针对这种情况,通常会采用异步 writes 来解决,而在处理高并发读取请求的时候,我们会在MYSQL和REDIS之间建立了一条消息队列通道.当达到一定负载阈值后,通过在MYSQL与REDIS之间建立通道后自动触发缓存数据的同步到MYSQL

也就是所有请求到达时都在redis中执行write操作而无需进行数据库的write操作由于redis的高性能使得系统能够快速响应。当请求数量减少或业务处于停滞状态时例如商品秒杀已结束或红包抢购已售罄的情况将Redis缓存的数据迁移到数据库中以实现持久化存储。

类似地,在进行读取操作时,系统会优先调用Redis缓存机制获取数据;如果Redis读取失败,则会转而从MySQL等其他数据源进行补充。

三、什么场景下考虑使用Redis?

之前提到Redis通常与MySQL一起应用。那什么时候应该用Redis呢?主要参考以下几个方面来看:

  1. 数据的命中率高低如何?若较低,则无需选用Redis。
  2. 是读多还是写多?若为后者,则不建议采用Redis。
  3. 数据规模如何?若较大,则可能给内存带来压力。

四、Redis支持的6种数据类型

该文章深入探讨了Redis底层所采用的核心数据结构及其相关特性。其中有序集合常用于存储按照一定顺序排列的数据元素;哈希表主要基于键值对进行数据存储与检索;列表通常用于动态管理数量可变的一系列元素;而字符串则主要用于存储单一字符。文章还详细阐述了这些数据结构在 Redis 中的应用场景及其性能特点,并通过实例分析展示了如何高效利用这些数据结构来优化 Redis 应用的性能表现

五、Spring中集成Redis

添加依赖

复制代码
 <!-- Redis依赖 -->

    
 <dependency>
    
    <groupId>redis.clients</groupId>
    
    <artifactId>jedis</artifactId>
    
    <version>2.7.1</version>
    
 </dependency>

做个简单的性能测试:

复制代码
 public void performance() {

    
     Jedis jedis = new Jedis("localhost", 6379);
    
     int i = 0;
    
     long start = System.currentTimeMillis();
    
     while (true) {
    
    long end = System.currentTimeMillis();
    
    if (end - start >= 1000) {
    
        break;
    
    }
    
    i++;
    
    jedis.set("key" + i, "value" +i);
    
     }
    
     System.out.println("redis每秒写入" + i + "次");
    
 }

测试结果显示Redis平均每秒提交19023次。这些测试是在串行模式下完成的,每一条记录是一次独立的操作,如果采用流水线处理技术,则性能会明显提升。

六、获取Redis连接的两种方式 及 两种操作方式

获取Redis连接有两种方式:

  1. 通过jedisPool
  2. 通过JedisConnectionFactory连接工厂

两种方式也分别对应两种操作方式:

Jedis

RedisTemplate

七、Redis的事务

在 Redis 中开启事务采用 multi 命令, 而用于执行事务的命令是 exec。Redis 的 multi 和 exec 命令之间会将请求暂存于队列中, 直到遇到 next exec command到来时, 才会一次性将该队列中的所有命令提交给服务器进行处理。在此期间, 在线的客户端无法再向服务器提交新的请求或修改操作, 这就是 Redis 的事务机制。

当发生回滚操作时,在处理完成后会将该操作加入到事务队列中以确保相关的操作不会被后续的事务执行。

在应用了 discard 指令之后,在随后应用 exec 指令时会出现错误。这是因为 discard 指令已经取消了事务内的相关操作。而当执行到 exec 的时候,在队列中已不再有任何可执行的操作。从而导致出现错误的情况。

八、发布订阅

当使用银行卡进行消费时,银行通常会通过微信、短信或邮件向用户发送交易信息。这相当于采用发布订阅模式,在这一模式中发布的环节是释放交易信息的信号码或提示信息。订阅则对应于各个信息发布的渠道。在实际工作中这一模式应用广泛,在分布式系统设计中尤其常见。Redis能够实现这一发布订阅模式

此内容涉及的概念为'观察者模式'(Observer pattern),具体可作为参考链接:[《深入理解 Observer 观察者模式》]( "深入理解 Observer 观察者模式")

发布订阅模式首先需要信息来源作为基础条件,在这种情况下信息必须能够向外传递出去。例如案例中的银行通知机制就属于这一范畴。具体来说首先由银行记账系统的接收端接收到交易指令随后经过完整的记账流程确保交易记录无误之后系统会将相关信息传递出去供订阅方及时响应与处理相关信息而观察者模式正是这一机制最典型的运用形式。

这个过程可以使用redis客户端模拟一下:

启动了三个客户端,并通过SUBSCRIBE命令注册了两个观测者(这两个都是客户端)。此外,在这些观测者之间创建了一个名为channel通讯渠道。一旦有消息发布到该渠道中,则会被所有观测者接收。

再使用第三个客户端(发布者,下面的一个客户端)向chat通道发布消息;

最后两个观察者都同时收到了发布的消息。

8.1 监听类

如何在实际代码中实现订阅发布功能?首先需要了解如何实现监听机制或订阅机制,在满足需求的前提下具体来说,在满足需求的前提下并重写onMessage()方法即可完成相关功能

复制代码
 public class Demo1 implements MessageListener {

    
     
    
     @Override
    
     public void onMessage(Message message, byte[] pattern) { 
    
     // 获取消息
    
     byte[] body= message . getBody() ;
    
     // 获取 channel
    
     byte[] channel = message . getChannel();
    
     }
    
 }

这里你肯定想知道Message类有啥,它实际是个接口,源码如下:

Message接口有个实现类DefaultMessage,源码如下:

8.2 怎么发布?

上面已经准备好接收了,那怎么发布信息呢?

复制代码
 public void publishMessage(){

    
     String channel = "chat";
    
     redisTemplate.convertAndSend(channel, "Hello");
    
 }

该方法的功能是向渠道 chat 发送消息。一旦发送后,相应的监听者就能接收消息。

九、Redis 内存回收策略

由于Redi可能会因内存不足而出现错误,并且长时间回收可能导致系统长时间停滞不前,在这种情况下掌握执行回收策略变得至关重要

该内存回收策略可在redis.windows.conf(redis.conf,L unix)中设置,并建议查阅文档中的详细说明:

有6种策略,默认为:noeviction

Olati-lru:该算法基于最近使用频率最少的原则进行内存管理,在具体实现中仅淘汰那些未超时(即未达到预设超时时间)的键值对。

该算法遵循基于最小使用频率的淘汰机制,在处理所有键值对时(不仅仅局限于已过时者),该方法会优先删除那些未被访问时间最短的部分

olatil-random :采用随机淘汰策略删除超时的(仅是超时的)键值对

通过随机淘汰的方法移除全部键值对(不仅仅限于那些已超时的情况),这一方法较少被采用

volatile-ttl: 采用删除存活时间最短的键值对策略

noeviction表示该系统不会淘汰任何键值对。当内存达到最大容量时进行读取操作(如GET指令)则正常运行。进行写入操作则会返回错误信息。换句话说,在这种情况下Redis系统一旦内存达到最大容量就只能执行读取操作而无法进行writes。

volatile-lru:当向容器中添加键时,若达到超时条件,则会移除那些设置了超时时间但未被使用最长的键

volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键

allkeys-lfu:从所有键中驱逐使用频率最少的键

十、Redis的持久化机制

Redis是一种基于内存的数据存储系统,在实际应用中我们发现记忆中的数据更新速度非常快而且容易出现不可预测的变化与丢失问题。为此Redis设计并实现了两种可靠的数据持久化机制分别为RDB(Redis 数据库)与AOF(Append Only File)。这些设置参数都详细记录在 redis.conf 文件内该文件不仅存储着基于RDB与AOF两种持久化方法的相关配置信息还包含了所有与Redis运行相关的设置参数信息。了解整个 Redis 的持久化实现流程对于正确管理和优化数据库性能具有重要意义

10.1 Redis持久化流程

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

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

服务层通过API向一个标准库函数发送了一个操作请求,该操作请求使得数据被写入磁盘;其中的数据存储于内存中的缓存区中。

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

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

10.2 RDB机制

RDB本质上就是将数据以静态存储形式固定在磁盘上。所谓快照,则是指通过捕获当前数据状态的方式实现信息保留。

RDB持久化即指在特定时间段内将内存中的数据集合以快照形式保存至硬盘上。这也是默认采用的一种持久化方式,在这种情况下内存中的数据会被以快照形式存储于一个二进制文件中,默认名称为dump.rdb。当Redis服务器启动时,默认情况下系统会读取dump.rdb文件中的数据重新加载至内存中。

采用RDB持久化模式相对较为简便。应用程序可被引导至Redis服务器并执行save或bgsave命令以生成rdb文件。此外,应用程序也可通过配置文件设定触发RDB条件的参数设置。

10.2.1 save触发方式

该命令会导致当前Redis服务器被阻塞,在执行save命令期间 Redis无法处理其他命令 直到RDB过程完成时 无法响应客户端请求

10.2.2 bgsave触发方式

相较于save命令而言,bgsave命令作为一个异步操作运行于后端。Redis会在后台执行快照生成的异步操作,并在生成快照的同时,该快照还能处理客户端的请求。

当客户端向Redis服务发送bgsave命令时,在接收到相关命令后,Redis服务器的主进程中创建一个子进程中执行数据同步操作;完成数据保存至rdb文件后,在完成这一操作后该子进程中将退出。

相较于save命令而言,在处理bgsave时Redis服务器采用了更为复杂的机制——通过使用子线程来进行IO操作,并且主进程中依然能够接收和处理其他请求。然而,在执行过程中,并没有影响其同步性——这表明即使在forked子进程中也会导致同样的问题出现。当一个forked子进程长时间运行(通常情况下很快完成)时这可能会导致bgsave命令暂时阻塞无法响应新客户端的需求。

10.2.3 自动触发(通过服务器配置文件指定触发RDB条件)

自动触发功能是基于我们的配置文件实现的,在Redis.conf中存在相关设置项,请您根据需要进行相应的参数调整

save 是用来配置触发 Redis 的 RDB 持久化条件的指令,在这种情况下它会检测内存中数据的变化频率并决定何时执行持久化操作。例如,在命令行界面中输入 'save m n' 这一语法结构时,默认情况下它会在 m 秒内发生 n 次修改时自动触发持久化操作。具体来说,默认情况下 save 命令表示在 m 秒内数据集存在 n 次修改时会自动触发 bgsave 操作;而设置为 save 60 1万的话,则会在 60 秒内至少有 1 个 key 的值变化时自动触发 bgsave 操作,并且当至少有 1万次修改发生时才会进行一次完整的持久化操作以保存当前的数据块。

该变量的默认设置为yes:stop-writes-on-bgsave-error。当启用RDB并发生一次后端保存失败的情况时,请问Redis是否会暂停接收新写入的数据?这将促使用户注意到数据未能成功持久化到存储介质上;否则人们通常不会察觉这一严重问题的存在。然而一旦Redis重新启动后,在此之前发生的任何问题都不会影响后续操作。

rdbcompression;其预设值为yes。当对快照进行存储至磁盘操作时,则允许指定是否执行压缩操作。

rdbchecksum :默认值设置为yes。当生成快照时,可以选择性地应用CRC64算法来验证数据完整性。这样操作通常会导致约10%的性能开销增加。若追求最大化的性能提升,则建议选择关闭该选项。

⑤dbfilename :设置快照的文件名,默认是 dump.rdb

⑥dir:指定快照文件存储的位置这一配置参数必须指定为目录而不是文件名。

该RDB的触发机制基于服务器配置文件,在满足触发条件后会启动一个子进程以同步数据。然而为避免潜在问题应尽量不采用此方法以控制RDB持久化的开启。具体而言若时间设置过短会导致频繁同步rdb文件从而影响性能而长时间设置可能导致数据丢失。

RDB 的优势和劣势:

①、优势

(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。

在生成RDB文件的过程中,在Redis服务器启动时会调用fork函数以启动一个子进程负责处理所有的存储事务。该主进程中不会执行任何磁盘I/O操作

(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

②、劣势

RDB快照相当于对数据库执行了一次全量备份。该操作以二进制序列化的形式将内存中的所有数据进行了完整记录。由于这种存储方式在空间占用上非常高效,在执行快速复制操作时,则会启动一个专门用于执行快速复制操作的子程序。这个子程序将继承当前主程序的所有内存副本,并且在主程序中的任何修改操作都不会影响到该子程序的状态。因此,在整个快速复制过程中修改的数据不会被保存下来可能会导致丢失未完成的数据。

10.3 AOF机制

全量备份往往耗时较长,在这种情况下我们偶尔会采用更为高效的方式AOF(Archive of Field Operations)。该运行机制相对简单:每当有一个‘写’命令到达时,Redis会通过write函数将其追加到文件中。每当接收一个‘写’命令时,请记住我们的数据会被永久保存在AOF文件中。若appendonly配置设为否,则该系统将禁用AOF方式进行备份。

10.3.1 AOF原理

采用AOF的方式可能会引发另一个问题。然而,在这种情况下, 持久化的文件体积可能会变得越来越大, 导致存储空间不足的问题出现。为了减少这种持续增长的趋势, Redis提供了一种名为bgrewriteaof的功能, 该功能允许系统通过将内存中的数据以命令的形式保存到临时存储区域, 并启动一个新的进程来进行重写操作, 从而有效地控制了持久化存储的增长速度。

在完成对aof文件的更新操作时,并未涉及读取旧版本的aof文件;相反地是以命令形式将内存中的全部数据库数据重新组织并保存到一个新的aof文件中。这种方法与快照机制存在相似之处

AOF也有三种触发机制

每修改持续同步:持久同步机制确保所有数据变更都会被即时记录至磁盘 该方案虽在性能上存在不足 但在保障数据完整性方面表现出色

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

(3)不同no:从不同步

10.3.2 AOF优缺点

优点:

AOF能够更有效地防止数据丢失。在一般情况下,每隔1秒(即每秒一次),通过一个后台线程执行一次fsync操作,从而确保最多只丢失1秒钟的数据

(2)AOF日志文件零磁盘访问开销,在写入方面表现出色,并且该存储方案具有良好的耐冲击性。

当AOF日志文件的数据量变得非常大时,在发生后端重写操作的过程中,对客户端的数据读取和书写操作不会产生任何影响。

(4)记录AOF日志文件的命令采用了极具可读性的格式,在特殊情况下特别适合用于处理重大数据丢失事件的紧急恢复工作。例如,在某次操作中不小心使用flushall命令清除了全部数据记录,在后台rewrite尚未完成的情况下,则可以直接将该AOF文件复制到本地存储空间中,并删除该条flushall命令指令后将其放回即可通过恢复机制自动补足所有数据。

缺点:

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

(2)当AOF参数启用时,在这种模式下能够实现的日志 writes QPS值通常低于在RDB模式下实现的日志 writes QPS值。这是因为AOF设置通常将日志同步频率定为每隔一秒同步一次日志文件;同时这一设置虽然看似降低了同步频率从而影响性能表现度但整体系统的运行效率依然保持在较高水平。

过去曾出现过AOF相关的问题。具体表现为,在进行数据恢复操作时,基于AOF记录的日志文件并未成功完全还原出一致的数据内容。

10.4 RDB和AOF到底该如何选择

建议选择两种持久化机制结合起来使用会更优。由于两个持久化机制已经理解了它们的特点与适用场景之后,在实际应用中还需要根据具体情况来决定如何配置它们之间的关系。具体的需求不同情况下可能会有不同的最佳选择。图可作为参考

对于快照备份而言,在当前Redis规模较大时进行快照备份可能导致Redis出现卡顿现象;然而在实际应用中这种问题通常能够迅速解决并不影响系统的正常运行。相比之下AOF备份则只需追加执行相应的命令即可完成;其优点在于不会对Redis运行造成额外负担但其缺点在于恢复后需要重新执行这些命令以完成数据恢复过程。此外AOF备份的数据量可能会比较大因此在实际操作中需要注意合理规划存储空间以避免存储压力过大影响系统的性能。

十一、Redis 部署架构

11.1 单机版

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

11.2 主从复制

Redis 的复制功能支持通过指定一个 Redis 服务器生成任意数量的复制品,在这些复制品中选择一个作为 master(主 server),其余则称为 slave(从 server)。当 master 和 slave 之间的网络连接正常时,
master 持续同步自身发生的更新至每个 slave。从而确保 master 和所有 slave 服务器始终保持数据一致。

特点:

  1. 定义了 master/slave 角色设定;
  2. 确保 master 和 slave 数据保持一致;
  3. 将 master 读压在传递给从库时得到显著降低。

问题: 1、存在高可用性的问题(具体而言,在 master 失效的情况下, 所有 subsequent slave 都会受到影响);2、压力未得到有效缓解。

11.3 哨兵

哨兵模式是一种独特的模式,在这种情况下Redis提供了执行哨兵命令的方式。这种机制中, 哨兵作为一个独立运行的进程,在作为进程的情况下,则会独立运行,从而实现对多个正在运行中的Redis实例的有效监控。其基本原理是通过发出命令并等待Redis服务提供响应,以便持续跟踪和管理这些实例的行为状态

这里的哨兵有两个作用:

通过发送相应的指令至Redis服务使其返回对其运行状态的监控结果涵盖主节点和从节点

当哨兵检测到 master 服务出现故障时(或者出现宕机状态),系统会自动启动备用机制,在 slave 节点中自动选出新的 master 作为备用主节点。随后,在 slave 节点中自动选出新的 master 作为备用主节点后(或者切换为主机节点),系统会将被选中的 slave 结点切换为主机节点,并通过发布订阅消息的方式通知所有从服务节点(或者从服务),以完成主机的更换过程。

在现实中,在单一的哨兵进程对 Redis 服务器实施监控时(虽然)可能引发问题,则可以部署多个独立的哨兵进程进行实时监控;同时,在每个哨兵内部也会设置子进程持续监督其他所有哨兵的状态;这样一来,则形成了一个由多层独立负责的多层次防御体系;此外,在每个层次上的哨兵节点也会主动检查本层内的所有节点是否正常运行

Redis sentry 是一种分布式系统中的实时监控解决方案,在主从服务器失效时能触发主动故障切换。其核心功能包含实时监控、智能报警机制以及自动负载均衡策略等三项主要功能特点:

  1. 实时监控功能:持续对 Redis 主从服务器运行状态进行动态评估;
  2. 智能报警机制:在关键节点异常情况下自动触发报警并发送相关通知;
  3. 自动负载均衡策略:实现资源的优化分配以保障服务可用性。
    技术特点
  • 采用分布式架构提升整体系统的可靠性和扩展性;
  • 配备多级负载均衡算法以减少服务中断概率;
  • 提供弹性伸缩能力以应对负载波动需求。
    系统问题:主从切换过程中存在短暂的数据丢失风险,并未有效缓解 master节点 writes压力问题。

11.4 集群(proxy型)

Twemproxy 是一个开源的 Twitter 项目,在 Redis 和 Memcache 上提供高效且资源占用低的代理服务;它是一个设计精简、专注于性能优化的代理工具,在内存缓存领域表现卓越。主要特点如下:

多种哈希算法包括 MD5, CRC16, CRC32, CRC32a, hsieh family, murmur family 和 Jenkins 算法

引入了新的代理服务器,需保障其高可用性.切换策略必须自行开发,该系统无法自动完成故障转移过程,且扩展能力有限.规模变化均需手动操作.

11.5 集群(直连型)_ Redis Cluster主从模式

自Redis 3.0版本起对Redis-Cluster的支持逐渐完善

Redis集群采用了master/slave模式以实现集群级别的高可用性和稳定性。该模式中每个主节点配置一组从节点作为其负载范围,并通过网络接口实时同步最新数据副本。作为数据存储的核心单元, master 节点负责本地文件系统中的持久化存储,而 slave 节点则通过网络接口实时同步最新数据副本,从而确保集群运行的安全性与可靠性。当发生故障时系统会自动切换至可用的备用 master 节点继续服务

以该实例为例,在该集群架构中存在三个关键主节点A、B、C。若这三个主节点均未加入从节点,则当其中一个主节点(例如B)发生故障失效时(即处于不可用状态),整个集群将因此而无法正常运行。此外,在这种情况下,A与C的槽位均将失去访问权限

因此,在构建集群时必须对每个主节点配置从机。例如这样配置:集群包含主节点A、B、C以及对应的从机A1、B1、C1。即便某个主节点失效(如B),系统仍能正常运行。

该系统将采用B1节点取代原有的B节点作为负载均衡后的主从分配方案;从而使得Redis集群的新主节点确定为该系统中的一个备用实例(如:R2)。这样不仅保证了服务的连续性,并且在原有配置基础上实现了负载均衡的效果;一旦系统中的某个模块(如:业务逻辑处理模块)重新启动,则该模块将被指定为该系统中另一个备用实例(如:R2)的从实例。

不过必须注意的是, 如果服务器节点B和B1同时崩溃了(故障终止),Redis集群将导致集群无法正常运行.

**redis****主从复制操作中包含了主节点与从节点之间的同步流程。

(1)从节点执行slaveofmasterIP,保存主节点信息

(2)从节点中的定时任务发现主节点信息,建立和主节点的socket连接

(3)从节点发送Ping信号,主节点返回Pong,两边能互相通信

(4)连接建立后,主节点将所有数据发送给从节点(数据同步)

主节点向从节点同步当前的数据之后, 实现了复制的建立过程. 随后, 主节点会不断地发送写命令给从节点, 以保证主从数据的一致性.

主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。

默认采用异步复制。

十二、如何用Redis实现分布式锁?

在实现分布式锁之前,我们先考虑如何实现,以及都要实现锁的哪些功能。

  1. 分散式特性(各分散于不同机器之上的实例皆可访问此把锁)
  2. 互斥性(任何时刻只有一个线程能拥有此把锁)
  3. 超时后自动释放之特性(持有所述之锁者需指定最长保持锁定之时段以避免长时间锁定导致之可能致死状况)

基于以上所列的分布式锁所需的核心属性与主要特性,在深入分析其功能需求的基础上 我们考虑如何在Redis中实现相应的机制

  1. Redis提供的第一个特性是支持分布式架构,在这种架构中单个Redis实例能够管理多个实例。
  2. 为了实现互斥性功能,在不使用其他工具的情况下可以通过Redis的setnx命令来达到目的。
  3. Redis具有的另一个重要功能是可以根据需要对某个键值对设置自定义的过期时间。
  4. 在完成任务后将分布式锁进行释放以释放资源。

Redis Setnx 命令:** 若无对应的键存在时,则将指定值赋给该键。此操作的成功与否将通过返回值来判断:若成功则返回1;若失败则返回0。

复制代码
 @RequestMapping("/stock_redis_lock")

    
 public String stock_redis_lock(){
    
     //底层使用setnx命令
    
     Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, "true");
    
     stringRedisTemplate.expire(lock_key,10, TimeUnit.SECONDS);//设置过期时间10秒
    
     if (!aTrue) {//设置失败则表示没有拿到分布式锁
    
     return "error";//这里可以给用户一个友好的提示
    
     }
    
     //获取当前库存
    
     int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
    
     if(stock>0){
    
     int afterStock = stock-1;
    
     stringRedisTemplate.opsForValue().set(key,afterStock+"");
    
     System.out.println("扣减库存成功,剩余库存"+afterStock);
    
     }else {
    
     System.out.println("扣减库存失败");
    
     }
    
     stringRedisTemplate.delete(lock_key);//执行完毕释放分布式锁
    
     return "ok";
    
 }

上面实现分布式锁的代码已经是一个较为成熟的分布式锁实现了,并非所有软件公司都能够满足需求

这段代码未预见到可能出现的情况。在实际情况中这段代码并非那么简单,在某些情况下可能存在许多复杂的操作可能导致错误的发生。因此,在释放锁的过程中必须采取措施以确保即使在出现错误的情况下也能正确处理。为此我们需要将释放锁的相关代码放置于try...catch finally块中以确保异常处理能够顺利完成。
我们的分布式锁获取和设置超时时间属于两步非原子操作。如果刚执行完第四行却未完成第五行则会发生如下情况:在发生故障前 lock 没有被设置超时值导致其他线程无法正确获取 lock 直到手动干预为止。这是一个重要的优化点因为Redis提供了一种更安全的方式来实现这一功能即通过SET命令实现带有超时值的安全更新。

该命令通过参数指定键值对的设置:键名设为 key、值设为 value;时间分辨率分别设为 EX(秒)、PX(毫秒);内存分配分别设为 NX 和 XX。

  • EX秒:使用 SET 命令将键值设为过期秒数。SET\ key\ value\ EX\ second 等价于 SETEX\ key\ second\ value
  • PX毫秒:使用 PSET 命令将键值设为过期时间毫秒。PSET\ key\ value\ PX\ millisecond 等价于 PSETEX\ key\ millisecond\ value

当指定的键不存在于数据库中时,NX 会对该键进行赋值操作。SET key value NX 的效果与 SETNX value key 相同。

XX :只在键已经存在时,才对键进行设置操作

SpringBoot的StringRedisTemplate也有对应的方法实现,如下代码:

复制代码
 @RequestMapping("/stock_redis_lock")

    
 public String stock_redis_lock() {try {
    
     //原子的设置key及超时时间
    
     Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, "true", 30, TimeUnit.SECONDS);
    
     if (!aTrue) {
    
         return "error";
    
     }
    
     int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
    
     if (stock > 0) {
    
         int afterStock = stock - 1;
    
         stringRedisTemplate.opsForValue().set(key, afterStock + "");
    
         System.out.println("扣减库存成功,剩余库存" + afterStock);
    
     } else {
    
         System.out.println("扣减库存失败");
    
     }
    
     } catch (NumberFormatException e) {
    
     e.printStackTrace();
    
     } finally {
    
     //释放锁
    
     stringRedisTemplate.delete(lock_key);
    
     }
    
     return "ok";
    
 }

这种实现方式是否达到了预期效果呢?对于低 concurrent 要求或非 high concurrent 的场景来说,这种实现方案已经足够满足需求了。

针对抢购类场景,在流量-intensive的情况下(即流量非常大的情况下),当服务器网卡、磁盘IO和CPU负载可能会达到极限状态(即资源极度紧张的状态),此时服务器对于一个请求的响应时间势必会比正常情况下慢很多(具体表现为响应速度明显降低)。假设所设置的锁超时时间为10秒,在这种情况下(即在等待超时发生之前),若某个线程获取到锁后因某些原因未能在10秒内完成相应的锁相关操作(即任务未及时解锁),则会导致其他线程抢占该分布式锁以执行其业务逻辑(即将等待解锁的机会让给其他操作)。待前一操作完成(即当前的操作已经结束并释放了资源),最终会触发finally块中的释放代码来释放当前占有的分布式锁(即将刚获取到资源的操作从系统中移除)。然而这意味着该当前持有锁的操作尚未完成(即资源仍然被占用的状态持续存在),因此就会导致其他线程有机会重新获取该分布式lock的机会不断出现(即多次竞争机会的存在)。从而导致其他线程有机会重新获取该分布式lock。这种情况下(即系统处于不断有其他竞争者争夺资源的状态下),整个分布式互斥机制便无法正常运转而产生预期之外的影响

所以这个问题总结如下:就是由于锁的过期时间设置不当或者某些原因使得代码执行时间超过锁的有效期进而导致并发问题以及当某个线程释放锁时可能会干扰到其他线程的操作从而造成分布式锁混乱的情况。简单来说就是存在两个主要原因:一方面代码执行时间未能满足锁的有效期要求另一方面可能存在其他线程干扰导致锁状态混乱。

  1. 自己的锁被别人释放
  2. 锁超时无法续时间

第一个问题 很好解决,在配置分布式锁机制时,在当前线程中生成唯一的UUID标识符,并将其值字段赋值为该唯一的UUID标识符。在finally块中检查当前锁的值字段与自己所赋值是否一致;如果一致,则执行删除操作。由于每个线程生成的独特UUID标识符特性,在执行过程中仅有本线程会检测到与自身所赋值匹配的情况

复制代码
 String uuid = UUID.randomUUID().toString();

    
 try {
    
     //原子的设置key及超时时间,锁唯一值
    
     Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key,uuid,30,TimeUnit.SECONDS);
    
     //...
    
 } finally {
    
     //是自己设置的锁再执行delete
    
     if(uuid.equals(stringRedisTemplate.opsForValue().get(lock_key))){
    
     stringRedisTemplate.delete(lock_key);//避免死锁
    
     }
    
 }

请问还有没有这个问题? 是的。 程序在finally块中检查当前锁定机制是否为自己所设定 如果是 则会移除锁定机制 并非完全独立的操作序列会导致潜在的问题 例如 如果刚完成判断服务处于已挂机状态 那么移除锁定机制的操作将无法执行 从而引发死锁现象 即使设置了超时时间 在未超时时间段仍然可能存在死锁问题

所以这里也是一个需要注意的地方,在保证原子操作的前提下,Redis支持执行Lua脚本的功能以确保操作的原子性。具体的实现细节不在本次讨论范围内。

第二个问 题怎么解决?

设置合理的超时时间至关重要, 既不能过高也不能过低, 必须先评估业务代码的执行效率, 例如可以设定在10秒至20秒之间的时间间隔作为默认值进行测试和调整。值得注意的是, 即使您的锁已设置了一个适当的超时时间值, 也有可能会遇到某些特定场景导致代码运行超出预期的时间限制的情况, 这就要求我们在实际应用中采取灵活的方法来应对可能出现的问题。解决办法就是适当延长锁的有效期, 这样可以在一定程度上平衡性能与可用性之间的关系。

大致思路是:每个主进程都会独立启动一个专门的子进程来负责管理与互斥相关的事务。每个主进程配上一个从进程用于监控和管理其上设置的分布式互斥 lock 的状态。定期监控主进程中设置的分布式互斥 lock 的存在性——如果发现该 lock 还是存在的,则意味着主进程尚未完成当前操作;此时需要将 lock 的超时时间进行相应地延长以确保后续操作能够及时完成;当 lock 已经不存在的时候,则表明当前主进程已经完成了相应的锁定操作并释放了资源;这个过程将被持续执行直至所有的相关操作都完成。

通常称为"看门狗机制"(Guardian Mechanism)的另一种称呼是"Look-and-Kill"(查看并kill)机制。这一机制本身的设计确实较为复杂,在实际应用中涉及的问题众多。不小心就会导致系统漏洞。即使代码简短就能轻易认为其实现难度较低的说法并不完全准确:如果缺乏相关的实践经验,则很可能遇到诸多挑战。例如上面已经说过的两个非原子的操作:P�

  1. 配置lock和设定过期时间属于非原子性的操作可能会引发deadlock.
    2.a在finally块里判断lock是否是自己配置过的,并且如果是的话再delete lock;这两步操作也不是atomically atomic的操作。
    b如果刚判断为true的服务挂掉后delete lock就不会被执行,则可能导致deadlock;即使lock被设置了 timeout period,在未到timeout period时也会导致deadlock.
    c这也是需要注意的地方;
    d为了确保整个过程是atomically atomic的操作,请参考Redis文档了解如何使用Lua脚本实现这一功能。

Redis支持运行Lua脚本以确保操作的原子性。因此,在实现这种机制时会遇到一些挑战。幸运的是,市场上的现有开源框架已经实现了这一功能,并称为Redisson框架。

Redis红锁

Red Lock 是redis提供的一种分布式锁的实现方案,或者说是一种算法。

旨在解决分布式环境中Redis实现分布式锁使用时的安全性问题。

在生产环境中通常采用主从+哨兵架构来部署Redis系统。当我们在使用Redis分布式锁进行主从切换时,由于当前状态下从节点尚未完成与主节点的lock信息同步

Redis官方正式发布了名为红锁的新机制以避免这种情况的发生。该机制主要解决了部分节点出现故障时不会影响锁的使用以及数据完整性的问题。该文还提出了一个解决方案以确保系统的可靠性和稳定性。

1. 红锁的实现原理

采用Redis的红锁机制时需进行集群部署,在官方建议配置下通常设置至少五个实例。客户端将依次向这五个实例申请加锁请求。当最终有超过一半的实例成功完成加锁请求时,则认为该加锁操作获得成功;反之则视为操作失败。

2. 红锁一定安全吗?

  • 如果client1获得了锁, 但此时业务逻辑处理耗时较长, 导致redis的锁在业务结束前已经过期, 此时client2也在此时拿到了同样的锁, 开始执行后续操作, 致使系统出现数据不一致性。
  • 除了GC机制外, 还有一种情况可能导致类似问题: 当多个Redis实例的时间发生了跳跃式变化时, 锁可能会提前失效, 这也会引发同样的抢锁现象。

红锁其实也并不能解决根本问题,只是降低问题发生的概率。

Redisson分布式锁的实现原理

  1. 首先Redisson会尝试进行加锁,加锁的原理也是使用类似Redis的setnx命令原子的加锁,加锁成功的话其内部会开启一个子线程
  2. 子线程主要负责监听,其实就是一个定时器,定时监听主线程是否还持有锁,持有则将锁的时间延时,否则结束线程
  3. 如果加锁失败则自旋不断尝试加锁
  4. 执行完代码主线程主动释放锁

那我们看一下使用后Redisson后的代码是什么样的。

①. 首先在pom.xml文件添加Redisson的maven坐标

复制代码
 <dependency>

    
     <groupId>org.redisson</groupId>
    
     <artifactId>redisson</artifactId>
    
     <version>3.12.5</version>
    
 </dependency>

②. 我们要拿到Redisson的这个对象,如下配置Bean

复制代码
 @SpringBootApplication

    
 public class RedisLockApplication {
    
     public static void main(String[] args) {
    
     SpringApplication.run(RedisLockApplication.class, args);
    
     }
    
     @Bean
    
     public Redisson redisson(){
    
     Config config = new Config();
    
     config.useSingleServer().setAddress("redis://localhost:6379")
    
             .setDatabase(0);
    
     return (Redisson) Redisson.create(config);
    
     }
    
 }

③. 然后我们获取Redisson的实例,使用其API进行加锁释放锁操作

复制代码
 //假设库存编号是00001

    
 private String key = "stock:00001";
    
 private String lock_key = "lock_key:00001";
    
 @Autowired
    
 private StringRedisTemplate stringRedisTemplate;
    
 /** * 使用Redisson实现分布式锁
    
  * @return
    
  */
    
 @RequestMapping("/stock_redisson_lock")
    
 public String stock_redisson_lock() {
    
     RLock redissonLock = redisson.getLock(lock_key);
    
     try {
    
     redissonLock.lock();
    
     int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
    
     if (stock > 0) {
    
         int afterStock = stock - 1;
    
         stringRedisTemplate.opsForValue().set(key, afterStock + "");
    
         System.out.println("扣减库存成功,剩余库存" + afterStock);
    
     } else {
    
         System.out.println("扣减库存失败");
    
     }
    
     } catch (NumberFormatException e) {
    
     e.printStackTrace();
    
     } finally {
    
     redissonLock.unlock();
    
     }
    
     return "ok";
    
 }

这个Redisson的分布式锁提供的API是不是非常的简单?

通过调用redisson.getLock方法获得R锁实例 redisson_lock;通常会返回Redisso lock实例对象。该 Redisso lock实例对象实现了对应的 Rlock 接口;其中 Rlock 接口继承自 JDK 中的 Concurrency 包中的 Lock 接口。

在使用Redisson加锁时,它也提供了很多API:

目前我们采用了最为基础的无参数锁方法,只需点击即可快速查看其源码。

通过深入分析可以看出,在底层架构中采用了一种基于Lua脚本机制的设计方案以确保事务的一致性和不可变性。具体而言,在数据存储层面采用了Redis hash结构来实现一种高效的加锁操作,并且在冲突处理机制上采用了可重入锁技术以进一步提升系统的容错能力。

该系统开发的分布式锁具备可重入和可等待功能;当尝试获取 lock 时,在一定时间段内若未成功,则返回 false;在上述代码中,redissonLock.lock()/操作会持续自旋直到成功加 lock;一直自旋并试图再次加 lock。

Redisson中的红锁:

在Redisson框架中通过实现红锁机制完成了对Redlock机制的支持.RedissonRedLock类作为对Redlock加法算法的具体实施保证了这一功能的有效性。该类不仅能够将多个独立存在的RLock对象组合成一个统一的红锁同时也允许来自不同Redison实例中的独立RLock对象进行关联。只有当一个红lock集合中的多数(即至少半数)成员完成成功加法操作时才算作一次成功的加法操作这种设计有效提升了分布式系统中的高可用性

复制代码
 @Test

    
 public void testRedLock() {
    
     Config config = new Config();
    
     config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    
     RedissonClient client1 = Redisson.create(config);
    
  
    
     RLock lock1 = client1.getLock("lock1");
    
     RLock lock2 = client1.getLock("lock2");
    
     RLock lock3 = client1.getLock("lock3");
    
     RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
    
  
    
     try {
    
     /** * 4.尝试获取锁
    
      * redLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS)
    
      * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
    
      * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
    
      */
    
     // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
    
     boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
    
     if (res) {
    
         //成功获得锁,在这里处理业务
    
         System.out.println("成功获取到锁...");
    
     }
    
     } catch (Exception e) {
    
     throw new RuntimeException("aquire lock fail");
    
     } finally {
    
     // 无论如何, 最后都要解锁
    
     redLock.unlock();
    
     }
    
 }

小结:

自己实现的redis分布式锁的话,需要特别注意四点:

  • 原子加锁即确保每个线程的操作都是独立且互不影响的。
  • 配置相应的超时参数可避免长时间挂起的情况。
  • 由谁执行加锁操作,则由谁负责执行相应的解锁操作。
  • 保证每个操作都是完整的、无 interleaving 的。

若采用现成的分布式锁框架Redisson,则需深入了解其常见API及其实现原理。此外,深入考察现有开源分布式锁框架,并选择最适合自身业务需求的那个。

十三、redisCluster集群扩容与收缩

扩容

首先提升主从节点的数量,并通过均分槽位的方式实现资源优化配置;同时增强其扩展能力以适应业务增长需求

redis集群扩容步骤:

(1)创建一对新的主从节点

(2)在某个节点上执行一次meet操作,在此过程中该集群中的所有redis节点都能识别并接受这两个指定的节点

(3)设置新增节点之间的主从关系

通过Redis Trib-rb工具将ip:port重定向以均分槽数(按提示继续操作)

以上四步完美地完成了集群的扩容。具体而言,则可以通过ruby工具或者其自带命令来实现这一目标。

方式一:ruby工具

1、创建新的节点

复制代码
 mkdir -p /opt/redis_{6390,6391}/{conf,logs,pid}

    
 mkdir -p /data/redis_{6390,6391}
    
 cd /opt/
    
 cp redis_6380/conf/redis_6380.conf redis_6390/conf/redis_6390.conf
    
 cp redis_6380/conf/redis_6380.conf redis_6391/conf/redis_6391.conf
    
 sed -i 's#6380#6390#g' redis_6390/conf/redis_6390.conf
    
 sed -i 's#6380#6391#g' redis_6391/conf/redis_6391.conf
    
 redis-server /opt/redis_6390/conf/redis_6390.conf
    
 redis-server /opt/redis_6391/conf/redis_6391.conf
    
 ps -ef|grep redis

2、将新的主节点加入集群

复制代码
 redis-trib.rb add-node 10.0.0.51:6390 10.0.0.51:6380

    
 #                                     将新的节点添加到6380所在集群

3、转移slot(重新分片)

复制代码
    redis-trib.rb reshard 10.0.0.51:6380

4、将从节点加入集群

复制代码
 redis-trib.rb add-node --slave --master-id 881a713dcd1569a2426a69038f76e9718884e227 10.0.0.516391 10.0.0.51:6380

    
 #                            添加从节点      主节点id

方式二:利用自带命令

1、创建新的节点

复制代码
 mkdir -p /opt/redis_{6390,6391}/{conf,logs,pid}

    
 mkdir -p /data/redis_{6390,6391}
    
 cd /opt/
    
 cp redis_6380/conf/redis_6380.conf redis_6390/conf/redis_6390.conf
    
 cp redis_6380/conf/redis_6380.conf redis_6391/conf/redis_6391.conf
    
 sed -i 's#6380#6390#g' redis_6390/conf/redis_6390.conf
    
 sed -i 's#6380#6391#g' redis_6391/conf/redis_6391.conf
    
 redis-server /opt/redis_6390/conf/redis_6390.conf
    
 redis-server /opt/redis_6391/conf/redis_6391.conf
    
 ps -ef|grep redis

2、将新添加的节点加入到集群

复制代码
 redis-cli -c -h 10.0.0.51 -p 6380 cluster meet 10.0.0.51 6390

    
 redis-cli -c -h 10.0.0.51 -p 6380 cluster meet 10.0.0.51 6391
    
 redis-cli -c -h 10.0.0.51 -p 6380 cluster nodes

3、扩容(重新分配槽位)

复制代码
    redis-cli --cluster reshard 10.0.0.51:6380

4、手动添加复制关系(主从关系)

缩容

redis缩容步骤:

(1)迁移下线master上的槽 使用redis-trib.rb reshard命令

为了使系统达到稳定状态,在Redis Tributary模式下操作时,请确保以下步骤:首先使系统中所有非主控制节点遗忘当前活跃的主控制节点;接着逐步使其遗忘主控制节点;最后执行$redis-trib del-node命令以完成删除操作。这样可以避免数据丢失的问题并确保系统的稳定性得到维护。

移除节点将需要每个节点自行触发删除命令;然而Redis Trib提供了del-node功能来实现整个系统的管理。

当存在12个槽位时,在移除一个 master 节点之前已有四个 master 节点均匀分配这些槽位(每个 slot 分配 3 个);当移除一个 master 节点时,则需将剩余的一个 slot 重新分配给仍存在的其他 master 节点。计算方法如下:被移除的 master 节点所拥有的总 slot 数量除以当前剩余 master 的数量

十四、Redis分区

将数据被拆分为多个Redis实例中的各个部分,并且每一个实例都分配了所有key的一个子键集合。

好处:

  • 1、性能得到显著提升。单机Redis在处理网络输入/输出和计算资源方面的能力是有限制的。通过将请求分布至多台服务器,并通过实施分布式架构以最大限度地释放各服务器计算能力的同时充分利用带宽。这种优化策略有助于整体提升Redis的服务效能。
  • 2、数据分布策略。即便当前Redis的能力足以满足应用需求。然而,在实际应用中当数据量持续增长时这一限制就显得尤为明显。因此有必要通过实施分布式存储策略使得系统在面对大规模数据时仍能保持良好的扩展性。

分区帮助我们突破了原本受限于单台计算机硬件资源的难题;存储不足、计算能力不足以及带宽受限等问题均可以通过增加更多的机器来解决。这些问题都可以借助增加更多机器的方式来有效应对。

坏处:

  • 1、无法进行多键操作,例如这些操作会被分配到不同的Redis实例。
  • 2、具有多个键值对的操作也是不被支持的。
  • 3、最低粒度为单个键,因此我们无法将关联一个键的大规模数据分散存储在多个实例中。
  • 4、当应用分区策略时会面临较高的复杂性,例如需要整合分布在不同节点上的多个rdb或aof文件并进行集中备份。
  • 5、维护服务器群组的过程非常复杂,虽然Redis集群可以在运行时自动优化资源分配以适应增减服务器的需求但客户端及代理分区类型则不具备这一功能。

实现方式

实现方式-1、客户端实现

即key在redis客户端就决定了要被存储在那台Redis实例中。

实现方式-2、代理实现

在代理机制中,客户端会将请求发送至代理服务器,并且该代理服务器实现了与Redis协议的兼容性。因此,在这种配置下,系统能够实现客户端与Redis服务器之间的通信。通过预先配置的分区策略(如schema分区),该系统能够根据请求类型自动分配至合适的Redis实例,并将反馈消息返回给客户端以完成整个通信流程。

实现方式-3、查询路由

查询路由是Redis Cluster实现的一种Redis分区方式。

补充 - 1:redis的通信协议

Redis通信协议被称为Redis Serialization Protocol(简称RESP),具有以下特性

  • 是二进制安全的
  • 使用TCP
  • 基于请求-响应的模式

需要用注意的是:RESP用于Redis客户端与服务端之间的通信协议 节点交互不采用RESP协议

补充 - 2:什么是缓存穿透?什么是缓存雪崩?何如避免? Redis 的热 key 问题怎么解决?

1、缓存穿透:

大多数缓存系统都是通过键来存储和查询数据。当没有相应的值时,则需要向后端系统求助(例如数据库)。恶意请求常常会故意访问不存在的数据键,并在短时间内发送大量请求,从而给后端带来巨大的负担。这种现象被称为缓存穿透。这些高频率的数据访问往往由攻击者发起,并会给数据库带来极大的压力。

如何避免?

  1. 将对查询结果为空的情况实施缓存操作,并设定合理的缓存时间参数;当该key对应的原始数据插入成功后,则应立即删除相关缓存项以释放资源。
  2. 在过滤机制中加入针对特定不存在键值对的筛选条件;可以通过构建一个包含所有可能键值对的大规模掩码表来辅助判断这些键值是否存在。
  3. 在接口层增加相应的验证措施;包括但不限于权限验证、身份认证、基础信息校验(如ID合法性检查),其中ID值小于等于零时应立即拦截请求。
  4. 当从缓存中无法获取所需数据且数据库同样也无法提供该数据时,则可以在数据库返回null值的同时记录事件信息;此时可将该key-value对标记为无效项,并设定较短的有效期(如30秒),以便及时清理掉无意义的数据。

2、缓存雪崩

在缓存服务器发生重启或出现大量缓存堆积在一个时间段时,在失效的过程中(即失效发生时),这将给后端系统带来显著的压力(即带来较大的压力),可能导致系统崩溃(即出现崩溃现象)。

产生雪崩的原因之一是即将临近双十二24点时分, 系统将会发起一波促销活动, 这些促销商品会被暂时存储在缓存中, 并假设其保质期为一小时。当凌晨一时整到来时, 这些存储的商品信息将在一小时后失效, 而这些促销活动的商品访问与查询操作将被导向数据库系统中

在同一个分类下的商品上加入一个随机因素。这样能够尽可能地分散缓存失效的时间,并且,在针对热门类别下的商品则会有较长的缓存有效期。相应地,在冷门类别下的商品则会有较短的缓存期限。从而能够有效地节约缓存服务器所需的人力和计算资源

如何避免?

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  2. 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
  3. 不同的key,缓存失效时间加入随机因子, 尽量让失效时间点均匀分布,设置不同的过期时间。

**3、**热 key 问题

在高负载场景下,当大量并发请求涌入某个特定的 key 时(即 hot key 情况),系统处理能力跟不上会导致该 Redis 服务器出现服务中断。

解决方案:

  • 结果可以被缓存至本地内存中。
    • 热 key 可以被分布至不同的服务器上。
    • 结果应被配置为永不失效。

补充 - 3:热点数据 和 冷数据 是什么

高频访问的数据节点:是指那些被频繁计算和使用的在线类型的数据资源。
离线性质且不频繁使用的数据:是指那些不具备高频访问特性的离线类资源集合。例如企业备份资源、业务操作日志集合、通话记录以及统计数据等信息存储载体。

将热数据进行本地化计算和部署管理,在线处理;而将冷数据集中存储至远程服务器,在线查询。由于热数据中心类别的访问频率较高,在线处理时对响应速度有较高要求从而实现本地化计算与部署策略。对于低频访问的数据类别可以在远程服务器上进行集中式部署管理并在由大规模分布式存储系统构成的环境中通过压缩、去重等手段降低运营成本

通过数据分析的角度来看,在这一层面不仅有冷热两种数据以及温数据的存在,并且还有其他类型的数据显示出来。而提出这一概念的是某种方法或模型,并非一个简单的术语。该方法/模型是如何介绍这一概念的:

  1. 静默的数据特征——基于性别维度的人口统计信息(如人口普查)、兴趣偏好(如用户调研)、地理分布(如位置服务)、职业领域(如企业名录数据库)以及年龄层次(如人口统计数据库)等多维度属性信息的集合体……表征"这个群体的基本属性特征";
  2. 时序的数据特征——记录了用户在过去一段时间内所使用的各类应用程序列表(如手机应用行为分析)、活动场所分布情况(如位置服务记录)以及浏览历史(如网页浏览记录)等具有时间背景的信息集合体……表征"最近的行为动向趋势";
  3. 即时的行为特征——包括当前访问的地点信息(如位置服务记录)、打开的应用程序列表(如应用启动记录)以及操作行为的时间序列信息(如点击频率统计)等显式或隐式的场景化属性指标……表征"实时的行为动向状态"。

为了实现冷热数据识别与交换的技术需求,阿里云推出了其自主研发的 Redis 混合存储产品(见链接:https://promotion.aliyun.com/ntms/act/hybridstore.html?spm=5176.54432.728894.1.2bc328f5sYt37q)。该产品实现了完全兼容 Redis 协议和特性,并且在功能上形成了独特的创新。通过将部分冷数据存放在磁盘上,在保证大部分访问性能不受影响的同时,显著降低了用户的成本,并突破了 Redis 单实例对内存占用的限制。

Redis混合存储实例将每个Key都被视为热数据,并仅消耗微乎其微的内存来确保所有Key访问请求的同时具有高效的性能和一致性的响应速度。对于Value字段而言,则在内存空间受限的情况下采用基于最近被访问的时间、频繁程度以及字段大小等因素进行筛选的方式将一部分Value字段作为冷存放在磁盘上进行同步备份处理直至系统可用空间未达到预先设定的标准。

在Redis混合存储实例中,我们假设所有的Key都是热数据并将其存储在内存中,基于以下两个原因

1、Key的访问频度比Value要高很多。

作为KV型数据库的一种实现方式,在常规访问请求中首先需要通过查找键来确定该键是否已存在。为了确定某个键不存在于系统中,则必须对整个键集合进行特定形式的检查操作。如果将所有的键存储在内存中,则能够确保键的存在性查询速度与完全采用内存缓存版本一致。

2、Key的大小占比很低。

即便如此,在通常的业务模型中,Value相较于Key会高出若干倍。尤其对于Set、List以及Hash等集合体而言,在这些数据结构中由多个成员所构成的Value相比单一的Key,则会高出多个数量级。

因此,Redis混合存储实例的适用场景主要有以下两种:

  1. 数据访问呈现分布不均的特点,并存在高频数据;
  2. 内存空间有限,在存储全部数据时遇到瓶颈;其中每个记录的数值规模远大于其对应的键值。

冷热数据识别:

在内存资源耗尽的情况下,在线实例会根据最近访问时间、访问频率以及value大小等因素综合评估后确定各value对应的权重值,并将权重最小的value存储至磁盘并将其从内存中移除。

补充 - 4:为什么Redis是单线程的,优势

Redis是以内存为基础的操作。 Redis的运行并未受到CPU的限制。 Redis的主要瓶颈通常是机器内存容量或网络带宽的问题。 基于现有资源配置和性能需求分析,单线程方案能够有效满足当前处理需求。

具体的原因:

1)不需要各种锁的性能消耗

对于单线程场景而言,在处理锁相关问题时可以完全忽略

2)CPU消耗

选择单线程模式以减少不必要的上下文切换次数以及竞争性条件的发生风险;同时确保系统运行过程中同样没有多进程或多线程造成的切换行为而导致CPU资源被占用。

Redis单线程的优劣势

1.单进程单线程优势

  • 代码更加整洁易懂。
  • 在本系统中无需担心任何类型的锁相关问题。
  • 本系统完全避免了由于多进程或多线程转换而导致的CPU耗能问题。

2.单进程单线程弊端

  • 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;

以上也是Redis能够支持高并发的原因。

补充 - 5:如何解决redis的并发竞争key问题?

这个问题大致就是,同时有多个子系统去set一个key。

方案:

(1) 如果对这个 key 操作不作顺序要求
在这种情况下,请建立一个分布式互斥机制。各节点会竞争获取互斥资源,并一旦获得该资源,则执行设置操作即可实现目标。
(2) 如果对这个 key 操作要求严格遵循特定顺序
针对同一个 key 1,请注意以下情况:系统 A 作为事务提交者需将其设置为 valueA;接着由系统 B 负责将其设置为 valueB;最后由系统 C 执行设置为 valueC 的操作。

期望按照key1.value依次经历valueA、valueB、valueC三个阶段的变化。当这种情况发生时,在向数据库中写入数据的过程中,请确保为每个记录生成一个唯一的时间戳。假设我们为每个记录生成的时间戳格式如下:

复制代码
 系统A key 1 {valueA  3:00}

    
 系统B key 1 {valueB  3:05}
    
 系统C key 1 {valueC  3:10}

那么,在这种情况下,在系统B先抢到锁的情况下,“将key1设置为{valueB 3:05}”。随后系统A也抢到锁,“随后发现自己的valueA的时间戳比缓存中记录的时间戳更早,则无需进行设置操作”。依此类推,“依次类推”。

其他方法,比如利用队列,将set方法变成串行访问也可以。

补充 - 6:如何保证Redis与数据库的数据一致性?

通常情况下,在采用了缓存机制的情况下(无论是哪种缓存技术),都可能引发数据库缓存一致性问题。

首先需要仔细斟酌的是到底是要执行数据库更新还是要更新缓存更为合适?这个问题至关重要,在目前的环境下进行缓存更新往往是一种不合算的投资。其主要原因在于当前环境下更新缓存往往伴随着额外的开销。

  • 在大多数情况下,默认情况下,
    Redis缓存中的数据并非完全复制自数据库(DB)中的对应数据,
    而是会对DB中的多张表执行数据重新计算操作,
    筛选后将结果更新至Redis缓存系统。
    当DB中某一张表的数据发生变化时,
    同步更新Redis中的相关值会导致较高的计算成本。

缓存更新后的数据可能不会立即被读请求命中。
如果一段时间内没有读请求命中该部分冷数据,
这会带来一定的资源浪费(包括计算资源和存储空间上的浪费)。

因此,在得出这一结论后发现:在具备可删除条件时应优先选择删除缓存数据,在成本高且难以完全清除缓存的情况下最好避免更新缓存。

那么删除缓存和更新DB谁先谁后呢?首先想到的可能就是:

在执行更新操作时,在完成数据库重置后清除缓存。
在读取数据时,在检查是否存在于缓存中之后:

  • 如果不存在于缓存中,则从数据库中查询;
  • 查询结果后将其存储到缓存中并立即返回响应。

但这样会带来一个问题:如果首先更新了数据库,在随后进行的删除缓存操作时遇到失败怎么办?这意味着 databases 中存储的是新 data 而 cache 中存放的是 old data 这会导致两者之间出现不一致性 如果我们采取相反的方式 先清除 cache 再进行 database 更新 那么即使后续 update 操作未能成功 因为 cache 已经清空 当程序试图从 database 重新获取 data 时 尽管获取到的数据均为 old data 但由于两者均为同一时间点的数据 所以并不会导致 inconsistency

  1. 在执行更新操作时,在确保系统处于干净状态的基础上进行操作。
  2. 在读取操作中:
    • 如果存在缓存数据,则优先使用缓存结果;
    • 如果不存在缓存数据,则从数据库中读取数据,并将其存储到缓存中后返回响应。

到这里为止,并非问题已经彻底解决。实际上,并非如此,在高并发情况下会出现类似状况:当数据发生变更时,首先会执行缓存清除操作;随后转向数据库的修改操作。在此时尚未完成相应的修改操作时,一个请求便到来-read cache-发现当前缓存为空-转而读取数据库获取到最新未修改前的数据,并将之前的数据暂时存储在缓存中;待数据变更程序完成数据库更新后发现当前缓存仍为空-此时系统发现数据不一致

所以有了下面几种方案:

(1)串行队列方案:

将"修改数据库"的任务安排至一个特定的JVM队列中,在接收新请求后,则同步执行"更新缓存"的任务。每个作业线程会按照指定的顺序依次执行其所属的队列中的所有任务。这样设计确保了所有"修改数据库"操作都会在其对应的"更新缓存"操作之前完成:当然这个方案还可以优化:

  1. 当读请求频率过高时,在队列中会出现多个"更新缓存"操作连续排列的现象。这种情况下是没有实际意义的操作。
    在将数据加入队列之前进行检查是必要的步骤。
  2. 在处理频繁更新数据库的业务场景时,
    会导致大量读请求长时间被阻塞。
    此时可以通过部署更多服务器来提升吞吐量,
    也可以选择返回上一次的结果值以避免阻塞。

(2) 延时双删策略**:**

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。伪代码:

复制代码
 public void write( String key, Object data )

    
 {
    
     redis.delKey( key );
    
     db.updateData( data );
    
     Thread.sleep(  );
    
     redis.delKey( key );
    
 }

步骤:

  1. 先删除缓存
  2. 再写数据库
  3. 休眠500毫秒
  4. 再次删除缓存

这个休眠时间等于读取业务逻辑数据所需的时间加上大约几百毫秒。为了避免潜在的问题,在处理新请求时应当清除该读请求可能导致的缓存不一致(脏)的数据

实际过程:

  • 请求A先对缓存进行清除后执行write operation.
  • 在读取数据之前完成相应的处理。
  • 因为A request已经完成了cache clearing.
  • 该操作所需时间为N seconds.
  • 因此,在这段期间内Redis仍保有旧的数据.
  • 经过短暂休眠(约M seconds)后再次执行cache clearing operation.
  • 这种方案虽然解决了data inconsistency的问题, 但是由于A request在完成数据库更新之后, 必须等待一段时间才能清除Redis cache, 这会影响 throughput.
  • 可以考虑将其拆分为独立线程以优化吞吐量, 比如使用消息队列 + retry mechanism 来管理相关的更新逻辑.

(3)异步更新缓存(基于订阅binlog的同步机制)

MySQL binlog 采用增量式订阅机制来处理数据流,并结合消息队列系统进行数据传输与管理;通过这种方式实现增量数据同步至Redis

流程:

  1. 向数据库提交更新数据指令;
  2. 系统会将操作记录写入binlog日志;
  3. 订阅程序负责提取所需数据和密钥;
  4. 单独编写非业务逻辑代码以获取相关信息;
  5. 尝试从缓存中删除该操作发现无法成功删除缓存项;
  6. 将相关信息传输至消息队列中;
  7. 再次从消息队列中读取该数据进行重试操作;

当MySQL执行新增加记录或修改现有数据等操作时

总结:

通常情况下,在系统不需要严格保证缓存与数据库完全一致的情况下,缓存可以在一定程度上与数据库偶尔存在一定程度的一致性差异。通过同步机制实现的数据更新操作能够有效维持数据一致性。然而,在这种机制下系统的吞吐量可能会显著下降。因此可能需要部署比常规情况多出一定比例的数量级来维持系统的稳定运行。

补充 - 7:redis为什么这么快?

官方统计数据显示 Redis 实现了每秒近10万的吞吐量。主要原因归因于以下几个方面:

  • 1: 完全遵循内存操作机制
  • 2: 采用了单线程模型来处理客户端请求,并从而减少了上下文切换的情况
  • 3: 实现了IO多路复用功能
  • 4: 系统采用C语言实现,并具备多种优化手段以提升性能……其中包含动态字符串处理功能。

补充 - 8:听说 redis 6.0之后又使用了多线程,不会有线程安全的问题吗?

不会

实际上 Redis 仍然采用了单线程架构来管理客户端的请求流程。然而,在数据读写与协议解析方面,则采用了多线 thread 技术。在实际操作中,每次执行命令时仍采用的是单一的 line 线程。这样一来就无需担心潜在的 threading security issues。

为了提高系统的响应速度和吞吐量,在Redis中发现其性能瓶颈主要体现在网络I/O操作上而非CPU资源利用率方面。通过采用多线程结构来优化网络I/O操作效率,并最终显著提升了Redis的整体性能水平。

补充 - 9: cluster集群模式是怎么存放数据的?

该集群由16384个节点组成,并将均匀地分派这16384个资源给每一个主节点。值得注意的是,在本上下文中,默认情况下所指的"节点"特指主从架构下的"主节点"角色实例。

补充 - 10: cluster的故障恢复是怎么做的?

其本质与其哨兵模式存在显著相似之处,在集群环境中运行时 每个节点会定期向其他节点发射ping命令 并根据是否接收到响应来判断是否有其他节点已处于在线状态

若长时间未收到响应,则发起ping命令的节点会判定目标节点处于疑似下线状态。同样可被视为一种主观下的判断方式但必须要求集群中的多数或全部节点一致确认该节点已处于下线状态才能判定其确实已下线

  • 当A检测到B可能已退出时, A会转发此信息给集群中的其它各节点。
  • 这些接收者随后会收到此通知并检查B的状态。
  • 如果上述过程持续一段时间而未得到回应, 集群将认为B已退出。
    • 如果超过半数成员确认了这一异常状态, 系统将标记B为目标已退出状态。
    • 这一操作将指示所有正常成员采取相应措施, 以便在必要时恢复集群功能。

补充 - 11: 主从同步原理是怎样的?

  • 一旦一个连接至数据库时, 它就会触发主数据库执行 SYNC 命令, master接收到该命令后会在后台生成快照, 我们将其称为 RDB 持久化过程, 而这一操作本身需要耗费时间资源, 同时由于 Redis 是单线程模式, 在执行此操作期间任何来自 Redis 的新请求都会被暂时缓存。
  • 完成生成后, 系统会将缓存的所有命令以及当前生成的快照打包并传递给从节点, 这一机制确保了主从数据的一致性得到有效维护。
  • 接收完快照信息及相关缓存指令后, 系统会将这些数据被写入至硬盘上的临时文件中, 当前过程结束后会替代原有 RDB 快照文件。需要注意的是, 这一操作并不会导致阻塞现象出现, 因此可以在同步过程中继续接收新请求处理它们。具体原因在于系统在此处调用了 fork 子进程机制, 让子进程负责完成这些同步任务。

由于没有阻塞,在这部分初始化完成后,在执行修改数据库数据的命令时(即触发复制同步环节),系统会将信息异步地向slave节点发送。这一过程贯穿于整个主从同步的过程中,在主从同步完成之后(即复制完成),复制同步才会终止。

补充 - 12: 无硬盘复制是什么?

主从之间采用RDB快照进行交互。尽管看似逻辑上比较简单但仍会遇到一些问题

  • 当 master 不再启用 RDB 快照功能时,在执行复制初始化操作后也会自动创建 RDB 快照文件。然而,在 master 发生重启的情况下,默认会通过 RDB 快照文件来恢复数据源的数据。需要注意的是,在此过程中可能会出现已有 RDB 文件被覆盖的情况。
  • 在这种一主多从架构下,在每次 master 节点与 slave 节点端口同步时都需要创建一次 RDB 文件以实现快速的数据备份。这种机制虽然有效但会导致在硬盘上生成多个 RDB 文件以供后续使用。

为了应对当前的问题,Redis在后续版本中增加了无磁盘复制的功能。值得注意的是,在这一改进下可以直接通过网络发送给 slave 节点,并且省去了与磁盘的交互步骤;然而这也会带来一定的IO开销。

补充 - 13: redis的key的过期时间和永久有效怎么设置?

EXPIRE 和 PERSIST 命令。

补充 - 14: redis集群最大节点个数?Hash槽是什么概念?

16384个。其中,主节点数量基本不可能超过1000个

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value

时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,

每个 key 都会分配一个编号,在 0 到 16383 之间的哈希槽中存放数据,并且 Redis 根据节点数量动态分配存储空间以满足数据库需求

致均等的将哈希槽映射到不同的节点。

好处:

  • 哈希槽的优点就是容易地添加或删除节点。
  • 在增加的时候,只需要将其他节点的一些哈希槽转移至新节点就可以。
  • 在移除的时候,在移除该节点后将其哈希槽转移至其他适当位置即可。

Redis未采用一致性的哈希算法而采用了哈希槽的概念;同时未采用一致性的哈希算法;难道不是都基于哈希吗?那么采取这种设计的原因是什么?

Redis的作者认为其采用crc16(key) mod 16384算法的表现已经相当出色了。尽管在灵活性方面略逊于一致性哈希方案,在实现上相对简单,并且其节点增删操作处理起来也非常便捷。一致性的哈希算法将空间抽象为一个环形结构,并遵循环形结构安排节点分布。然而Redis Cluster的空间划分具有高度定制性,并且这种分区划分具有高度定制性,并且类似于Windows操作系统中的磁盘分区概念。此外,在数据分布上也存在严重的倾斜现象

补充 - 15: Redis的管道机制是啥?

pipeline出现的背景:

redis客户端执行一条命令分4个过程:

客户端发送命令-〉命令排队-〉命令执行-〉返回结果到客户端

这个过程被称为往返时间(缩写为RTT),在计算机网络领域具有重要意义。它指的是从发送端发射数据开始到发送方从接收方得到确认(此时接收方收到数据后立即返回确认)总共经历的时间延迟。传统的请求模型不具备处理批量操作的能力,每次处理都需要消耗N次RTT,在这种情况下就需要引入流水线技术来解决这一问题。

Redis引入了管道机制来处理一组命令。其命令通过一次RTT发送到Redis,并按顺序返回结果。例如,在使用管道时连续执行n条命令时,整个流程只需一次RTT即可完成。

底层避免了用户态切换到内核态。

使用举例:

该框架通过提供executePipelined方法来实现管道的支持。 作为一个位于管道中的Redis队列操作实例,在执行时会将其处理。

复制代码
 @RunWith(SpringRunner.class)

    
 @SpringBootTest
    
 @Slf4j
    
 public class RedisPipeliningTests {
    
  
    
     @Autowired
    
     private RedisTemplate<String, String> redisTemplate;
    
     private static final String RLIST = "test_redis_list";
    
  
    
     @Test
    
     public void test() {
    
       Instant beginTime2 = Instant.now();
    
  
    
       redisTemplate.executePipelined(new RedisCallback<Object>() {
    
       @Override
    
       public Object doInRedis(RedisConnection connection) throws DataAccessException {
    
           for (int i = 0; i < (10 * 10000); i++) {
    
               connection.lPush(RLIST.getBytes(), (i + "").getBytes());
    
           }
    
           for (int i = 0; i < (10 * 10000); i++) {
    
               connection.rPop(RLIST.getBytes());
    
           }
    
           return null;
    
       }
    
       });
    
       log.info(" ***************** pipeling time duration : {}", Duration.between(beginTime2, Instant.now()).getSeconds());
    
   }
    
 }

注意executePipelined中的doInRedis方法返回总为null

使用管道技术的注意事项

当你要进行大量Redis请求时,请问您是否考虑过采用管道技术?因为这将有助于以显著提升性能水平为目标,并且能够有效地减少网络延迟。

当管道承受过量的请求流量时

在处理队列监听的情景中,在检测到队列返回的数据为空时(一种常见的策略),会将线程暂停运行一段时间(让线程休眠),直到数据量达到一定程度后切换到管道机制获取数据(通过管道去取)。这种策略既保留了管道带来的高效性能(高性能),又能有效降低CPU利用率(避免了高负载风险)。

补充 - 16: redis如何做大量数据插入?

在短时间内, Redis 实例需要处理由大量用户产生的数据. 成百万规模的 key 需迅速生成. 即进行大规模的数据注入(Mass Insertion).

在常规Redis客户端中进行大规模数据插入请求显然是低效的做法:因为逐个处理每个命令都需要等待返回响应的时间会累积起来。

采用管道(pipelining)相当可靠。当在处理大量数据时必须同时运行其他新指令时,在读取数据的过程中必须确保尽可能快速地将它们写入存储。

从Redis 2.6版本起,redis - cli工具开始提供一种名为pipe mode的新模式,用于进行大量数据的插入操作。

使用pipe mode 模式的执行命令如下:

复制代码
    cat data.txt | redis-cli --pipe

1、未使用pipeline执行N条命令

2、使用了pipeline执行N条命令

采用Pipeline进行处理时的速度相比逐条运行更高;特别是在客户端与服务器之间的网络延迟增大时,系统的性能表现会更加显著。

补充 - 17 、redis从海量的key里面查询出某一固定前缀的key

语法
scan cursor [MATCH pattern] [COUNT count]

  • 遵循基于前一游标的迭代器遵循前一状态以继承前一状态。
  • 从初始状态出发进行新一轮迭代直至命令返回初始状态完成一轮遍历。
  • 不可能预知每次执行会返回多少元素同时支持模糊查询。
  • 无法精确预测结果数量但通常会满足count参数的需求。
复制代码
 127.0.0.1:6380> scan 0 match k1* count 10

    
 1) "655360"
    
 2) 1) "k1864385"
    
    2) "k1392840"
    
    3) "k1388130"
    
    4) "k1357007"
    
    5) "k1743332"
    
    6) "k1593973"
    
    7) "k1399047"
    
 127.0.0.1:6380> scan 655360 match k1* count 10
    
 1) "327680"
    
 2) 1) "k1610178"
    
    2) "k1693505"
    
    3) "k1032175"
    
    4) "k1721788"
    
    5) "k1678140"
    
    6) "k1359412"
    
 127.0.0.1:6380> scan 327680 match k1* count 10
    
 1) "2031616"
    
 2) 1) "k1798037"
    
    2) "k1805785"
    
    3) "k1837836"
    
    4) "k1138914"
    
    5) "k1689917"
    
    6) "k1033258"

使用SCAN命令从游标0的位置开始扫描匹配所有以aaa开头的键,并设置每次最多返回5条记录。需要注意的是,这个最大值并非固定不变,因为具体的返回数量取决于Redis当前的状态。

补充 - 18redis如何实现异步队列?

异步队列是一种设计模式...用于处理无需立即响应但需可靠执行的任务...例如发送邮件...生成报告...进行日志处理等操作...通常需要一定的时间才能完成...但不应干扰主流程运行...异步队列通过将这些耗时任务编入一个队列中让后台工作者进程依次处理从而防止用户因等待任务而影响系统响应速度

Redis 的 LIST 数据结构可以直接支持 队列的实现。它们的基本操作包括 对应的命令应用。例如,在一个任务调度系统中:

  • 生产者 :生产者会将任务压入队列的前端,并执行LPUSH操作。
    • 消费者 :消费者会从队列的后端取出任务,并执行RPOP操作。

例如,以下是生产者插入任务的命令:

复制代码
 LPUSH task_queue "task1"

    
 LPUSH task_queue "task2"

消费者取出任务的命令为:

复制代码
    RPOP task_queue

生产者与消费者分别在不同时间段执行操作,在队列管理中采用先进先出原则。

Redis 作为异步队列的应用场景
1 消息通知系统
在消息通知系统中,消息的发布通常采用异步机制。如用户在注册时会收到自动发送的欢迎邮件,在订单创建完成后也会收到相应的确认短信等操作性信息。这些耗时的任务都会被系统整合到Redis队列中进行处理。当用户触发这些操作时,Redis会将任务插入到队列中等待处理,在线 consumers则会从队列中接收任务并由相关服务接收并执行通知任务以完成相应的操作流程。这种设计不仅能够加快用户的实际操作速度,在后台则负责处理耗时的通知发布工作从而保证了不会影响用户体验的情况发生

2 订单处理系统
在电商平台上,订单创建与支付处理是其中最为重要的环节。为了缩短业务流程处理时间,在提升系统响应效率的同时可考虑将涉及订单库存检查与支付确认等核心业务流程的操作任务委托至Redis异步队列中进行批量处理。通过这一机制不仅能够有效缩短从发起到完成整个业务流程的时间而且还能让复杂的业务逻辑由后端服务器按需启动并执行相关操作从而实现从发起到完成整个业务流程的时间压缩

3 日志收集与分析
在大型应用环境中,日志收集通常会对系统性能产生一定影响。Redis异步队列能够实现日志事件的批量写入到队列中,借助专业的日志处理服务在非主流程任务处理下进行数据解析和持久化存储,从而有效降低对系统运行效率的影响。这种技术方案逐渐成为监控中心、审计部门等相关领域的标准方案。

Redis 异步队列的挑战和解决方案:

Redis作为一个内存式数据库,在其实例重启或崩溃的情况下可能导致数据丢失风险。因此,在使用Redis作为异步队列时需关注任务的持久性问题。可以通过启用Redis的AOF(Append-Only-File)持久化模式来有效降低数据丢失风险;然而此做法将带来一定的性能代价

2 消费确认与重复消费

因为网络故障或消费者进程崩溃的原因可能导致的任务重传现象

3 队列积压问题

在高负载状态下(即当生产者的工作流生成速率显著高于消费者处理速率)时

Redis提供多种数据存储方案,在实际应用中推荐采用Redis列表(List)类型作为消息队列,并通过rpush方法实现消息的发布。当通过lpop方法实现消息的消费时,在未获取到有效消息的情况下建议适当延长睡眠时间后进行重新尝试以避免长时间空闲状态。

Redis 异步队列的最佳实践

在Redis被用作异步队列的时候,在配置Redis之前就应当为每个操作设定相应的执行超时参数。为了提高系统的可靠性,在每次操作完成后都应检查Redis连接状态并及时重连。此外,在应用开发中应当尽量避免直接操作Redis数据库对象以提高程序健壮性。

采用独特的标识符对各项任务进行追踪 为确保在任何情况下都能有效识别和管理各项活动 每个项目都应当被赋予一个独一无二的编号 这种做法不仅有助于快速定位问题所在 而且对于维护系统的稳定运行至关重要

3 监控与告警
重要的是监控和告警Redis异步队列的使用情况。通过监控队列长度、消费者处理的任务数量以及失败率等指标来及时发现潜在的问题。Redis支持通过INFO命令获取队列详细信息。

为了确保操作的原子性而采用 Lua 脚本,在任务处理过程中可能会频繁地进行Redis数据的读取与更新操作。以确保操作的原子性为前提,可以通过Lua脚本将多个操作整合到一起。

小结:

Redis 作为一种高效率内存数据库,在实现异步队列场景中具有广泛的应用。基于列表(LIST)数据结构及其丰富的操作命令,Redis 轻松实现生产者-消费者模式,能够高效处理消息通知、订单处理、日志分析等多样化的异步任务。然而,在使用 Redis 实现异步队列时会面临一些挑战性问题:如数据丢失、任务重复消费等问题。这些挑战可通过设定合理的持久化策略、使用唯一标识符以及引入监控和告警系统等方式来解决这些问题。本文旨在帮助您掌握如何利用 Redis 实现高效可靠的异步队列系统,并通过提升吞吐量和可靠性来增强整体性能。

补充 - 19 、redis如何实现延时队列?

延迟队列的使用场景

1、 当分布式锁加锁失败时,将消息放入到延迟队列中处理

2、订餐通知:下单成功后60s之后给用户发送短信通知

在订单系统中,在用户发起下单请求之后的一段时间内(一般为30分钟),系统会自动提供支付功能。若该时段内未完成支付操作,则该交易将被标记为已完成并予以关闭

Redis实现延迟队列的基本原理

在延时任务检测器内部的架构中存在两个主要功能模块:一个是负责查询延迟的任务队列信息的子系统(即延迟检测功能),另一个是负责根据时间状态自动触发相应处理流程的子系统(即定时执行机制)。系统首先会对延时任务队列中的各项信息进行读取,在此基础上识别当前队列中的已过期的任务,并触发定时机制以执行这些已过期的任务。

在Redis的数据结构中有哪些能进行时间设置标志的命令?

该数据结构允许我们采用zset(sortedset)进行操作。基于预先设定的时间戳值对元素进行排序后,在内存中不断生成新消息可以通过持续执行zadd score1 value1 ...等命令实现这一目标。为了获取所有符合条件的任务信息,则需要调用zrangebyscale命令来获取所需的数据项;而如果希望只获取最早完成的任务记录,则可以直接调用zrangebyscale key min max withscores limit 0 1来获取最早的任务记录

生产端将数据加入到列表队列中(触发延时操作),核心在于使用redis的有序集合

jedis.zadd(queueKey,System.currentTimeMillis()+10000,s);

在zadd函数中,第二个参数被定义为score值,在这里,得分值则通过将当前时间戳与预期延迟秒数相加来计算得出;而第三个参数则对应于value值。

消费者多线程轮询 zset 获取到期的任务进行处理

Set values=jedis.zrangeByScore(queueKey,0,System.currentTimeMillis(),0,1);

这个函数实现了获取0秒到当前时间戳的数据的一条数据进行处理的功能

  • 标识符(key):顺序数据集的名称。
    • 最低分值(min)与最高分值(max):分数范围内的最小分位点与最大分位点。
    • 带分数选项标志位(WITHSCORES):可选参数,在有此标志位时会返回成员所对应的分数信息。
    • 限定条目数量/起始位置的位置索引(LIMIT):可选参数,在有此指针时会限制返回结果的数量及起始位置。

offset是0,count 是1,表示按照分数大小从小到大进行消费。

Redis用来进行实现延时队列是具有这些优势的:

  1. Redis zset 采用了高效的 score 排序技术。
  2. Redis 在内存中运行,并表现出极高的速度。
  3. 在搭建 Redis 集群时,在消息数量激增的情况下可以通过集群提升消息处理速度并增强系统的可用性。
  4. Redis 提供了数据持久化的机制,在系统故障发生时可以通过 AOF 和 RDB 方式恢复数据以确保数据可靠性。

ZRANGEBYSCORE命令在处理大量数据的过程中可能会导致较高的内存占用以及较高的CPU消耗。该命令依赖于对整个有序集合进行扫描。这提醒我们在使用该命令时需综合考虑数据规模与查询效率。

补充 - 20 、Redis官方为什么不提供Windows版本?

由于Redis以其高效的性能著称,并且采用单线程设计,在处理高并发请求时表现出色;因此Redis内部实现的特点决定了其对操作系统机制中的轮询进行特殊设计,在资源分配和优先级调度方面存在显著差异。

简单来说,在Linux轮询中使用epoll,并采用selector进行窗口管理。在性能方面(epoll)的表现优于(selector)。

所以redis推荐使用linux版本。

补充 - 21、大key和热key

大key:

含有较大数据或含有大量成员的Key称之为大Key,常见的大key如:

  • 字符串类型的键值超出1\text{kb}范围
  • 这些数据结构中的元素数量达到5, 2. 5, 3. 6, 7.
  • 虽然这些数据结构中的元素数量仅为1\text{k}+,但是这些元素所对应的属性总大小却达到了128\text{M}

注意:以上值只是参考,根据实际情况确定。

热key:

某个Key接收到的访问次数、显著高于其它Key时,称之为热Key。

【 大key带来的问题】:
  1. Redis请求速度减缓。
  2. Redis内存持续增长可能导致OOM错误,并可能因达到maxmemory而引发写入阻塞或关键键被逐出。
  3. 某些Redis节点的内存占用显著高于其他节点。
  4. 在分布式架构中,若因处理大量大key而出现请求阻塞,则可能导致服务雪崩。
  5. 删除大型Key耗时较长且可能导致主节点发生阻塞进而触发主从节点切换。
  1. Redis请求速度减缓。
  2. Redis内存持续增长可能导致OOM错误,并可能因达到maxmemory而引发写入阻塞或关键键被逐出。
  3. 某些Redis节点的内存占用显著高于其他节点。
  4. 在分布式架构中,若因处理大量大key而出现请求阻塞,则可能导致服务雪崩。
  5. 删除大型Key耗时较长且可能导致主节点发生阻塞进而触发主从节点切换。
【 热key带来的问题】:
  1. 大量请求涌入服务器系统时可能会让其难以承受压力,并可能导致缓存层发生穿透而导致后端存储出现问题(数据库)。这种情况下会严重影响依赖后端存储服务的所有业务运行。
  2. 在Redis集群中如果出现各节点之间存在流量不平衡的情况,则可能导致Redis集群无法充分释放其分布式计算的优势。具体表现为其中一个数据片负载异常高而其他数据片则相对空闲。

大key、热key的产生原因:

  1. 存储存在不合理之处...存放了内存无法容纳的数据(大key)。
  2. 设计存在缺陷...造成个别key中成员过多。(大key)
  3. 未设置过期时间导致数据持续增长...未定期清理数据(大key)。
  4. 流量出现突增情况...如某款爆款商品等(热key)。
  5. 系统出现异常情况...代码的业务逻辑只增不减且未设置过期时间(热key)。
2.查找方法

2.1.知道具体哪个key有问题
利用调试指令进行分析查找大key:

在调试过程中需要获取Key的相关信息,并返回这些信息的具体内容以及相关的参数设置情况等详细信息...其中...表示该Key的序列化长度可以用此序列化长度作为度量标准来评估该Key的大致大小但这一指标并不一定完全精确而且需要注意的是在执行此操作时会导致所有后续操作等待此过程完成直到当前调试指令完成才可继续其他工作为了避免干扰在线开发环境中不建议使用此功能

利用操作指令进行分析查找大key:

Redis的各种数据结构的操作功能都内置了返回其成员数量的命令。通过采用这些命令进行分析,风险会降低。

2.2.无法明确确定具体的某个关键点存在问题
之前的指令通常会对特定的关键点进行分析
当不确定某个关键点会出现问题时
可以通过参数来进行分析

Redis CLI中的bigkeys命令专为处理大规模键设计。该命令仅能返回单个最大键值。若需执行范围查询,请注意其局限性:例如,在一个集合中筛选出元素个数大于10的所有哈希时,则无法通过此方法完成任务。

redis-cli的hotkeys参数用于统计热key。该参数能够统计各个键被访问的次数。使用该功能的前提条件是确保redis-server中的maxmemory-policy参数配置为最低频率优先(LFU)。

3.处理方法

3.1.大key的处理方法
大key的处理方法有两种:拆分、删除

  • 拆分:例如将规模较大的hash分割为多个hash。
  • 删除:例如将不适用于Redis能力的数据转移至其他存储空间,并在Redis中进行处理。需要注意的是,在使用Redis时,在处理大规模数据(即大key)时可能会遇到耗时较长的问题,并且由于Redis是单线程执行的特性,在批量操作时容易导致阻塞。自Redis 4.0版本起,默认提供了UNLINK命令(也被称为EXPLunge),该命令能够以异步的方式安全地进行大规模数据(即大Key)的安全删除。

3.2.热key的处理方法
热key的处理方法有:复制、读写分离、多级缓存

  1. 复制:在使用Redis集群时,在热key上进行拷贝操作可以在各个Redis节点间进行存储,在这种情况下压力会被分散到各个节点上从而避免单个节点的压力过高从而达到缓解单节点压力的目的然而这种复制方案仅限于代码层面的手动完成而且由于数据的一致性问题导致每次复制都需要重新校验所有从节点的数据因此这种方案仅适用于解决短期的线上问题。

  2. 读写分离:热key大多属于高频率读取操作通过实施读写分离的方式能够保证数据的一致性并能够轻易地实现横向扩展从而有效缓解压力同时这种方式还会带来一定的资源浪费因为在执行读写分离时所有的从节点都会存储相同的热key数据。

  3. 多级缓存:当热key的数量较少例如电商平台的商品促销活动这个时候进行读写分离虽然能够保证数据的一致性但由于机器配置的成本较高因此使用多级缓存是一种更为经济的选择具体实现策略有两种:

复制代码
a. 本地缓存策略:通过在业务服务器与Redis之间设置一个中间层(称为proxy探查层)专门用于探测达到预设阈值的热key并将其结果传递给业务服务器以供其在本地进行缓存这种设计能够在一定程度上平衡系统资源的同时减少对Redis服务器的压力。

b. 单独缓存策略:当proxy探查到需要扩展时会将相关热key推送到另一个单独的Redis集群中如果当前集群仍无法承受负载则会继续横向扩容这种设计虽然增加了服务器的数量但是由于每次扩容之间存在时间间隔因此新旧数据之间会存在最终一致性的问题需要注意的是为了防止逻辑错误应该仅将缓存用于数据查询而不用于业务逻辑处理。

在实际应用中多级缓存可能会因为不同层次之间的数据不一致而导致最终结果出现偏差因此建议根据系统的具体负载情况选择适合自己的优化策略。

补充 - 22、Redis主从复制原理

Redis在主从复制过程中采用的策略为:当主从服务器首次建立连接时,则会立即启动一次全量同步操作;完成全部数据的全量备份后,则会切换到增量复制模式。此外,在特定情况下需要时,在线节点有权利随时执行完整的全量备份操作。

1、主从全量复制的流程:

Redis 全量复制常见于 Slave 初始化阶段,在此期间 Slave 必须将 Master 上的所有数据全部复制一份,请参考以下详细步骤进行操作。

(1)当 slave 服务器与 master 服务器建立连接后,在其后立即启动数据同步流程 1;随后会发送 psync 指令(在 Redis 2.8 版本以前使用 sync 指令)。

(2)当 master 服务器接收到 psync 指令后会启动 bgsave 命令来生成 RDB 快照文件,并利用缓存区来记录以后的所有 write 操作。

  • 如果 master 接收到多个 slave 的并发连接请求时,则只会执行一次持久化操作而非针对每一个连接单独执行一次;接着将此次持久化的数据发送给所有接入的 concurrent connections。
  • 当 RDB 复制耗时超过 60 秒时(repl-timeout),该 slave 服务器会认为复制过程出现了问题;此时建议适当调高相关参数以改善性能。

(3)当 master 服务器 bgsave 执行完成后立即向所有 Slava 服务器发送快照文件,并在此过程中继续将已执行的写命令记录于缓冲区。

  • 客户端输出缓冲区限制设置为 slave 节点分别配置为 256MB、64MB 和 60 MB。若在复制过程中内存占用连续超出 64 MB 或一次性达到或超过 256 MB,则会停止复制操作以避免内存溢出。

(4)当slave服务器接收到RDB快照文件时,在磁盘上完成对该数据的存储后,在删除原有的数据记录基础上进行加载操作,并基于原有的数据版本对外提供服务。

(5)在master服务器完成RDB快照文件发送任务之后,在随后立即向slave服务器发起读取缓冲区中的修改指令。

(6)当slave服务器成功读取并存储了快照文件后,则会转为接收来自主节点的命令请求,并按照顺序执行来自主节点缓冲区的所有修改指令。

(7)如果该slavelike节点启用了自动故障恢复功能,则会立即执行自动故障恢复重写操作。

2、增量复制:

Redis中采用增量复制的方式是在全量复制完成后启动主从服务正常运行的情况下,在主节点完成所有的数据writes之后才会触发次节点的数据同步过程。这种机制的主要特点是当主节点每次执行一个write操作时都会向次节点发送对应的write指令,在此过程中次节点负责接收并处理来自主节点的所有write指令。

3、断点续传:

当网络连接断开需进行重连时, Slave与Master能在仅从中断处继续执行数据复制操作,而无需重新建立同步关系.这被称为断点续传机制.每个主从服务器都维持了一个复制偏移量参数(replication offset)和一个 master线程ID(master run id).每当主 server与从 server 进行同步操作时,从 server 会携带 master run id 和最后一次成功同步所记录的复制偏移量 offset值.通过该参数值可以判断主从两端的数据存在不一致的情况.

4、无磁盘化复制:

在全量复制过程中, master会将数据保存于磁盘中的RDB文件并传输给slave服务器.然而,当master所在的磁盘空间受限或传输速度较慢时,这种操作会对master服务器造成较大负担.对此,自Redis 2.8版本后可采用无磁盘复制技术来解决此问题:通过启动一个socket,在内存中生成新的RDB文件后再传输给slave服务器,无需借助磁盘作为临时存储介质.这种方法通常适用于磁盘空间有限但网络环境较为稳定的场景

repl-diskless-sync :是否开启无磁盘复制

主从复制的特点:

在Redis的主从复制机制中, master节点不会被复制任务所阻塞.这表明,在初始同步阶段,当有多个从节点参与复制时, master节点仍能正常接收和处理来自外部的请求流量.

全部评论 (0)

还没有任何评论哟~