Redis(四):持久化和事务:RDB(定期备份)【触发机制、流程说明、文件的处理、优缺点】、AOF(实时备份)【使用AOF、命令写入、文件同步、重写机制、启动时数据恢复】、Redis 事务【操作】

接上次博客:Redis(三) Redis客户端:底层的通信原理、Java使用样例列表(引入依赖、配置端口、转发连接、通用命令、字符串操作、列表操作、集合操作、哈希操作、有序集合操作)、集成到Spring Boot-CSDN博客

目录

RDB(定期备份)

触发机制

流程说明

RDB 文件的处理

RDB 的优缺点

AOF(实时备份)

使用 AOF

命令写入

文件同步

重写机制

启动时数据恢复

Redis 事务

事务操作

MULTI命令

EXEC命令

DISCARD命令

WATCH命令

乐观锁和悲观锁

WATCH实现原理

UNWATCH命令


在Redis中,数据必须存储在内存中,这样才能保证快速访问。但是,人们也渴望将数据保存下来,使其在Redis重新启动后不会消失。

于是,有了一个精妙的解决方案。每当有新的数据要进入Redis时,它们会立即在内存和硬盘上留下痕迹,就像在地图上画下两条重叠的线一样。这样一来,无论是快速查找还是安心存储,都变得轻而易举。当需要查询特定数据时,可以直接从内存读取,保证了速度和高效;而硬盘上的数据则负责在Redis重新启动时用于恢复内存中的数据。

当然,这背后隐藏着一个代价:更多的空间消耗。但是不管怎么说,硬盘挺便宜的,这样的开销并不会带来太多的成本。

那么Redis实现持久化的时候具体是安装什么样的策略实现的呢?

Redis支持RDB(Redis Database)和AOF(Append-Only File)两种持久化机制,这些机制有效地防止了由于进程退出而导致的数据丢失问题。通过持久化功能,Redis可以将内存中的数据定期写入到硬盘上的文件中,这样即使Redis服务器意外关闭或重启,也可以利用之前持久化的文件来实现数据的恢复。

在接下来的内容中,我们将介绍RDB和AOF的配置和运行流程,以及控制持久化的命令,例如bgsave和bgrewriteaof。我们将深入了解如何配置Redis来启用这些持久化机制,并且了解它们的工作原理以及如何根据需求进行调整。

此外,我们还将对常见的持久化问题进行分析、定位和优化,以帮助大家更好地理解持久化机制,并在实际应用中遇到问题时能够快速有效地解决。

RDB(定期备份)

RDB持久化是Redis将当前进程中的数据生成快照并保存到硬盘的过程。

在计算机科学领域,快照(Snapshot)是指在某一特定时刻对系统状态的完整拷贝或记录。这个系统可以是计算机的内存、硬盘上的文件系统,或者是分布式系统中的某个节点状态等等。

在数据库或者分布式系统中,快照通常用于数据备份、恢复或者复制。它记录了某一时刻的数据状态,可以在需要时快速恢复到该状态。比如,数据库系统经常会生成定期快照,以便在系统发生故障时,可以通过恢复到最近的快照来保护数据完整性。

在分布式系统中,快照也常用于实现一致性。通过对系统的快照进行复制,可以确保不同节点上的数据一致性,以及在节点故障时能够迅速恢复。

触发RDB持久化过程有两种方式:手动触发和自动触发。

  1. 手动触发: 手动触发RDB持久化通常通过执行SAVEBGSAVE命令来完成。

    • SAVE命令会阻塞Redis服务器进程,直到RDB快照过程完成为止。这意味着在执行SAVE命令期间,Redis服务器将无法处理其他请求。
    • BGSAVE命令会在后台异步执行RDB快照过程,不会阻塞Redis服务器进程,因此可以在快照过程执行期间继续处理其他请求。
  2. 自动触发: 自动触发RDB持久化是通过Redis配置文件中的相关参数来设置的。通过配置save指令可以定义一组规则,当满足这些规则时,Redis服务器会自动执行BGSAVE命令进行持久化。这样可以根据数据修改的频率和累积量来灵活地调整RDB持久化的触发条件,以满足实际需求。

接下来我们就详细了解看看。

触发机制

手动触发RDB持久化分别对应SAVE和BGSAVE命令:

  • SAVE 命令:执行SAVE命令会导致当前Redis服务器进程被阻塞,直到RDB快照过程完成为止。在RDB快照生成期间,Redis服务器无法处理其他请求,因此对于内存较大的实例来说,可能会造成长时间的阻塞。由于阻塞时间较长且影响Redis服务器的正常响应,SAVE命令在实际应用中基本不被采用。

  • BGSAVE 命令:执行BGSAVE命令会导致Redis进程执行fork操作,创建一个子进程来负责RDB持久化过程。持久化过程由子进程完成,完成后子进程自动结束,而主进程不会被阻塞。因此,阻塞只会发生在fork阶段,通常只会持续很短的时间。由于不会阻塞主进程,BGSAVE命令更为常用。在Redis内部,所有涉及RDB的操作都采用类似BGSAVE的方式,以确保Redis服务器的高性能和稳定性。

Redis在执行RDB(Redis Database)持久化时使用的是多进程并发编程,而不是多线程并发编程。

在多进程并发编程中,每个进程都是独立的执行单元,拥有自己的地址空间。在Redis的RDB持久化中,当进行持久化操作时,Redis会派生一个子进程,子进程负责将内存中的数据写入到磁盘上的RDB文件中。这个子进程会复制一份父进程的内存数据,然后独立地进行写入操作,这样可以避免影响到Redis服务器的主要工作流程。

多线程并发编程则是在同一个进程内部创建多个线程,这些线程共享同一个进程的地址空间,可以更方便地共享数据和通信。

但是,在Redis的RDB持久化中,由于涉及到文件操作等IO密集型任务,而且Redis本身采用的是单线程模型,所以选择了多进程的方式来进行并发编程,以确保高效性和稳定性。

当执行SAVE命令时,Redis服务器会立即开始生成RDB快照,这个过程是同步的,因此会阻塞主进程,直到快照生成完毕为止。这意味着在生成期间,Redis无法处理其他客户端的请求,而且阻塞时间会随着数据量的增加而增加,对于大型数据库来说,可能需要花费很长时间。

相比之下,BGSAVE命令使用了一种非阻塞的方式来生成RDB快照。它通过创建一个子进程来执行持久化操作,主进程不会被阻塞,因此能够继续处理其他请求。尽管在执行fork操作时会存在一定的短暂阻塞,但这个过程通常非常快速,并且不会影响Redis服务器的整体性能。由于BGSAVE命令的这种非阻塞特性,它更适合在生产环境中使用,可以保证Redis服务器的响应速度和稳定性。

需要注意的是,虽然BGSAVE命令不会阻塞主进程,但在fork操作期间,仍然会消耗一定的系统资源,特别是在内存较大或数据量较大的情况下。因此,应该根据实际情况选择适合的持久化方式,以平衡性能和数据可靠性的需求。

除了手动触发之外,Redis 还可以通过自动触发 RDB 持久化机制来实现持久化。这种自动触发机制更为实用,主要包括以下几种情况:

  1. 使用 save 配置:通过在 Redis 的配置文件中设置 save 参数,如 "save m n",表示在 m 秒内数据集发生了 n 次修改时,Redis 会自动触发 RDB 持久化。这样可以根据实际需求设置自动触发持久化的条件,以满足不同的业务需求。

  2. 从节点全量复制操作:当从节点进行全量复制操作时,主节点会自动执行 RDB 持久化,并将生成的 RDB 文件内容发送给从节点。这样可以确保从节点在复制完成后拥有与主节点相同的数据快照,保证数据的一致性和完整性。

  3. 执行 shutdown 命令关闭 Redis:当执行 shutdown 命令关闭 Redis 服务器时,Redis 会在关闭过程中执行 RDB 持久化操作,将当前数据集保存到硬盘上的 RDB 文件中。这样可以确保在重启时能够使用之前持久化的数据来恢复 Redis 服务器的状态,避免数据丢失和损坏。

通过这些自动触发机制,Redis 能够在不同的情况下自动执行 RDB 持久化操作,保障数据的持久性和安全性。

流程说明

bgsave 是主流的 RDB 持久化方式,接下来我们就根据下图来了解它的运作流程:

bgsave 命令的运作流程

  1. 当执行 bgsave 命令时,Redis 父进程首先会检查当前是否有其他正在执行的子进程,例如 RDB 或 AOF 子进程。如果存在正在执行的子进程,bgsave 命令会直接返回,不会触发新的持久化操作,以避免影响正在进行的持久化过程。

  2. 如果当前没有其他正在执行的子进程,Redis 父进程会执行 fork 操作,创建一个子进程来执行后续的持久化过程。在 fork 过程中,父进程会被阻塞,直到子进程成功创建。

  3. 父进程成功完成 fork 操作后,bgsave 命令会立即返回信息 "Background saving started",表示持久化过程已启动。此时父进程不会再被阻塞,可以继续处理其他命令请求,而持久化过程会在子进程中异步进行,不会影响 Redis 的正常响应。

  4. 子进程负责执行实际的 RDB 持久化操作。它会根据父进程当前内存中的数据生成临时快照文件,并在生成完成后对原有的 RDB 文件进行原子替换,确保持久化过程的原子性和一致性。持久化完成后,子进程会向父进程发送信号表示持久化成功。

  5. 父进程收到子进程发送的持久化完成信号后,会更新相关的统计信息,例如记录最后一次持久化操作的时间。这些统计信息可以通过 Redis 的 info 命令来查看,以便监控和管理持久化过程的状态和性能。

