redis事务的那些事情

时间:2022-05-31 16:33:09


很多人认为redis实际没有事务,redis提供的所谓“事务”只不过是一种批处理,与数据库事务基本不是一回事情。

而事实上redis的确实现了真正意义上的ACID事务。
但是的确与传统关系数据库提供的事务有很多不同。

首先看看基于multi的事务。这个事务之所以特别的奇葩,以至于很多人不认为他是真正的事务,
最主要的是因为redis当时缺乏一个脚本,在事务中居然无法读数据,这是指读的数据只能返回给客户端,
而无法在随后的事务中使用,因为redis无法保存读取的数据!

看看mysql,借助存储过程中的脚本

begin work
update t1 set s='123' where id=1
if row_count()=0 then
  rollback
else
   update t2 set s='123' where id=1
   if row_count()=0 then
 rollback
   else
 commit
   end if
end if

很清楚的实现了一个事务,如果t1,t2都存在id=1记录,则同时更新他们。
假如更新一半,断电了,mysql重启后会自动回滚,回到事务开始前的状态。
假如更新时s上有唯一索引,导致更新失败,同样整个事务会回滚,
这种情况并非程序逻辑控制,由系统保证,当然你也可以接管这种事件,
当发生记录重复错误时你自己来决定如何后续操作。

然而类似逻辑完全无法使用multi实现,因为redis无法在multi中判断语句执行结果或者读取数据。
mysql即便不使用存储过程,依然可以在客户端实现控制逻辑,我们可以简单认为mysql开启事务后,
每条sql都是立刻执行然后返回结果,而redis的multi则相当于mysql的存储过程,
但是这个过程内却完全没有控制逻辑。好在redis2.6之后提供了lua脚本,彻底解决了这个问题,
官方文档说明也明确了,lua脚本可以完全替代multi事务,且做得更好,之所以没有去掉multi这个命令,
仅仅为了兼容,未来如果不再需要兼容,multi就会被去掉。

A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.

This duplication is due to the fact that scripting was introduced in Redis 2.6 while transactions already existed long before. However we are unlikely to remove the support for transactions in the short time because it seems semantically opportune that even without resorting to Redis scripting it is still possible to avoid race conditions, especially since the implementation complexity of Redis transactions is minimal.

However it is not impossible that in a non immediate future we'll see that the whole user base is just using scripts. If this happens we may deprecate and finally remove transactions.


所以从现在开始就可以忘记这个命令了。

不过我们依然可以进一步探讨一下multi。即便有这么多限制,multi的确还是实现了真正的事务。

原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)
redis通过单线程,并且保证exec时不会被打断,实际上类似数据库的串行隔离。
ACI都实现了。

持久性(Durability),比较特别,假如redis设置就是不需要持久化,那么根本不存在这个问题。

如果redis需要持久化,此时redis必须启用AOF
appendonly yes

并且设置为每次都fsync
appednfsync always

有些地方表述可能不太清楚,会让人认为redis是后台单独线程写AOF,而返回成功时可能还未实际写盘。
When the AOF fsync policy is set to always or everysec, and a background
# saving process (a background save or AOF log background rewriting) is
# performing a lot of I/O against the disk

不过仔细阅读官方配置中给出的参考
http://oldblog.antirez.com/post/redis-persistence-demystified.html

appednfsync always
In this mode, and if the client does not use pipelining but waits for the replies before issuing new commands,
 data is both written to the file and synched on disk using fsync(2)
before an acknowledge is returned to the client.

那么我们可以确信,此时redis的确完成了事务中的D,虽然变得很慢,但是真的很慢吗?
如果每次都fsync的话,显然写操作最多也就是一般硬盘的100 op/s左右。而实际上redis写速度远远超过!

原来redis还有group commit,和mysql类似,但是实现更简单。
简单说就是1000个client同时写,redis会组合在一起,只作1次fsync,然后再给所有client返回成功。


还是刚才那篇文章:

What Redis implements when appendfsync is set to always is usually called group commit. This means that instead of using an fsync call for every write operation performed, Redis is able to group this commits in a single write+fsync operation performed before sending the request to the group of clients that issued a write operation during the latest event loop iteration.

 In practical terms it means that you can have hundreds of clients performing write operations at the same time: the fsync operations will be factorized - so even in this mode Redis should be able to support a thousand of concurrent transactions per second while a rotational device can only sustain 100-200 write op/s.

 This feature is usually hard to implement in a traditional database, but Redis makes it remarkably more simple.


不用测试我们也知道,如果只有一个client,那么写速率必然会很低,这是你要D数据安全持久的必然代价。

有意思的是这篇文章随后还对比了mysql和postgreSQL的持久化,结论是:
Long story short: even if Redis is an in memory database it offers good durability compared to other on disk databases.

呵呵,的确,你会发现redis在D这方面真的比mysql还牛!


所以,即便是multi也提供了ACID的事务!这点不要怀疑了。
只是multi有些非事务上的缺陷,比如没有可控制的逻辑(当然更不可能有rollbak这个命令,没有逻辑控制即便有命令你什么时候执行?!)。

为了弥补一下这个缺陷,redis增加了watch这个命令,看经典例子:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

因为multi中无法读数据,所以只能在外面读数据,事务外读的数据可能就被别人修改,所以再加个监视,
等于是乐观锁,到multi里一看,watch的已经变了,exec就失败,这样保证ACID的前提下也可以读数据了。

好在这些都是过眼烟云了,现在有了lua脚本,几乎一切问题都迎刃而解,直接可以和mysql的存储过程PK了。
当然只能和存储过程PK,因为无法在客户端执行任何逻辑。

最后之所以是几乎一切,因为lua脚本中也漏了一个命令rollback。作为习惯关系数据库事务的人,这个不啻于晴天霹雳!
但是ACID事务从来没说必须要支持rollback阿,这个的确仅仅是额外的一个功能。

通过这个功能,我们可以执行大量的写操作,然后发现不对了,简单的rollback。
没有rollback的话,你需要自己执行所有写操作的反操作,而显然,反操作需要你把原来的状态先保存起来...

这对于任何复杂逻辑虽然很麻烦,但是却是可以完成的任务。
比如我们自己可以在事务中模拟数据库本身的做法,每改一个地方都将之前的状态保存到临时表里,
需要rollback时在反过来。

redis选择了,将这个艰巨的任务交给你自己,他偷懒少做一个功能,换来就是核心更简单更快速。
对于不太复杂的逻辑来说,这种取舍是有价值的,要时刻注意redis是单线程,脚本执行太长时间,会阻赛所有其他client。

需要特别注意的是,redis脚本中无法主动控制rollback,但是redis本身是会保证脚本要么执行要么不执行,
而不会发生执行一半的情况,即便执行长脚本,redis被杀或者直接拔电。这是通过AOF来实现的,不要怀疑。

(redis可以提供一个自杀的命令来实现rollback,就是脚本一执行这个命令redis就直接崩溃然后重启,这样就等于rollback了和断电被杀类似)


所以总结一下,如果没有bug,redis即便作为一个金融交易数据库也是完全胜任的。

有bug的话,oracle也是没戏啦。