欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > Java八股文——Redis「事务篇」

Java八股文——Redis「事务篇」

2025/6/21 12:10:23 来源:https://blog.csdn.net/qq_54452916/article/details/148719201  浏览:    关键词:Java八股文——Redis「事务篇」

如何实现Redis原子性?

面试官您好,在Redis中实现原子性,我们需要分两种情况来讨论:单条命令的原子性多条命令组合的原子性

1. 单条命令的原子性:由Redis的单线程模型天然保证
  • 原理:Redis处理客户端命令的核心工作流程,是由一个单的主线程来负责的。这个主线程以串行的方式,从命令队列中取出一条命令,执行它,然后再去取下一条。
  • 效果:在执行任何一条命令的期间,绝对不会有其他命令来“插队”或并发执行。因此,每一条Redis命令本身,都是原子操作。比如INCR, HSET, LPUSH等等,我们完全不用担心它们在执行中途会被打断。
2. 多条命令组合的原子性:必须借助额外手段

然而,在很多业务场景中,我们需要将多个Redis命令组合成一个不可分割的操作单元。比如,经典的“读取-修改-写回”(Read-Modify-Write)模式。

如果简单地将多条命令一条条地发送给Redis,那么在这些命令的执行间隙,就可能会有其他客户端的命令插进来,从而破坏了我们想要的原子性。

为了解决这个问题,Redis提供了几种方案,其中Lua脚本是目前最推荐、也是最强大的方式

  • 终极方案:Lua脚本

    • 它是什么? Redis内嵌了一个Lua语言的执行引擎。我们可以将一系列的Redis命令,编写在一个Lua脚本中。
    • 如何保证原子性? 当我们向Redis发送一个Lua脚本时,Redis会将整个脚本作为一个单独的、不可分割的命令来执行。在执行这个脚本的整个过程中,Redis主线程会被完全占用,不会去处理任何其他请求。这就从根本上保证了脚本内所有命令的原子性。
    • 一个经典的实践案例:安全的分布式锁解锁
      • 问题场景:在解锁时,我们通常需要两步操作:
        1. GET锁的值,判断是不是当前线程加的锁。
        2. 如果是,再执行DEL命令删除锁。
      • 风险:如果不用Lua脚本,那么在一个客户端执行完第一步GET,判断锁是自己的,但在它准备执行第二步DEL之前,这个锁可能因为超时而自动释放了,然后又被另一个客户端B获取。此时,我们再去执行DEL,就会错误地删除了客户端B的锁
      • Lua解决方案:我们会把“判断”和“删除”这两个操作,写在一个Lua脚本里:
        -- script.lua
        if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1])
        elsereturn 0
        end
        
        通过EVAL命令执行这个脚本,就能保证“判断”和“删除”之间,绝对不会有任何其他命令插入,从而实现了安全的解锁。
  • 传统方案:事务 (MULTI / EXEC)

    • 它是什么? Redis也提供了简单的事务功能。通过MULTI开启一个事务,然后将多条命令放入一个队列,最后通过EXEC一次性地、按顺序地执行它们。
    • 它的局限性(为什么不完美)
      1. 不保证原子性(非ACID):Redis的事务,只保证了队列中的命令在执行期间不会被其他命令打断(串行执行)。但是,如果队列中的某条命令在执行时出错(比如对一个String类型执行LPUSH),Redis并不会回滚已经执行成功的命令。它会继续执行完队列里的所有命令。这不符合我们传统数据库中事务的原子性定义。
      2. 缺乏逻辑判断能力:Redis事务只是简单地将命令入队,它不支持在事务中间进行逻辑判断。比如,它无法实现像上面解锁场景中“先GET,然后根据GET的结果,再决定是否DEL”这样的条件逻辑。
    • 结论:由于这些局限性,Redis的事务功能在实践中用得相对较少。

总结一下

  • 对于单条命令,Redis的单线程模型已经为我们保证了原子性。
  • 对于需要将多条命令组合成一个原子操作的复杂场景,Lua脚本是当之无愧的最佳选择。它既能保证原子性,又提供了强大的逻辑编程能力,是实现复杂Redis原子操作的利器。