fork是一个操作系统中的概念,通常指的是在Unix/Linux系统中的一个系统调用,用于创建一个新的进程(称为子进程),该子进程是调用进程(称为父进程)的副本。简单来说,fork会复制父进程的内存和资源,并将其用于子进程,使得子进程可以在独立的执行环境中执行任务。在Windows系统中,类似的功能由CreateProcess函数提供。

具体来说,fork创建子进程的方式十分直接:它直接将当前的父进程复制了一份,一旦复制完成,父子进程就成为两个独立的进程,各自执行各自的任务。fork会复制许多内容,包括程序计数器、虚拟地址空间、文件描述符表等。因此,随着fork的进行,子进程的内存中也会存在与父进程完全相同的变量和数据。

这意味着,复制出来的子进程的内存中的数据与父进程的一模一样。随后,可以安排子进程执行持久化操作,这样就相当于将父进程的内存状态持久化了。例如,如果父进程打开了一个文件,子进程也可以使用相同的文件描述符,导致子进程写入的数据会与父进程写入的数据相同。

另外,不知道你有没有疑问:如果当前Redis服务器中存储的数据特别多,内存消耗特别大。此时进行复制操作是否会带来很大的内存开销?

此处的性能开销其实不大,实际上,fork并非会无脑地将所有数据都拷贝一遍,而是采用了写时复制(copy-on-write)的机制来完成。

写时复制是一种延迟内存拷贝的策略。在fork操作中,当父进程创建子进程时,子进程并不会立即复制父进程的所有内存数据。相反,它们会共享同一份内存数据,也就是说,子进程内部的内存指针指向父进程相同的内存地址。这样做的好处是,节省了内存和时间,因为不需要立即拷贝整个内存空间。

当父进程或子进程中的一个尝试修改内存数据时,写时复制机制才会生效。此时,操作系统会检测到修改操作,并意识到这个数据已经不再安全共享了。于是,它会立即为修改过的数据分配新的内存空间,然后将修改写入新的内存空间中。这样一来,父进程和子进程的内存空间就变得独立了。

这种延迟拷贝的方式使得fork操作的内存开销变得更为轻量级。只有在真正需要时才会进行内存拷贝,而不是在每次fork操作时都进行。这对于Redis服务器这样存储大量数据的场景尤其有利,因为它可以避免在每次复制操作时都消耗大量的内存资源。

因此,尽管Redis服务器中存储的数据量巨大,进行复制操作也不会立即带来大量的内存开销。只有当子进程或父进程对数据进行修改时,才会引起实际的内存拷贝,这种方式可以有效地减少内存开销。

RDB 文件的处理

保存:RDB 文件是 Redis 在进行持久化时生成的快照,它默认保存在配置指定的目录下,通常是 /var/lib/redis/,文件名由 dbfilename 配置项指定,默认为 dump.rdb。如果需要在运行时动态修改保存目录和文件名,可以通过执行 config set dir {newDir} 和 config set dbfilename {newFilename} 命令来实现。这样,在下次 Redis 重启时,新生成的 RDB 文件会保存到新的目录中。

 

这个文件目录通常用于存储 Redis 数据库文件,包括持久化数据、日志文件等。我们打开看看:

dump.rdb就是RDB机制上次的镜像文件。"镜像文件"是一个通用术语,用于描述在某个特定时间点上系统或数据的快照或副本。在这种情况下,dump.rdb可以被看作是Redis数据库在某个特定时间点上的镜像文件,因为它保存了该时间点上内存中的所有数据的副本。这种镜像文件的存在允许在需要时恢复数据库的状态,以及在备份和复制数据时使用。

所以我们就可以说,dump.rdb是Redis数据库的快照文件。它是Redis数据库持久化机制中RDB(Redis DataBase)方式的产物。

Redis默认开启了RDB机制,它将当前内存中的数据保存到一个二进制文件中,这个文件就是dump.rdb。这种方式能够以压缩的形式将内存中的数据保存到硬盘上,从而节省存储空间。

RDB文件保存了Redis数据库在某个特定时间点的数据快照,包括所有的键值对、过期时间和键空间的配置信息等。由于是二进制格式,因此占用的空间相对较小,而且在恢复数据时速度较快。

尽管RDB方式能够节省存储空间并且在数据恢复时速度较快,但它也有一些缺点。首先,生成RDB文件需要消耗一定的CPU资源,因为Redis需要遍历整个数据集并将其序列化到磁盘上。其次,RDB文件是一个快照,只保存了生成时刻的数据,因此在发生故障时可能会丢失部分数据。因此,如果对数据的实时性要求很高,可能需要结合使用AOF(Append Only File)方式进行持久化,以提高数据的安全性和可靠性。

RDB提供了一个用于检查RDB文件的工具——“redis-check-rdb”,它用于验证dump.rdb文件的完整性,并提供有关文件是否受损或格式错误的信息。

语法:

redis-check-rdb <rdb-file>

<rdb-file>:要检查的RDB文件的路径。

工作原理

  1. redis-check-rdb命令会打开指定的RDB文件。
  2. 它会对文件进行解析,验证文件头和版本信息是否正确。
  3. 逐个解析文件中的数据结构,如键值对、过期时间等。
  4. 执行校验和验证以确保文件的完整性。
  5. 如果发现任何问题,它会输出相应的错误信息,指示文件可能存在的问题。
     

注意事项

  • 在运行redis-check-rdb命令时,请确保Redis服务器未运行,并且没有其他进程正在访问或修改RDB文件,以避免文件被锁定或修改。
  • 如果redis-check-rdb命令输出了错误信息,建议尝试修复问题并重试检查,或者考虑从备份中恢复数据。 

redis-check-rdb命令可以用来检查RDB文件的格式是否正确,并且可以检测文件是否受损或损坏。它会扫描RDB文件的内容并执行各种检查,包括检查文件头、版本信息、数据结构和校验和等。如果发现任何问题,该工具将会输出相应的错误信息,指示文件可能存在的问题。

通过定期运行redis-check-rdb命令,可以帮助我们确保dump.rdb文件的健康状态,及时发现并修复任何潜在的问题,从而保障数据的可靠性和持久化机制的有效性。

压缩:

在生成RDB文件时,Redis默认会使用LZF算法对文件进行压缩处理。这种压缩能够显著减小RDB文件的体积,从而节省存储空间。通过压缩,Redis能够更有效地利用硬盘空间,并且在传输和备份过程中减少数据的传输时间和网络带宽消耗。

默认情况下,Redis是开启RDB文件压缩的,这意味着在生成dump.rdb文件时会自动应用压缩。不过,如果需要,也可以通过执行config set rdbcompression {yes|no}命令来动态修改压缩设置。设置为"yes"表示开启压缩,而设置为"no"则表示关闭压缩。通过灵活调整压缩设置,我们可以根据实际需求平衡存储空间和CPU资源的利用。

💡虽然压缩 RDB 文件会消耗一些 CPU 资源,但由于大幅降低了文件大小,方便保存到硬盘或通过网络发送到从节点,因此建议开启压缩功能。

Redis在执行RDB持久化操作时的典型行为:

当执行生成RDB镜像的操作时,Redis会首先将要生成的快照数据保存到一个临时文件中,而不会直接覆盖或修改原始的dump.rdb文件。这样做的目的是为了在生成快照的过程中,保证原始的dump.rdb文件的完整性,防止因为写入过程中出现意外情况导致数据丢失。

一旦新的快照数据生成完毕,Redis会执行以下步骤:

  1. 删除原始的dump.rdb文件,这样就不会产生文件覆盖的冲突。
  2. 将临时生成的RDB文件重命名为dump.rdb,使之成为Redis使用的新的持久化文件。
  3. 这个过程确保了持久化操作的原子性和可靠性。在执行这些步骤期间,Redis会暂停接受新的写入请求,以确保数据的一致性。这也意味着在持久化操作期间可能会存在一些延迟,特别是当数据集较大时。

通过这种方式,Redis能够在持久化操作过程中保持数据的一致性,并且在生成新的RDB文件时不会丢失任何数据。

save指令: save指令用于配置Redis何时执行持久化操作,将内存中的数据写入到磁盘中的RDB文件。指令格式为save <seconds> <changes>,其中<seconds>表示持久化操作之间的时间间隔,<changes>表示在指定时间间隔内对数据库所做的更改次数。只有在指定的时间间隔内,且对数据库的更改次数达到或超过指定的阈值时,Redis才会执行持久化操作。

保存规则示例: 注释提供了一个示例来说明save指令的工作原理。在示例中,配置了三个保存规则:

  • 每隔900秒(15分钟),如果至少有1个键发生了更改,Redis将执行持久化操作。
  • 每隔300秒(5分钟),如果至少有10个键发生了更改,Redis将执行持久化操作。
  • 每隔60秒,如果至少有10000个键发生了更改,Redis将执行持久化操作。

在Redis中,生成RDB快照的成本较高,这意味着在配置生成快照的时间间隔时需要谨慎考虑。频繁地生成快照可能会增加系统的负载,并且在持久化操作期间可能会导致Redis服务器的性能下降。此外,在快照生成的时间间隔内,如果有大量的键值变化操作,可能会导致实时数据与快照数据之间存在较大的偏差。

