java 如何在Redis中实现带回滚的事务

8yoxcaq7  于 2023-08-01  发布在  Java
关注(0)|答案(3)|浏览(157)

我的程序需要将数据作为一个事务添加到Redis中的两个列表中。两个列表中的数据应一致。如果出现异常或系统故障,因此程序只将数据添加到一个列表中,系统应该能够恢复和回滚。但基于Redis文档,不支持回滚。我该如何实现这一点?我使用的语言是Java。

mzaanser

mzaanser1#

如果你需要事务回滚,我建议使用Redis以外的东西。Redis事务与其他数据存储不同。即使Multi/Exec也不能满足您的需求-首先是因为没有回滚。如果你想回滚,你必须把这两个列表都拉下来,这样你才能恢复--希望在我们的错误条件和“回滚”之间,没有其他客户端也修改了这两个列表。以理智和可靠的方式做到这一点并不琐碎,也不简单。对于SO来说,这可能也不是一个好问题,因为它非常广泛,而不是Redis特定的。
现在说说为什么EXEC不做人们可能会想的事情。在您提出的方案中,MULTI/EXEC * 仅 * 处理以下情况:
1.设置WATCHes以确保没有发生其他更改
1.您的客户在发出EXEC之前死亡

  1. Redis内存不足
    发出EXEC命令完全有可能导致错误。执行EXEC命令时,Redis会执行队列中的所有命令,并返回一个错误列表。它不会提供add-to-list-1工作和add-to-list-2失败的情况。你仍然会有你的两个列表不同步。当你发出MULTI命令后,比如说LPUSH命令,你总是会得到一个OK,除非你:
  • a)以前添加了一个手表,并且该列表中的内容发生了更改,或者
  • B)Redis返回OOM条件以响应排队的推送命令

丢弃并不像有些人想象的那样工作。DISCARD用于而不是EXEC,不是作为回滚机制。一旦您发出EXEC,您的交易就完成了。Redis根本没有任何回滚机制--这不是Redis的事务所关注的。
理解Redis如何调用事务的关键是要认识到它们本质上是客户端连接级别的命令队列。它们不是数据库状态机。

mfpqipee

mfpqipee2#

Redis交易是不同的。它保证了两件事。
1.执行所有命令或不执行任何命令
1.顺序和不间断命令
话虽如此,如果你能控制你的代码,知道什么时候会发生系统故障(某种捕获异常的方式),你就可以用这种方式实现你的要求。

  1. MULTI ->开始交易
  2. LPUSH queue 11->在队列1中推送
  3. LPUSH队列2 1 ->推入队列2
    1.执行/放弃
    在第4步中,如果没有错误,执行EXEC,如果遇到错误或异常,并且您想要回滚,请执行DISCARD。
    希望它有意义。
3xiyfsfu

3xiyfsfu3#

有一种方法,它是多么聪明/错误,由你来决定。
正如其他同事所注意到的,Redis事务保证了原子性,但根本不保证一致性。此外,MULTI/EXEC|DISCARD块之间的所有内容都是作为一个命令发送的,所以如果你使用任何一种更高级别的Redis客户端库,你实际上无法控制沿着发生的事情。
如果真实的的需要篡改中间结果或基于它们执行条件语句,那么默认的选择是编写一些Lua脚本。在脚本中,你可以做任何事情,只受Lua本身的功能和一些Redis环境的限制。
事务中的脚本将被逐个执行,并且您无法停止它。但是,您可以跳过特定的脚本,甚至是其中的一部分。
为了做到这一点,每个交易脚本批必须提供一个唯一的ID(UUID或其他),作为参数传递(我们称之为trx_id):

--[[
  Minor advice: define keys/args tables with local aliases upfront,
  makes things a lot easier while working with the script itself.
--]]

local keys = {
  ...
}

local args = {
  ...
  trx_id = ARGV[13]
}

字符串
现在,如果某个条件未满足,并且您希望停止后续脚本的执行,请使用trx_id end exit设置一个唯一的锁:

--[[
  Minor advice: using hash tag in lock key name will make it drop into the same hash
  slot thus playing nice in the clustering mode (refer to the docs for more info).
--]]

local placeholder = 1

if (true) then
  if args.trx_id then redis.call('SET', '{halt_mark}:' .. args.trx_id, placeholder, 'PX', 1000, 'NX') end
  error(...)
end


不提一些复杂的边缘情况,这将设置一个锁。现在,您有了一个锚点,可以供批处理中的后续脚本使用,因为它们已经提供了相同的trx_id

--[[
  Minor advice: args must be always checked for existence, because Redis
  will silently allow to call a script with less arguments than expected.
--]]

local keys = {
  ...
}

local args = {
  ...
  trx_id = ARGV[42]
}

if args.trx_id and not redis.call('SET', '{halt_mark}:' .. args.trx_id, 1, 'PX', 3000, 'NX') then
  error(...)
end


这个简单的条件将尝试获取锁,如果它已经被占用,则会失败,这意味着前面的操作之一决定它已经足够了。
本质上,它允许您根据“暂停标记”(当您需要中途终止事务时设置)跳过事务中的下一个脚本。
如果一切顺利,不要忘记在链中的每个脚本的末尾释放锁:

if args.trx_id then redis.call('DEL', '{halt_mark}:' .. args.trx_id) end


正如我从一开始就表示的那样,这在任何情况下都不是推荐的方式/最佳实践,但它将在一致性方面模仿关系数据库风格的事务(特别是如果提供了针对错误的定制回滚操作),当没有其他方法时。

相关问题