除了Lua有没有什么也能保证Redis的原子性?

面试官您好,除了Lua脚本,Redis自身还提供了一套事务机制,也可以在一定程度上保证多个操作的原子性。

这套机制主要由三个命令组成:MULTIEXECDISCARD

1. Redis事务是如何工作的?
  • MULTI:当客户端发送MULTI命令时,服务器会开启一个事务上下文。这就像按下了“开始录制”按钮。
  • 命令入队:在MULTI之后,客户端发送的所有命令,都不会被立即执行,而是会被放入一个事务队列中。服务器只会简单地回复QUEUED
  • EXEC:当客户端发送EXEC命令时,服务器会一次性地、按顺序地、不间断地执行事务队列中的所有命令。这个执行过程是原子的,期间不会被其他客户端的命令所打断。
  • DISCARD:如果在EXEC之前,想取消这个事务,可以发送DISCARD命令,它会清空事务队列,并结束事务。
2. Redis事务的“不完美”原子性

Redis事务提供的原子性,和我们传统数据库(ACID)中的原子性,其内涵是不完全相同的。它的“不完美”主要体现在对错误的处理上。

我们需要区分两种类型的错误:

  • a. 命令入队时的错误(编译时错误)

    • 场景:在MULTIEXEC之间,如果发送了一个语法错误的命令(比如命令名字拼错了),或者命令的参数数量不对。
    • Redis的处理:服务器在接收到这个错误命令时,就会立即向客户端报错。并且,当后续EXEC被调用时,Redis会拒绝执行整个事务队列中的所有命令,并返回一个错误。
    • 结论:在这种情况下,事务的原子性是得到保证的(“全不做”)。
  • b. 命令执行时的错误(运行时错误)

    • 场景:所有命令的语法都是正确的,但在 EXEC执行期间,某个命令操作了一个错误类型的数据(比如对一个String类型的键执行LPUSH操作)。
    • Redis的处理
      1. 对于这条出错的命令,Redis会返回一个错误。
      2. 但是,它并不会因此停止或回滚。它会继续向下执行事务队列中剩余的所有命令。
      3. 最终,只有出错的那个命令没有被执行,而其他正确的命令都成功执行了。
    • 结论:在这种情况下,事务的原子性被破坏了(没有做到“全做或全不做”)。
3. 为什么Redis事务不提供回滚?

这是Redis官方的一个明确的设计取向。Redis的作者认为:

  1. 错误只应在开发阶段出现:一个命令会出现运行时错误(如类型不匹配),这通常是程序员的编码错误,应该在开发和测试阶段就被发现和修复,而不应该出现在生产环境中。
  2. 保持简单与高性能:不引入复杂的回滚机制,可以让Redis的内部实现保持极致的简单和高性能
总结:事务 vs. Lua脚本
特性Redis事务 (MULTI/EXEC)Lua脚本
原子性保证不完全 (执行时错误不回滚)完全 (整个脚本作为单一命令执行)
逻辑处理能力 (只能打包命令)强大 (支持if/else, 循环等)
推荐使用简单场景,且不关心运行时错误复杂场景,需要条件逻辑,或追求严格原子性的首选

所以,总结来说,虽然Redis事务也能提供基本的“打包执行”原子性,但由于它缺乏回滚机制逻辑判断能力,其功能是比较受限的。在绝大多数需要保证复杂操作原子性的场景下,Lua脚本是更强大、更可靠、也更受推荐的解决方案


Redis有哪2种持久化方式?分别的优缺点是什么?

面试官您好,为了保证内存中的数据在Redis重启后不丢失,Redis提供了两种主要的持久化方式:RDB(Redis DataBase)AOF(Append Only File)

这两种方式各有优劣,在生产环境中,我们通常会将它们结合使用,以达到最佳的数据安全性和性能。