这种情况下,如果Redis服务器在下一次生成快照之前发生故障,就可能会导致数据丢失。因此,在设置生成快照的时间间隔时,需要根据实际情况平衡系统的性能和数据的可靠性。一般来说,可以考虑以下几点:

  1. 适当延长快照生成的时间间隔: 如果系统的数据变化不是非常频繁,可以适当延长生成快照的时间间隔,减少持久化操作的频率,从而降低系统负载和性能损耗。

  2. 使用AOF持久化方式: 除了RDB持久化方式外,还可以考虑使用AOF(Append Only File)方式进行持久化。AOF方式记录每个写操作,因此可以提供更加精确的持久化,但相对来说会增加写入操作的延迟。

  3. 备份和监控: 定期对Redis数据进行备份,并设置监控机制,及时发现和处理潜在的故障情况。这样即使发生数据丢失,也可以从备份中恢复数据。

禁用持久化: 注释提到了可以通过注释掉所有save指令来完全禁用持久化操作。这意味着Redis将不会自动将数据写入到磁盘中的RDB文件,这在某些情况下可能会导致数据丢失,因此谨慎使用。

删除保存点: 如果需要移除先前配置的保存点,可以添加一条空字符串参数的save指令,即save "",这将删除所有先前配置的保存规则。

通过配置save指令,我们可以根据实际需求来定制Redis的持久化策略,以平衡数据的安全性和性能。

我们可以手动执行一下 save & bgsave 触发一次生成快照:


通过上述操作,可以看到即使我们重启Redis,内存也能够恢复之前的状态:

此时我们再次set一个新的key,但是并不写入bgsave命令,来看看Redis是否会自动进行保存:

 

 

文件里面竟然也保存好了:

 

所以其实,对于Redis生成快照操作,除了可以手动执行命令触发外,还有多种自动触发的方式,其中包括:

  • 通过配置文件中的save指令: 可以在Redis的配置文件中设置save <seconds> <changes>来配置自动触发生成快照的条件。当指定时间内(<seconds>)对数据库所做的更改次数达到或超过指定的阈值(<changes>)时,Redis会自动执行生成快照的操作。
    注意,修改配置之后不会立即生效,而是需要重新启动服务器。这是因为Redis在启动时会读取配置文件,并将配置项加载到内存中,因此在修改配置文件后,需要重新启动服务器才能使新的配置生效。如果想要立即生效,也可以通过命令的方式修改配置:
    CONFIG SET save "<seconds> <changes>"
    

    其中,<seconds>和<changes>分别是想要修改的保存条件,可以根据需要进行调整。执行上述命令后,Redis会立即将新的保存条件应用到内存中,而无需重新启动服务器。

    需要注意的是,通过命令方式修改配置只会在当前Redis进程中生效,不会永久保存到配置文件中。如果想要永久保存修改后的配置,仍然需要手动编辑配置文件并重新启动服务器。

  • 通过shutdown命令: 当执行Redis服务器的shutdown命令(或者通过操作系统的服务管理工具如service redis-server restart)来关闭Redis服务器时,会触发生成快照的操作。Redis会在关闭之前生成当前内存中的数据快照,并保存到RDB文件中。
  • 主从复制过程中: 在Redis进行主从复制的过程中,主节点会在特定情况下自动生成RDB快照,并将快照文件的内容传输给从节点。这样可以确保从节点在进行初始化同步时能够从主节点获取完整的数据集。

通过这些自动触发的方式,Redis能够在适当的时候自动执行生成快照的操作,以确保数据的持久化和备份。这对于数据的安全性和可靠性至关重要,特别是在面对意外故障或者维护操作时。

因此,我们在作业中,担心的其实不是关闭服务器,而是非正常关闭服务器,比如服务器崩溃或者进程被强制终止等。在这些情况下,如果Redis没有及时完成持久化操作并生成快照,可能会导致数据丢失。

bgsave操作的流程是通过创建子进程来完成的。在这个子进程中,Redis会执行持久化操作,将当前内存中的数据写入到一个新的RDB文件中。一旦持久化操作完成,新的RDB文件会被用来替换旧的RDB文件。

然而,由于我们的数据量较小,持久化操作通常速度较快,因此很难直接观察到子进程的执行过程。为了更好地观察持久化过程中文件的替换过程,我们可以利用Linux的stat命令来查看文件的inode编号。

inode编号是文件系统中用于唯一标识文件的一种标识符。当文件被替换时,其对应的inode编号也会相应地发生变化。因此,通过查看文件的inode编号,我们可以间接观察到文件的替换过程。

 

与之相对的,save命令是在Redis的主进程中执行的,而不会触发子进程。当执行save命令时,Redis会在当前进程中将内存中的数据写入到指定的RDB文件中,而不会创建新的子进程。这意味着save命令不会引起文件的替换,而是直接将数据写入到同一个文件中。

因此,相对于bgsave命令,save命令的持久化操作通常会更加简单和轻量,但它也会导致阻塞当前Redis进程,直到持久化操作完成。由于阻塞了当前进程,如果数据量较大或者持久化操作耗时较长,可能会影响Redis的性能和响应速度,特别是在高负载情况下。因此,通常推荐在生产环境中使用bgsave命令进行持久化操作,以避免阻塞Redis进程,保证系统的稳定性和可用性。

关于inide区域,这个内容涉及到Linux文件系统的知识:

在Linux文件系统中(如ext4),典型的组织方式将整个文件系统划分为三个主要部分,它们分别是:

  1. 超级块(Superblock): 超级块是文件系统中非常重要的一部分,它包含了文件系统的重要管理信息,例如文件系统的类型、大小、空闲块的数量、inode 的数量等。超级块通常位于文件系统的起始位置,并且在整个文件系统中只有一个。

  2. inode区域(Inode Area): inode区域存放着一组inode数据结构,每个文件在文件系统中都会分配一个对应的inode数据结构。这个inode数据结构包含了文件的各种元数据信息,如文件的权限、所有者、大小、指向文件数据块的指针等。由于每个文件都有对应的inode,因此inode的数量通常决定了文件系统能够管理的文件数量的上限。

  3. 数据块区域(Block Area): 数据块区域用于存放文件的实际数据内容。文件系统会将文件的数据分成多个数据块存储在数据块区域中。inode数据结构中包含指向这些数据块的指针,以便文件系统能够根据inode找到文件的实际数据内容。

通过这种组织方式,Linux文件系统能够有效地管理文件和目录,并且提供高效的数据存储和访问功能。超级块和inode区域用于管理文件系统的元数据信息,而数据块区域则用于存储文件的实际数据内容。

另外,当执行flushall命令的时候,文件中的内容也相应的被清空了:

校验:

Redis 在启动时会加载 RDB 文件,如果加载到损坏的 RDB 文件,它会拒绝启动,以避免数据丢失或错误。此时可以使用 Redis 提供的 redis-check-dump 工具来检测 RDB 文件,并获取对应的错误报告。这个工具可以帮助管理员快速识别和解决 RDB 文件损坏的问题,确保 Redis 数据的完整性和可靠性。通过详细了解保存、压缩和校验过程,可以更好地管理和维护 Redis 数据库。

RDB 的优缺点

优点:

  1. 紧凑的二进制文件格式:RDB 是 Redis 数据在某个时间点上的紧凑压缩二进制文件格式。这种格式非常适用于备份和全量复制场景。通过定期执行 bgsave 命令生成 RDB 文件,可以将其复制到远程机器或文件系统(如 HDFS),用于灾备和数据恢复。

    RDB(Redis Database)文件是以二进制格式组织数据的,它直接将内存中的数据以二进制形式写入到文件中。在RDB文件中,不同类型的数据以不同的字节格式进行存储,并且具有固定的数据结构。因此,在读取RDB文件时,可以直接按照字节的格式将数据读取到内存中,并且根据预先定义的数据结构将数据映射到相应的数据类型中,如字符串、列表、哈希表等。这种二进制格式的存储方式具有高效性和紧凑性,适合用于数据的持久化和恢复。

    而AOF(Append Only File)文件则是以文本的方式组织数据的,它记录了每个写操作的命令和参数,以文本形式保存在文件中。在AOF文件中,不同的命令和参数之间通常使用空格或者其他分隔符进行分割,因此在读取AOF文件时需要进行一系列的字符串切分操作,将命令和参数解析出来,并根据其内容执行相应的操作。虽然AOF文件相对于RDB文件来说更容易人类可读,但由于其文本格式的特性,相比于二进制格式,AOF文件在存储和读取时通常会占用更多的空间,并且可能会对性能产生一定的影响。

  2. 备份和全量复制:由于 RDB 文件包含了整个数据集的快照,它非常适用于备份和全量复制的场景。管理员可以定期执行 bgsave 命令来生成 RDB 文件,并将其复制到远程机器或者文件系统中,以供灾备和数据恢复之用。

  3. 快速的数据恢复速度:与 AOF 持久化方式相比,通过加载 RDB 文件来恢复数据通常更快。这是因为 RDB 文件是一个数据快照,Redis 只需将其加载到内存中,而不需要逐条执行命令回放的过程。

  4. 离线生成:RDB 文件的生成过程是离线的,不会对 Redis 实例的性能产生明显的影响。这意味着即使在生成 RDB 文件的过程中,Redis 服务仍然可以继续提供服务,不会中断用户的请求。

缺点:

  1. 非实时持久化:RDB 持久化方式无法实现实时持久化或秒级持久化,因为执行 bgsave 命令需要进行 fork 操作来创建子进程。这是一个相对重量级的操作,可能会影响到 Redis 服务器的响应时间,特别是对于大规模数据集和高并发请求的情况下。

  2. 兼容性问题:RDB 文件的格式取决于 Redis 版本,并且随着 Redis 版本的演进,可能会出现多个不同的 RDB 文件版本。这可能会导致兼容性问题,尤其是在升级 Redis 版本或者迁移数据时。因此,在进行版本升级或者数据迁移时,需要特别小心确保 RDB 文件的兼容性。

  3. 数据丢失风险:由于 RDB 文件是在内存快照的基础上生成的,如果在 bgsave 命令执行期间出现故障或者系统崩溃,可能会导致部分数据丢失。尽管 Redis 会尽量保证数据的一致性和完整性,但仍然存在一定的风险。

  4. 增量备份困难:RDB 文件只能提供全量备份,无法实现增量备份。如果需要增量备份,就需要借助其他机制或者技术来实现,增加了管理和维护的复杂度。

AOF(实时备份)

AOF(Append Only File)持久化是一种在 Redis 中用于数据持久化的主要方式。它通过以独立日志的方式记录每次写命令,将这些命令追加到一个文件中,从而实现数据的持久化。在Redis重新启动时,Redis会重新执行AOF文件中的命令,以达到恢复数据的目的。

AOF持久化的主要作用是解决了数据持久化的实时性问题。通过记录每次写命令,AOF能够几乎实时地将数据的变化写入到磁盘中,从而保证了数据的持久化。相比之下,传统的RDB持久化方式需要定期执行bgsave命令来生成快照文件,因此无法做到实时性持久化。

目前,AOF已经成为Redis持久化的主流方式。它不仅能够提供实时性的数据持久化,还能够保证数据的可靠性和一致性。理解和掌握好AOF持久化机制对于确保数据安全性和性能优化是非常有帮助的。通过合理地配置AOF持久化参数,可以根据业务需求平衡数据的安全性和性能之间的关系,从而提高Redis的稳定性和可靠性。

使用 AOF

开启 AOF 功能需要在 Redis 的配置文件中设置 appendonly yes,默认情况下是不开启的。需要修改配置文件开启AOF功能:

 

 

里面通过一些特殊符号作为分隔符,对命令的细节做出了区分: 

此外,可以通过配置 appendfilename 来设置 AOF 文件的名称,默认是 appendonly.aof。保存 AOF 文件的目录通常与 RDB 持久化方式一致,可以通过 dir 配置项指定。

引入AOF(Append Only File)之后,虽然需要同时写入内存和硬盘,但是AOF机制并没有直接影响到Redis处理请求的速度,它不会直接将数据写入硬盘,而是先写入到内存中的缓冲区。只有在缓冲区中的数据积累到一定程度时,Redis才会将数据一次性地写入硬盘。这样做的好处是减少了频繁写入硬盘的次数,从而反而通过一些优化手段提高了性能。

  1. 缓冲区写入: AOF机制会将新的写操作先写入到内存中的缓冲区,而不是立即写入硬盘。这样做的好处是可以快速响应客户端的请求,而不必等待数据写入硬盘完成。

  2. 延迟写入: Redis会定期将缓冲区中的数据批量写入硬盘,而不是每次都立即写入。通过积累一定量的数据并一次性地写入硬盘,可以减少硬盘写入的频率,提高性能。

  3. 顺序写入优化: AOF机制将新的写操作追加到AOF文件的末尾,这属于顺序写入。与随机写入相比,顺序写入的性能更好,因为硬盘进行顺序读写的速度比随机读写要快得多。这样做可以降低写入硬盘的次数,并进一步提高性能。

综上所述,AOF机制通过延迟写入、顺序写入等优化方式,并没有直接影响到Redis处理请求的速度,反而提高了性能。通过减少对硬盘的频繁写入操作,并优化写入方式,AOF机制确保了数据持久化的同时,保持了Redis的高性能特性。

AOF 的工作流程主要包括命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)等步骤:

  1. 命令写入(append):所有的写入命令都会被追加到 AOF 缓冲区中,而不是直接写入到 AOF 文件中。这样可以提高写入操作的性能,因为写入到缓冲区中的数据可以一次性批量写入到磁盘,减少了频繁的磁盘写入操作。

  2. 文件同步(sync):根据配置的同步策略,AOF 缓冲区中的数据会定期或者根据条件触发同步到硬盘上的 AOF 文件中。同步操作可以使用 fsync 或者 fdatasync 等系统调用来保证数据的持久化,以防止数据丢失。

  3. 文件重写(rewrite):随着时间的推移,AOF 文件会变得越来越大。为了减少文件的体积并优化性能,Redis 会定期执行 AOF 文件重写操作。在重写过程中,Redis 会分析内存中的数据并将其转换为命令序列,然后写入到新的 AOF 文件中。新的 AOF 文件通常比原始文件体积更小,并且不包含冗余的命令,从而达到压缩的目的。

  4. 重启加载(load):当 Redis 服务器启动时,可以加载 AOF 文件来进行数据恢复。Redis 会按照 AOF 文件中的命令顺序重新执行命令,从而将数据恢复到最新状态。这种方式确保了数据的持久性,并且在服务重启后可以快速恢复到之前的状态。

通过以上工作流程,AOF 持久化机制能够有效地保证数据的安全性和一致性,同时也能够提高 Redis 的性能和稳定性。

工作流程

命令写入

在AOF持久化中,命令写入的内容直接是文本协议格式,这是因为Redis使用文本协议来表示命令和数据。Redis的文本协议是一种简单的文本格式,它以易于阅读和解析的方式表示命令和数据,并且具有良好的兼容性、实现简单和可读性的特点。

在Redis的文本协议中,每个命令和参数都以一系列特定的字符表示。例如,对于命令"set hello world",其文本协议格式如下:

*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n

在AOF持久化过程中,Redis需要使用aof_buf这个缓冲区的主要原因有以下几点:

  1. 减少IO次数:Redis采用单线程响应命令的模式,在命令写入到AOF文件时,如果每次都直接同步到硬盘,性能会受到较大影响。因为磁盘IO的速度远远慢于内存的读写速度,这样会导致命令的执行延迟增加。通过先将命令写入到aof_buf这个缓冲区,可以有效减少IO次数,提高性能。

  2. 提供多种缓冲区同步策略:Redis为了兼顾数据的安全性和性能,提供了多种缓冲区同步策略,让用户根据自己的需求做出合理的平衡。例如,可以配置在aof_buf中的命令累积到一定数量后再一次性同步到硬盘,或者根据时间间隔定期同步等。这样可以根据实际情况灵活调整同步策略,兼顾了数据的安全性和性能。

  3. 保证可靠性:将命令先写入到aof_buf中,意味着即使在出现异常情况下(比如服务器崩溃),数据仍然会被保存在内存中的缓冲区中。这样可以避免数据丢失的风险,一旦服务器恢复正常,就可以继续将缓冲区中的数据写入到AOF文件中,保证数据的完整性和一致性。

总的来说,aof_buf这个缓冲区的引入使得Redis在AOF持久化过程中能够更加高效地处理命令写入和数据同步,提高了系统的性能和可靠性。同时,通过提供多种缓冲区同步策略,Redis还能够灵活地满足不同用户的需求,平衡数据安全性和性能之间的关系。

文件同步

所谓有得必有失,当发生服务器临时掉电或者进程挂了的情况,如果缓冲区中的数据还没有来得及写入硬盘,那么这部分数据将会丢失,可能导致数据不一致或者丢失。

Redis 提供了多种 AOF 缓冲区同步文件策略,由参数 appendfsync 控制,不同值的含义如下:

AOF 缓冲区同步文件策略

选择合适的AOF缓冲区同步文件策略取决于我们对数据持久化的要求和对性能的权衡。如果数据安全性至关重要,可以选择更频繁的同步策略,而如果追求更高的性能,则可以选择更少的同步次数:

可配置值说明
always

命令写入 aof_buf 后调用 fsync 同步,完成后返回。

频率最高,数据可靠性最高,性能最低。

everysec

命令写入 aof_buf 后只执行 write 操作,不进行 fsync。每秒由同步线程进行 fsync。

频率较低,数据可靠性也会降低,性能会提高。

no

命令写入 aof_buf 后只执行 write 操作,由操作系统控制 fsync 频率。

频率最低,数据可靠性最低,性能最高。

刷新(或同步)频率越高,即将数据写入硬盘的频率越高,数据的可靠性就越高,因为较少的数据会丢失。但与此同时,刷新频率越高,性能影响也越大,因为每次写入硬盘都会引起一定程度的延迟,导致响应时间变长,从而降低了性能。

因此,对于每个应用程序,都需要权衡数据可靠性和性能之间的关系。如果数据的安全性至关重要,可以选择较高的刷新频率以确保数据持久化,即使在发生意外情况时也可以最大限度地减少数据丢失。但如果应用程序更注重性能,并且可以容忍一定程度的数据丢失,那么可以选择较低的刷新频率,以提高系统的响应速度。