1. RDB (Redis DataBase) —— “数据快照”
  • 它是什么?

    • RDB是一种快照(Snapshot)持久化方式。它会在某个时间点,将Redis内存中的所有数据,生成一个经过压缩的二进制快照文件(默认为dump.rdb),并保存到磁盘上。
  • 如何触发?

    • 自动触发:通过在配置文件中设置save规则,比如save 900 1(表示在900秒内,如果有至少1个key发生变化,就自动触发一次快照)。
    • 手动触发:执行SAVE(阻塞)或BGSAVE(非阻塞,在后台子进程中执行)命令。
  • 优点

    1. 恢复速度极快:RDB文件是一个紧凑的、包含了某个时间点全量数据的二进制文件。在Redis重启时,只需要直接加载这个文件即可,恢复速度远快于AOF。非常适合用于灾难恢复和冷备份。
    2. 文件体积小:经过压缩,并且只存储了数据本身,所以RDB文件通常比AOF文件要小得多。
    3. 对性能影响小:通常是通过BGSAVE命令,由一个子进程来负责生成快照,对Redis主进程的影响很小。
  • 缺点

    1. 数据丢失风险高:这是它最致命的缺点。RDB是间隔性地进行快照的。如果在上一次快照之后,下一次快照之前,Redis发生了宕机,那么这期间所有发生变化的数据,都将全部丢失
    2. 数据量大时,fork()子进程可能阻塞:虽然是子进程在工作,但在fork()创建子进程的那一瞬间,如果数据量巨大,可能会消耗较多时间和内存,对主进程造成短暂的阻塞。
2. AOF (Append Only File) —— “操作日志”
  • 它是什么?

    • AOF是一种日志持久化方式。它会将客户端发送过来的每一条写命令(如SET, INCR),都以追加(Append) 的方式,记录到一个日志文件(默认为appendonly.aof)的末尾。
  • 如何工作?

    • Redis重启时,会重新执行一遍AOF文件中的所有命令,从而将数据恢复到宕机前的状态。
  • 刷盘策略 (appendfsync)

    • always:每执行一条写命令,就立即同步地刷写到磁盘。最安全,但性能最差。
    • everysec (默认):每秒钟,由一个后台线程将写命令批量同步到磁盘。这是性能和安全性的最佳平衡。即使宕机,最多也只会丢失最后一秒的数据。
    • no:完全由操作系统决定何时刷盘。性能最好,但数据丢失风险最高。
  • 优点

    1. 数据可靠性高:在默认的everysec策略下,数据丢失的风险极低(最多1秒)。
    2. 文件可读性好:AOF文件保存的是文本协议的命令,理论上可读性比RDB好,便于分析和修复。
  • 缺点

    1. 文件体积大:AOF记录的是每一条写命令,所以其文件体积通常会比RDB大得多。
    2. 恢复速度慢:重启恢复时,需要逐条地重新执行所有写命令,当AOF文件很大时,恢复过程会非常漫长。
    3. 有潜在的Bug风险:在某些复杂的命令或场景下,理论上AOF重放的结果可能与原始状态不完全一致(尽管这种情况非常罕见)。
  • AOF重写 (Rewrite):为了解决AOF文件不断膨胀的问题,Redis提供了AOF重写机制。它会在后台创建一个新的、更紧凑的AOF文件,只保留能恢复当前数据集的最小命令集,然后用这个新文件替换掉旧的。

总结与最佳实践

特性RDB (快照)AOF (日志)
可靠性较低(可能丢失几分钟数据)较高(最多丢失1秒)
恢复速度较慢
文件大小(压缩二进制)较大(命令日志)
对性能影响较小(后台子进程)较小(后台线程,但I/O更频繁)

生产环境最佳实践

  • 通常会同时开启RDB和AOF
  • 使用RDB来进行冷备份、全量复制、以及快速的灾难恢复
  • 使用AOF来保证日常运行中数据的最大程度不丢失
  • 当Redis重启时,如果同时存在RDB和AOF文件,它会优先使用AOF文件来进行数据恢复,因为AOF的数据通常比RDB更完整。

通过这种“双保险”的策略,我们就能兼顾性能、恢复速度和数据的可靠性。

参考小林 coding

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词