系统调用write和fsync的说明如下:

  • write操作:write操作是向文件系统提交数据的基本方式之一。在执行write操作时,数据首先被写入到系统缓冲区中,这样可以提高写入速度,因为它不需要立即写入到磁盘上。相反,write操作将立即返回,允许程序继续执行而不必等待数据写入磁盘。系统会在后台负责将缓冲区中的数据写入磁盘,但具体的时间取决于系统调度机制。这种机制被称为延迟写(delayed write)。

  • Fsync操作:fsync是一种用于文件操作的系统调用,它会将文件系统缓冲区中的数据强制写入到磁盘中,确保数据持久化。fsync操作会阻塞调用它的进程,直到数据写入完成。

  • 配置为always:当配置为always时,每次写入都会立即触发fsync操作,确保数据被写入到磁盘并持久化。这会导致性能较差,因为每次写入都需要等待磁盘操作完成。在一般的SATA硬盘上,只能支持约几百TPS(每秒事务数)的写入。除非数据非常重要,否则不建议使用此配置。

  • 配置为no:配置为no时,系统并不会立即触发fsync操作,而是由操作系统来控制数据同步到磁盘的频率。这样可以提高性能,因为程序不需要等待每次写入都被同步到磁盘。然而,由于同步策略不可控,数据丢失的风险会显著增加。除非数据重要程度非常低,一般不建议使用此配置。

  • 配置为everysec:这是默认配置,也是推荐的配置方式。在这种配置下,系统会每秒钟执行一次fsync操作,将缓冲区中的数据写入到磁盘中,以确保数据的持久化。这种方式兼顾了数据安全性和性能,因为即使系统发生故障,最多也只会丢失1秒的数据。

重写机制

AOF(Append-Only File)是Redis持久化机制之一,它记录了Redis服务器接收到的每个写操作命令,并以追加的方式写入到文件中。随着时间的推移和命令的不断写入,AOF文件会变得越来越大。为了解决这个问题,Redis引入了AOF重写机制,通过重写AOF文件来压缩文件体积。

AOF文件重写的过程是将Redis进程内的数据重新转化为写命令,并以同步的方式写入到一个新的AOF文件中。经过重写后的AOF文件之所以能变小,主要有以下原因:

  1. 进程内已超时的数据不再写入文件:在Redis中,数据通常有过期时间(TTL)。在AOF重写过程中,Redis会检查进程内的数据是否已经过期。对于那些已经超时的数据,即使它们在过去被写入到AOF文件中,但在当前时刻已经失效,不再具有任何意义。因此,在AOF重写期间,Redis会跳过这些已经过期的数据,不再将它们写入到新的AOF文件中,从而减少了文件体积。

  2. 删除旧AOF中的无效命令:在AOF文件中可能存在一些已经失效的命令,比如对已经被删除的键执行的del命令,或者对已经不存在的成员执行的hdel、srem等命令。在AOF重写过程中,Redis会解析旧AOF文件,并只保留那些最终状态的数据,而不保留对应的删除操作。这样做不仅可以减小新AOF文件的体积,还可以简化文件结构,提高AOF文件的可读性和解析效率。

  3. 合并多条写操作为一条:在AOF文件中,可能存在多个连续的写操作针对同一个键,比如连续执行多次lpush list命令。在AOF重写过程中,Redis会将这些连续的写操作合并为一条命令,从而减少了AOF文件中的写命令数量。这种优化不仅减小了文件体积,还能提高AOF文件的写入和解析效率,因为更少的写命令意味着更少的磁盘IO操作和更简单的文件结构。

较小的AOF文件不仅降低了硬盘空间的占用,还能提升启动Redis时数据恢复的速度。因为在重启Redis时,需要加载AOF文件中的数据进行恢复,如果AOF文件体积较小,则读取和解析的时间会更短,从而加快了Redis的启动速度。

AOF重写过程可以手动触发和自动触发:

  • 手动触发:通过调用bgrewriteaof命令来手动触发AOF重写。这个命令会在后台异步执行AOF重写过程。

  • 自动触发:AOF重写也可以根据一定条件自动触发,这些条件由以下两个参数来确定:

    • auto-aof-rewrite-min-size:表示触发重写时AOF文件的最小大小,默认为64MB。当AOF文件大小达到或超过这个阈值时,Redis会考虑执行AOF重写。

    • auto-aof-rewrite-percentage:代表当前AOF文件占用大小相比上次重写时增加的比例。例如,如果设置为100%,表示AOF文件大小翻倍时触发重写。这个参数的默认值为100%。

当AOF文件大小达到auto-aof-rewrite-min-size并且相比上次重写时增加的比例达到auto-aof-rewrite-percentage时,Redis会自动触发AOF重写过程。

自动触发AOF重写的机制可以帮助维护AOF文件的大小,防止文件过大影响性能,并且保证了AOF文件的可持续性和稳定性。

当触发AOF重写时,重写流程如下:

  1. 执行AOF重写请求:

    • 如果当前进程正在执行AOF重写,则请求不执行。
    • 如果当前进程正在执行bgsave操作,重写命令将延迟到bgsave完成之后再执行。
    • 如果在执行bgrewriteaof的时候,发现当前的redis正在生成RDB文件的快照,AOF重写操作将会等待RDB快照生成完毕之后再进行AOF重写。
  2. 父进程执行fork创建子进程。
    在调用fork创建子进程时,子进程会继承父进程的内存状态,包括数据、代码、打开的文件等。但是,由于子进程是在fork之前的内存状态下创建的,因此它不会知道fork之后父进程收到的新请求对内存造成的修改。

    为了解决这个问题,父进程在准备fork之后会创建一个AOF rewrite buffer缓冲区,用来存放fork之后收到的新请求对内存的修改。当子进程完成AOF数据的写入后,它会通过信号通知父进程,然后父进程将AOF rewrite buffer缓冲区中的内容写入到新的AOF文件中。这样就确保了新的AOF文件包含了所有在fork之后发生的修改,从而保证了数据的完整性。

    一旦新的AOF文件生成完成并且写入了所有的AOF数据,父进程就可以使用新的AOF文件代替旧的AOF文件,从而完成AOF重写的过程。这种机制保证了在AOF重写过程中不会丢失任何新的写操作请求,同时确保了数据的一致性和完整性。

  3. 重写:
    a. 主进程在fork之后继续响应其他命令。所有修改操作都会被写入AOF缓冲区,并根据appendfsync策略同步到硬盘,以确保旧AOF文件的一致性。
    b. 子进程只具有fork之前的所有内存信息。父进程需要将fork之后这段时间的修改操作写入AOF重写缓冲区中。

    子进程根据内存快照,将命令合并到新的AOF文件中。
    在Redis中,AOF重写的过程与RDB生成快照的过程非常相似,都是为了将当前内存中的所有数据状态记录到文件中,从而实现数据的持久化。具体来说,AOF重写的过程如下:

    启动AOF重写子进程: 当Redis服务器需要进行AOF重写时,它会启动一个AOF重写子进程。这个子进程负责遍历当前内存中的所有数据,按照AOF的要求生成一个新的AOF文件。遍历内存中的数据: AOF重写子进程会遍历当前内存中的所有数据,包括键值对、过期时间等信息,然后按照AOF文件的格式将这些数据逐个写入到新的AOF文件中。
    生成新的AOF文件: AOF重写子进程根据遍历内存数据的结果,生成一个新的AOF文件。这个新的AOF文件包含了当前内存中的所有数据状态,可以完整地恢复服务器的状态。
    替换旧的AOF文件: 一旦新的AOF文件生成完成,Redis服务器会使用新的AOF文件替换掉旧的AOF文件。这样就完成了AOF重写的过程,新的AOF文件中包含了当前内存中的所有数据状态。

    需要注意的是,与RDB生成快照不同的是,AOF重写的过程是按照AOF文件的格式来生成新的AOF文件的,而不是二进制格式。因为AOF文件是以文本形式记录写操作命令的,所以AOF重写子进程会按照AOF文件的格式将数据写入新的AOF文件中,以确保新的AOF文件能够正确地恢复服务器的状态。

  4. 子进程完成重写:
    a. 新文件写入后,子进程向父进程发送信号。
    b. 父进程将AOF重写缓冲区内临时保存的命令追加到新AOF文件中。
    c. 使用新AOF文件替换旧AOF文件。

    在进行AOF重写的过程中,虽然子进程正在写入新的AOF文件,但父进程仍然会继续接收来自客户端的新请求。当父进程接收到新的写操作请求时,它会将产生的AOF数据先写入到缓冲区中,而不是立即写入到原有的AOF文件中。

    这种机制可以保证在AOF重写过程中,不会丢失来自客户端的写操作请求。即使在AOF重写的同时,父进程仍然会将新的写操作数据缓存起来,并在合适的时机将其写入到新的AOF文件中。

    一旦AOF重写完成并且新的AOF文件已经成功生成,父进程会将缓冲区中积累的数据写入到新的AOF文件中,然后继续处理新的客户端请求。这样就保证了数据的完整性,并且不会丢失来自客户端的任何写操作数据。

AOF 重写流程

这个流程确保了在AOF重写过程中,Redis服务器仍然可以响应其他命令请求,并且通过fork创建子进程来减轻主进程的负担,从而提高了系统的并发性能。同时,通过AOF重写缓冲区的机制,可以保证重写过程的数据一致性和安全性。

对于上述流程,你会不会感到疑问:在AOF重写的过程中,父进程已经启动子进程来写入新的AOF文件,而随着时间的推移,子进程很快就会完成新文件的写入。但在这个过程中,父进程仍然继续写入旧的AOF文件是否还有意义?

考虑到极端情况,如果在AOF重写过程中,服务器突然挂了,那么子进程内存中的数据会丢失,新的AOF文件内容也可能不完整。在这种情况下,如果父进程不继续写入旧的AOF文件,那么在服务器重启时就无法保证数据的完整性了。

因此,为了保证数据的完整性,即使在AOF重写过程中,父进程仍然需要坚持写入旧的AOF文件,直到新的AOF文件已经完全写入并代替了旧的AOF文件。只有在新的AOF文件完全生成且写入完成后,父进程才能停止写入旧的AOF文件,并使用新的AOF文件。这样可以确保在任何情况下都能够保证数据的完整性和一致性。

 

 

观察后你会发现文件存储的内容大大减少。

但是你肯定会感到奇怪,我们不是说AOF是按照文本的方式写入文件的吗?为什么这个文件里面全都是我们看不懂的二进制文件?

AOF 本来是按照文本的方式来写入文件的,但是文本的方式写文件后续加载的成本是比较高的的。redis 就引入了"混合持久化" 的方式。

"混合持久化"是Redis引入的一种持久化方式,结合了RDB和AOF的特点,旨在兼顾性能和成本的平衡。

具体来说,当启用了混合持久化后,Redis会按照AOF的方式记录每一个写操作到AOF文件中。这种方式确保了数据的完整性和持久性,因为每个写操作都会被记录下来,可以确保服务器在重启后能够恢复到最新的状态。

同时,当触发AOF重写时,Redis会将当前内存中的数据状态按照RDB的二进制格式写入到新的AOF文件中,这样可以减少AOF文件的体积,并降低后续加载的成本。这种方式将内存中的数据以二进制格式写入到AOF文件中,相比于文本格式,可以减少文件体积,并且加载时更加高效。

在混合持久化的方式下,后续的写操作仍然会按照AOF的文本方式追加到文件的末尾,以确保AOF文件中包含了最新的写操作。这样,Redis既能够保证数据的持久性,又能够降低持久化操作的成本,从而实现了性能和成本的平衡。

 配置文件中这个选项为yes就代表开启了混合持久化:

 

 

在Redis中,当同时存在AOF文件和RDB快照时,AOF文件通常会被视为主要的持久化方式,而RDB快照则会被忽略。

这是因为AOF文件中包含了更全面的数据记录,每个写操作都会被记录到AOF文件中,因此可以确保数据的完整性和持久性。而RDB快照只是在特定时间点将内存中的数据快照保存到文件中,可能会因为保存间隔而导致数据的部分丢失。

当Redis服务器启动时,如果同时存在AOF文件和RDB快照,Redis会优先加载AOF文件来恢复数据状态,因为AOF文件包含了更全面的数据记录。如果AOF文件不存在或损坏,则Redis会尝试加载RDB快照来恢复数据状态,但这种情况下可能会丢失从最近一次RDB快照到服务器宕机之间的数据变更。

因此,一般来说,AOF文件会被视为主要的持久化方式,而RDB快照则可以作为一种备份的手段。当同时使用AOF和RDB时,可以根据实际需求来配置持久化方式,以确保数据的安全性和可靠性。

最后,我们补充一点关于“信号”的知识:

信号是Linux系统中用于进程间通信和进程管理的一种重要机制,可以被视为Linux的神经系统,因为它允许进程之间相互发送通知、传递消息,并对进程的行为进行控制。信号可以用于多种场景,如通知进程某个事件的发生、中断进程的执行、以及处理异常情况等。

在父子进程场景中,子进程完成了某项任务后可以向父进程发送信号,通知父进程任务的完成情况。这种简单的信息传递对于某些场景是足够的,而且使用信号作为进程间通信的方式具有简单、快速的特点。但是,信号的信息量有限,只能传递一些简单的通知,不能传递大量或复杂的数据。

在Java生态中,通常不鼓励直接使用多进程模型编程,而更倾向于使用线程模型。这是因为Java提供了强大的线程机制,可以更方便地管理和协调线程之间的并发操作。此外,Java中也提供了其他的进程间通信方式,如管道、共享内存、消息队列等,这些方式更适合在Java中进行进程间通信。

总的来说,虽然信号是一种简单而有效的进程间通信方式,但在复杂的场景下可能不足以满足需求。在选择进程间通信方式时,需要根据具体的需求和环境来选择合适的方式,以确保通信的可靠性和效率。

在Java的课程体系中,与信号最接近的概念可以说是事件。在Java中,事件是指某个对象发生的特定动作或状态改变,通常由事件源、事件的类型以及事件的处理函数组成。

具体来说,一个事件通常包含以下几个要素:

  1. 事件源(Event Source): 事件的来源,即触发事件的对象或组件。比如,在图形用户界面(GUI)应用程序中,按钮、文本框等都可以作为事件源。

  2. 事件的类型(Event Type): 表示事件的种类或类型,比如按钮被点击、键盘按键按下等。

  3. 事件的处理函数(Event Handler): 用于处理特定类型的事件的方法或函数,也称为事件监听器。当事件发生时,与之关联的事件处理函数会被调用,执行相应的操作。

信号在Linux内核版本中的事件机制与事件类似,它也有一些类似的要素:

  1. 信号源(Signal Source): 信号的来源,即触发信号的事件或条件,通常由操作系统或其他进程生成。

  2. 信号的类型(Signal Type): 表示信号的种类或类型,比如SIGINT表示中断信号、SIGSEGV表示段错误信号等。

  3. 信号的处理函数(Signal Handler): 用于处理特定类型信号的函数或方法。当收到特定类型的信号时,与之关联的信号处理函数会被调用,执行相应的操作。

可以说,事件与信号在概念上有一定的相似性,都是用于表示某种事件或条件的发生,并且都具有相应的处理机制。在Java中,事件机制更加高级和灵活,而在Linux内核版本中,信号则是操作系统级别的事件机制。

启动时数据恢复

当Redis启动时,会根据RDB(Redis DataBase)和AOF(Append-Only File)文件的内容进行数据恢复。

  1. RDB文件的恢复:如果Redis配置了使用RDB持久化方式,在启动时,Redis会首先检查是否存在RDB文件。如果存在RDB文件且没有设置AOF持久化,Redis会优先加载RDB文件,并将其中存储的数据库状态恢复到内存中。这意味着Redis会忽略AOF文件,而直接使用RDB文件中的数据进行恢复。

  2. AOF文件的恢复:如果Redis配置了使用AOF持久化方式,或者即使配置了RDB持久化也启用了AOF,Redis在启动时会检查AOF文件的存在。如果AOF文件存在且没有关闭AOF功能,Redis会按照AOF文件中记录的写命令逐条执行,从而将数据库状态恢复到最后一次AOF文件记录的状态。这意味着Redis会忽略RDB文件,而直接使用AOF文件中的数据进行恢复。

  3. RDB与AOF文件同时存在的情况:如果Redis既配置了RDB持久化,又配置了AOF持久化,并且同时存在RDB和AOF文件,那么Redis会优先选择AOF文件进行恢复,因为AOF文件中包含了更精确的数据库变更记录。在这种情况下,Redis会加载AOF文件并逐条执行其中记录的写命令,而RDB文件则会被忽略。

Redis 根据持久化文件进行数据恢复

综上所述,Redis在启动时会根据配置以及存在的RDB和AOF文件,进行相应的数据恢复操作,确保数据库能够恢复到最近一次持久化时的状态。

Redis 事务

事务是数据库管理系统(DBMS)中的一个重要概念,它指的是一系列数据库操作(如查询、插入、更新、删除等)的逻辑单元,这些操作被视为一个整体,要么全部执行成功,要么全部执行失败,不会出现部分执行成功、部分执行失败的情况。事务可以确保数据库的一致性、完整性和并发控制。

在Redis中,事务的概念与MySQL的事务类似,都是将一系列命令打包成一个单元进行执行。

然而,需要注意Redis的事务与MySQL事务在实现上有一些区别:

  • 弱化的原子性:Redis的事务并没有像传统数据库中那样的原子性。在Redis中,事务中的命令会依次被执行,但并不会在事务执行过程中进行回滚。如果在事务执行过程中出现了错误,Redis会继续执行后续的命令,而不会回滚到事务开始前的状态。这意味着Redis的事务只能保证命令在事务中的顺序执行,而不能保证整个事务的原子性。

  • 不保证一致性:在Redis中,一致性是指事务运行前和运行后的状态是合理有效的。虽然Redis的事务可以将一系列操作打包执行,但是并不涉及约束或回滚机制来保证数据的一致性。因此,在Redis中,一致性主要体现在事务执行前和执行后的状态是符合预期的,而不会出现中间的非法状态。

  • 不需要隔离性:由于Redis是单线程处理请求的,因此不存在并发执行事务的情况。在Redis中,并没有像传统数据库中那样的隔离级别概念。因为Redis在处理请求时是单线程顺序执行的,所以不需要考虑多个事务之间的隔离性问题。

  • 不需要持久性:Redis的事务是保存在内存中的,并不涉及持久化机制。Redis服务器是否开启持久化与事务本身无关,这完全由redis-server自身的配置决定。因此,即使发生服务器崩溃或重启,事务中的命令也不会被持久化到磁盘上,而是会随着Redis服务器的重启而丢失。

总的来说,Redis的事务机制虽然与MySQL的事务概念相似,但是在实现上有一些不同,特别是在原子性、一致性和持久性方面的处理方式与传统关系型数据库不同。

在Redis中,事务的实现本质上是基于一个称为"事务队列"的概念。当客户端开启一个事务时,它所发送的命令并不会立即执行,而是被放入客户端自己的事务队列中。只有当客户端发送EXEC命令时,才会触发服务器执行队列中的所有命令,从而实现事务中的批量操作。

具体来说,Redis中的事务执行过程如下:

  1. 客户端发送MULTI命令,表示开启一个事务。此时,Redis服务器会为该客户端创建一个事务队列,并将后续发送的命令放入队列中。

  2. 客户端发送的命令并不会立即执行,而是被暂存到该客户端的事务队列中。这样,即使其他客户端在此期间发送了命令,也不会影响到正在执行事务的客户端。

  3. 当客户端发送EXEC命令时,Redis服务器会执行该客户端事务队列中的所有命令。这些命令会按照在事务中的顺序依次执行,并且在执行过程中不会受到其他客户端的干扰。

  4. 执行完事务队列中的所有命令后,Redis服务器会将执行结果返回给客户端,并清空该客户端的事务队列。

需要注意的是,Redis的事务是由主线程完成的,而不是由专门的线程或进程执行。主线程会按照FIFO(先进先出)的原则依次执行每个事务队列中的命令,并在执行完毕后再处理其他客户端的命令请求。这种机制保证了事务的原子性和一致性,使得多个客户端可以并发地使用事务,而不会相互影响。

因此,Redis的事务相比于MySQL来说,功能上是弱化很多的。它只能保证事务中的这几个操作是"连续的",不会被其他客户端"插队",仅此而已。

你或许会问:Redis 的事务为啥就设计得这么简单?为啥不设计成和 MySQL 一样强大呢?

Redis的设计理念与MySQL有所不同,它主要以内存为核心,追求高性能和简单性。因此,Redis的事务设计也是为了保持这种简单性和高性能,而不会像MySQL那样提供复杂的事务支持。

MySQL的事务支持是非常强大和复杂的,它支持ACID(原子性、一致性、隔离性、持久性)特性,具有较高的数据一致性和完整性,但这也意味着MySQL需要花费更多的空间和时间来维护事务的状态,包括undo log、redo log、事务锁等机制,同时会增加数据库的负载和性能开销。

相比之下,Redis更注重于简单、快速的数据存储和访问,因此它的事务设计更加轻量级和简单。Redis事务的主要目的是为了保证一系列操作的原子性,但并没有提供像MySQL那样复杂的事务隔离级别和锁机制。Redis的事务机制主要基于命令队列的方式,可以确保一系列操作要么全部执行成功,要么全部失败,但并不支持像MySQL那样的回滚和事务的隔离级别。

因此,Redis的事务机制虽然简单,但也正是这种简单性和高性能,使得Redis在某些场景下具有更好的性能表现和适用性。当应用场景对数据的一致性要求不是特别严格,但需要高性能和低延迟时,Redis就成为了一个非常合适的选择。

Redis事务的适用场景

  1. 原子性操作: Redis事务适用于一系列操作需要作为一个整体执行,且可以容忍部分操作失败的场景。尽管Redis事务不能支持全部回滚,但仍然可以确保一组操作要么全部成功执行,要么全部失败。

  2. 批量操作: 如果需要一次性执行多个操作,并希望减少网络通信的开销,可以使用Redis事务来打包多个操作。这样可以减少网络延迟,提高性能。

  3. 乐观锁控制: 在需要乐观锁控制的场景下,可以使用Redis事务来实现乐观锁。通过在事务中对数据进行检查和更新,可以避免并发冲突,确保数据的一致性。

  4. 队列操作: 当需要对队列进行一系列操作,如入队、出队、查看队列长度等,可以使用Redis事务来确保操作的原子性和一致性。

  5. 计数器操作: 当需要对计数器进行增减操作,并确保操作的原子性和一致性时,可以使用Redis事务来执行计数器操作。

总的来说,Redis事务适用于那些需要一次性执行多个操作,并可以容忍部分操作失败的场景。它能够简化代码逻辑,提高性能,并保证数据的一致性。但需要注意的是,Redis的事务并不支持回滚操作,因此在设计事务时需要考虑到事务的原子性和一致性。

事务操作

MULTI命令

MULTI命令用于开启一个事务。在执行MULTI命令后,Redis服务器会进入事务模式,接下来的所有命令都会被添加到一个事务队列中,而不会立即执行。如果MULTI命令执行成功,服务器将返回"OK"作为确认。

127.0.0.1:6379> MULTI
OK

在这个例子中,MULTI命令成功执行,并返回"OK",表示事务已经成功开启。接下来的所有命令都会被添加到事务队列中,直到EXEC命令被调用以执行事务中的所有命令。

EXEC命令

EXEC命令用于执行事务队列中的所有命令。在调用EXEC命令时,Redis服务器会按照事务队列中命令的顺序依次执行,并返回每个命令的执行结果。在使用MULTI命令开始一个事务后,每次向事务队列中添加命令时,服务器都会返回"QUEUED"来确认命令已被成功添加到队列中。但这些命令并不会立即执行,而是在执行EXEC命令时才真正被发送给服务器执行。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 "Hello"
QUEUED
127.0.0.1:6379> GET key1
QUEUED
127.0.0.1:6379> INCR counter
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "Hello"
3) (integer) 1

在这个示例中,首先使用MULTI命令开启了一个事务。然后,使用SET命令设置了键key1的值为"Hello",GET命令获取了键key1的值,以及INCR命令对counter进行了自增操作。每次命令执行后都返回"QUEUED",表示命令已经被成功添加到了事务队列中。

最后,在执行EXEC命令时,服务器会按照事务队列中命令的顺序依次执行这些命令。在这个示例中,三个命令都成功执行,因此EXEC命令返回了一个包含三个结果的数组,分别是对应命令的执行结果。

 

DISCARD命令

DISCARD命令用于放弃当前事务,它会直接清空事务队列,使得事务队列中的所有命令都不会被执行。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 "Hello"
QUEUED
127.0.0.1:6379> GET key1
QUEUED
127.0.0.1:6379> INCR counter
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> GET key1
(nil)
127.0.0.1:6379> GET counter
(nil)

在这个示例中,首先使用MULTI命令开启了一个事务,并向事务队列中添加了三个命令。然后,执行DISCARD命令,服务器会放弃当前事务,清空事务队列。

最后,使用GET命令尝试获取key1和counter的值,发现它们都返回了nil,表示这些键并不存在,因为之前的操作都没有被执行到。

在Redis中,如果我们开启了一个事务(通过 MULTI 命令),然后发送了一系列的命令(比如使用 EXEC 来执行这些命令),但在这些命令执行完毕之前服务器重启了,那么这个事务的效果将会等同于 DISCARD,也就是事务中的所有命令都会被取消,不会对数据造成任何影响。

这是因为在Redis中,事务并不是像关系型数据库中那样具有隔离性和持久性的概念。Redis的事务仅仅是将一系列命令打包发送给服务器,服务器在收到 EXEC 命令时才会执行这些命令。因此,如果在事务执行期间服务器重启,那么服务器会丢弃这个事务的所有命令,就好像这个事务从来没有执行过一样。

此时考虑一个场景,我们先在客户端1里面设置key,然后不执行EXEC,去客户端2里面设置可以,看看哪个后执行?

 


在 Redis 中,MULTI 和 EXEC 命令之间发送的命令被视为一个事务。因此,如果客户端1先发送了 set key hello 命令,然后发送了 EXEC 命令,而客户端2在此期间发送了 set key 66666 命令,那么即使客户端1先发送了命令,但实际上在执行时是按照命令的先后顺序来执行的。

换句话说,Redis 会按照收到命令的先后顺序来执行事务中的命令,而不是根据命令的实际执行时间来决定执行顺序。因此,无论客户端1的命令在时间上是先于客户端2的命令,但在事务执行时,如果客户端1先发送了 EXEC 命令,那么它的命令会在客户端2的命令之前执行。

这个时候我们就可以使用WATCH命令监控这个key,看看这个key是否在MULTI 和 EXEC 命令之间被外部的客户端修改了。

WATCH命令

WATCH命令用于在事务执行之前(必须搭配事务使用,而且必须在MULTI命令之前执行)监视一个或多个键,如果在事务执行期间,被监视的键被其他客户端修改了,那么事务将被中止。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key 100
QUEUED
127.0.0.1:6379> set key 200
OK
127.0.0.1:6379> EXEC
1) OK
127.0.0.1:6379> get key
"100"

我再问:此时, key 的值是多少呢???

尽管从输入命令的时间顺序来看,客户端1先执行了set key 100,而客户端2后执行了set key 200,但是实际执行的时间顺序却是相反的,因为在Redis中,命令的执行顺序是按照命令到达服务器的顺序来确定的。由于Redis是单线程处理请求的,所以它会按照命令到达的顺序来执行。

因此,当客户端1执行EXEC命令时,实际上是在客户端2已经执行完了set key 200命令之后。由于WATCH命令没有被使用,Redis不会监视键的变化,因此客户端1执行EXEC命令时,会成功执行set key 100命令,将键key的值设置为100。

这种情况下,尽管存在命令输入的时间顺序与实际执行的时间顺序不一致的情况,但由于没有使用WATCH命令来监视键的变化,所以Redis不会中止事务,也不会回滚到事务开始前的状态。因此,键key的值最终是100。

然而,这种情况容易引起歧义和混淆。虽然Redis的事务机制不提供严格的隔离性,但在类似的情况下,至少应该向用户提示当前操作可能存在风险。

WATCH命令在Redis中被用来解决并发修改导致的数据不一致问题。通过WATCH命令,客户端可以监视一组特定的键。

具体来说:

  • 当客户端调用WATCH命令时,Redis服务器会记录下被监视键的当前版本号。这个版本号是一个简单的整数,每次对键的修改都会使得版本号增加。服务器会维护每个被监视键的版本号情况。
  • 在开启事务时,如果事务中的命令需要对被监视的键进行修改,Redis会检查被监视键的版本号是否与事务开始时记录的版本号一致。如果一致,事务会继续执行;如果不一致,Redis会标记事务并在执行时将其中止。这样可以确保事务执行期间被监视的键没有被其他客户端修改过。
  • 在真正提交事务时,Redis会再次检查被监视键的版本号是否发生变化。如果发现当前服务器上被监视键的版本号已经超过了事务开始时记录的版本号,说明事务执行期间被监视的键已经被其他客户端修改过,此时Redis会让事务执行失败,事务中的所有操作都不会被执行。

我们再次演示如何使用WATCH命令来实现乐观锁并保证事务的一致性:

首先,客户端1开始监控键k1:

127.0.0.1:6379> WATCH k1
OK

然后,客户端1开启一个事务,并对键k1进行修改,同时也向事务队列中添加了对键k2的修改操作: 

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 100 # 将键k1的值设置为100,并记录此时服务器端的k1的版本号为0
QUEUED
127.0.0.1:6379> SET k2 1000 
QUEUED

这些命令只是入队列,还没有真正提交事务执行。

此时,客户端2执行了一个修改操作,成功地将键k1的值修改为200,从而使得服务器端的k1的版本号从0变为1:

127.0.0.1:6379> SET k1 200 # 成功修改,使服务器端的k1的版本号从0变为1
OK

客户端1再次执行事务:

127.0.0.1:6379> EXEC
(nil)

由于客户端2在事务执行期间修改了键k1的值,导致服务器端的k1的版本号变化,因此在客户端1执行事务时,服务器发现k1的版本号已经超过了事务开始时记录的版本号,于是取消了事务。因此,事务中的所有操作都没有被执行。

此时,键k1的值已经被客户端2修改为200,而键k2没有被修改过,因此最终的结果是键k1的值为200,而键k2不存在。

回到最初的例子:

 

WATCH命令的实现类似于一个乐观锁。

我们先来了解一下什么是乐观锁,什么是悲观锁,这样才方便我们探讨WATCH命令背后的原理。

乐观锁和悲观锁

乐观锁和悲观锁是并发控制的两种不同策略:

  1. 悲观锁(Pessimistic Locking):在悲观锁的策略下,系统默认会认为并发冲突是常态,因此在操作数据之前会先获取锁,确保在操作过程中其他线程不能修改数据,这样可以保证数据的一致性。常见的悲观锁实现包括数据库中的行级锁和表级锁。

  2. 乐观锁(Optimistic Locking):与悲观锁相反,乐观锁假设并发冲突的概率较低,因此不会直接加锁,而是在更新数据时先读取数据并记录版本号或者时间戳等信息,然后在更新数据时检查这些信息是否发生变化,如果没有变化则更新成功,否则认为发生了并发冲突,需要进行相应的处理。乐观锁常见的实现方式包括版本号机制和时间戳机制。

在两个线程同时操作同一个锁的情况下:

  • 如果采用悲观锁,其中一个线程会成功获取到锁,而另一个线程则会被阻塞,直到获取到锁为止。
  • 如果采用乐观锁,两个线程都可以尝试进行操作,但在更新数据时会检查数据是否被其他线程修改过,如果没有则更新成功,否则需要处理冲突。

因此,乐观锁和悲观锁的选择取决于实际情况以及对并发控制的要求。

在 C++、Linux 中,mustd::mutex 是一种悲观锁。当一个线程获取了 std::mutex 的锁,其他线程如果要获取该锁,就会被阻塞,直到锁被释放。这种行为符合悲观锁的特点,即默认情况下假设并发冲突是常态。

而在 Java 中,synchronized 关键字既可以是悲观锁,也可以是乐观锁,这取决于具体的使用方式。

  • 悲观锁(Pessimistic Locking):当你使用 synchronized 块来同步访问共享资源时,它默认是一种悲观锁。例如
    synchronized (lockObject) {
        // 访问共享资源的代码
    }
    
    在这种情况下,线程会先获取 lockObject 的锁,如果其他线程要访问该共享资源,就会被阻塞,直到锁被释放。
  • 乐观锁(Optimistic Locking):Java 中的乐观锁一般是基于 CAS(Compare and Swap)操作实现的。例如,java.util.concurrent.atomic 包中的原子类(如 AtomicInteger、AtomicLong)就是乐观锁的典型应用。这些原子类提供了一种无锁的并发操作方式,通过 CAS 操作来保证并发安全。

所以synchronized 关键字在 Java 中可以用作悲观锁,而乐观锁通常需要借助特定的类或者自定义实现。

悲观锁优点:

  • 保证数据一致性: 悲观锁通过在操作数据之前获取锁来确保数据的一致性,因此可以有效地避免并发修改导致的数据不一致问题。
  • 简单易用: 使用悲观锁可以简化并发控制的实现,只需在关键代码段获取锁即可,不需要考虑额外的冲突检测和处理逻辑。


悲观锁缺点:

  • 性能开销较大: 悲观锁会在操作数据之前先获取锁,如果有大量的并发访问,那么可能会导致线程竞争和性能下降。
  • 可能引起死锁: 如果对资源的加锁操作不当,可能会引发死锁,使得线程陷入等待状态,无法继续执行。


乐观锁优点:

  • 降低锁冲突: 乐观锁不会直接获取锁,而是先进行操作,只有在提交时才进行冲突检测,因此可以降低锁冲突的概率,提高并发性能。
  • 减少线程阻塞: 乐观锁允许多个线程同时读取和修改数据,只有在提交时才会检测冲突,因此可以减少线程的阻塞时间,提高系统的响应速度。

乐观锁缺点:

  • 冲突检测和处理造成额外的开发和维护成本: 乐观锁需要在提交时检测数据是否被其他线程修改过,如果发现冲突,需要进行相应的处理,比如重试或者回滚操作,增加了额外的开发和维护成本。
  • 不适用于频繁更新场景: 如果数据的更新频率较高,那么乐观锁可能会导致大量的冲突和重试操作,从而降低系统的性能。

综上所述,选择使用悲观锁还是乐观锁取决于具体的业务需求和系统性能要求。悲观锁适用于需要确保数据一致性的场景,而乐观锁适用于并发访问频率较高且冲突概率较低的场景。

WATCH实现原理

WATCH 命令在 Redis 中用于实现乐观锁的机制,其基本原理是通过给指定的键设置一个版本号或者计数器,然后在执行事务之前监视这些键,以便在事务执行期间检测到这些键是否被其他客户端修改过。如果被监视的键在事务执行期间被修改过,那么事务会被放弃。

下面是 WATCH 命令的基本实现原理:

  1. 当客户端执行 WATCH key 命令时,Redis 会为该键设置一个版本号或者计数器,并将该键标记为被监视的状态。
  2. 当另一个客户端修改了被监视的键时,Redis 会增加该键的版本号或者计数器,并将该键标记为已修改的状态。
  3. 在事务执行期间,如果被监视的键被修改过(即版本号或计数器发生变化),那么 Redis 将放弃执行事务,并向客户端返回一个 WATCH 错误。
  4. 如果被监视的键未被修改过,那么 Redis 将继续执行事务,并在 EXEC 命令执行完毕后返回事务执行结果。

这种方式实现了一种乐观锁的机制,在执行事务之前并没有立即加锁,而是在事务执行期间检测是否有其他客户端修改了被监视的键。因此,WATCH 命令的本质是给 EXEC 命令加上了一个乐观锁的判定条件,以确保事务的原子性和一致性。

在 CAS(Compare and Swap)操作中,ABA 问题是一个经典的例子,而解决这个问题的思想方法与我们提到的乐观锁中的版本号控制类似。

ABA 问题指的是,在执行 CAS 操作时,如果被比较的内存值从 A 变为 B,然后又恢复为 A,那么尽管值发生了变化,但 CAS 仍会成功。这可能会导致一些意外的后果,因为 CAS 操作无法检测到中间的变化。

为了解决 ABA 问题,通常会引入版本号或者标记,类似于乐观锁的机制。当执行 CAS 操作时,不仅会比较内存值,还会比较版本号。如果内存值发生了变化,但版本号也同时发生了变化,那么 CAS 操作就会失败,因为这表示在 CAS 操作期间发生了其他的并发修改。

这种思想方法的应用不仅限于 CAS 操作和乐观锁,而是可以在许多并发控制和数据一致性的场景中得到应用。通过引入额外的版本号或者标记来跟踪状态的变化,可以有效地避免一些并发问题,保证操作的正确性和一致性。

UNWATCH命令

UNWATCH命令用于取消对某些键的监视,它是WATCH命令的逆操作。

具体来说:

  • 当客户端调用UNWATCH命令时,Redis会取消对之前使用WATCH命令监视的所有键的监视。
  • 取消监视后,客户端不再关注任何键的版本号变化,因此之后对这些键的修改不会影响到事务的执行。

UNWATCH命令通常在客户端需要结束事务或者切换到监视其他键时使用。

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值