Redis事务和乐观锁原理详解
Posted 公众号JavaEdge
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis事务和乐观锁原理详解相关的知识,希望对你有一定的参考价值。
Redis事务和乐观锁原理详解
得益于Redis单线程模型的内存处理,没有并发事务,所以无隔离级别概念。
1 事务命令
1.1 MULTI
MUIIT命令执行,标识一个事务的开始,它总返回 OK 。收到该命令后,Redis就知道,接下来再收到的命令需要放到一个内部队列,当EXEC命令被调用时,所有队列中的命令才会被执行,保证原子性。
1.2 EXEC
表示一系列原子性操作的结束。
Redis收到该命令,表示所有要保证原子性的命令操作都已发送完成。此时,Redis开始执行刚才放到内部队列中的所有命令操作。
负责触发并执行事务中的所有命令:
- 若客户端在使用 MULTI 开启事务后,因断线而未成功执行 EXEC ,则事务中的所有命令都不会被执行
- 若客户端成功在开启事务之后执行 EXEC ,则事务中所有命令都会被执行
当使用 AOF 时, Redis 会使用单个 write(2) 命令将事务写入到磁盘中。
然而,若 Redis 服务器因某些原因被管理员杀死或硬件故障,则可能只有部分事务命令成功落盘。
若 Redis 在重新启动时发现 AOF 文件出了这样问题,则它会退出,并汇报一个错误。
使用redis-check-aof程序可修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器顺利启动。
从 2.2 版本开始,Redis 还可以通过乐观锁(optimistic lock)实现 CAS (check-and-set)操作。
DISCARD
客户端可以清空事务队列, 并放弃执行事务。
以下是一个事务例子, 它原子地增加了 foo 和 bar 两个键的值:
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
EXEC 命令的响应是一个数组, 数组中的每个元素都是执行事务中的命令所产生的回复。
回复元素的先后顺序和命令发送的先后顺序一致。
当客户端处于事务状态时, 所有传入的命令都会返回一个内容为 QUEUED 的状态回复(status reply), 这些被入队的命令将在 EXEC 命令被调用时执行。
命令1到命令N是在MULTI命令后、EXEC命令前发送的,它们会被一起执行,保证原子性。
命令入队
当一个客户端切换到事务状态后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MLITL EXEC WATCH、 DISCARD中的一个,立即执行该命令,否则将命令放入一个事务队列,然后
向客户端返回QUEUED回复。
- 如果客户端发送的命令为EXEC、 DISCARD、WATCH、MULTI四个命令的其中一个,那么服务器立即执行这个命令。
- 如果客卢端发送的是四个命令以外的其他命令,服务器不立即执行这个命令。 首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient) 的flags属性关闭REDIS_MULTI标识,并且返回错误信息给客户端。 如果正确,将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复。
事务队列是按照FIFO的方式保存入队的命令。
事务执行
事务中的错误
使用事务时可能会遇上以下两种错误:
事务在执行 EXEC 前
入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:
- 如果命令入队时返回 QUEUED ,则入队成功
- 否则,即入队失败 如果有命令在入队时失败,则大部分客户端都会停止并取消该事务。
但从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。
Redis 2.6.5 以前, Redis 只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,因为发送事务和读取事务的响应都只需要和服务器进行一次通讯。
命令可能在 EXEC 调用之后失败
事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。
至于那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。
从协议的角度来看这个问题,会更容易理解一些。 以下例子中, LPOP 命令的执行将出错, 尽管调用它的语法是正确的:
Trying 127.0.0.1...
Connected to localhost.
Escape character is ^].
MULTI
+OK
SET a 3
abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-ERR Operation against a key holding the wrong kind of value
EXEC 返回两条bulk-string-reply: 第一条是 OK ,而第二条是 -ERR 。 至于怎样用合适的方法来表示事务中的错误, 则是由客户端自己决定的。
- Redis 不会停止执行事务中的命令 即使事务中有某条/某些命令执行失败了, 事务队列中的其他命令仍然会继续执行。
以下例子展示的是另一种情况, 当命令在入队时产生错误, 错误会立即被返回给客户端:
MULTI
+OK
INCR a b c
-ERR wrong number of arguments for incr command
因为调用 INCR 命令的参数格式不正确, 所以这个 INCR 命令入队失败。
为什么 Redis 不支持回滚(roll back)
使用过mysql的, 都会好奇为何 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”。
优点
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现)或命令用在了错误类型的key:即从实用性来看,失败的命令是由编程错误导致,而这些错误应该在开发过程中被发现,而不该出现在生产环境。
- 因为无需支持回滚,所以 Redis 可保持简单快速
有人认为 Redis 处理事务的做法会产生 bug , 但注意通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将value加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。
放弃事务
当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出:
> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"
check-and-set 操作实现乐观锁
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。
被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。
案例
原子地为某值+1(假设 INCR 不存在)。
可能会这样做:
val = GET mykey
val = val + 1
SET mykey $val
这个实现在只有一个客户端时执行得很好。 但多个客户端同时对同一个键操作时, 就会产生竞态。
比如两个客户端 A 和 B 都读取了键原来的值, 比如 10 , 则两个客户端都会将键的值设为 11 , 但正确的结果应该是 12。
WATCH即可解决这类问题:
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
在 WATCH 执行后, EXEC 执行前, 若有其他客户端修改了 mykey 的值, 则当前客户端事务失败。 程序需要做的, 就是不断重试该操作, 直到没有发生冲突。
这种锁称作乐观锁,大多数情况下, 不同的客户端会访问不同键, 碰撞情况一般很少, 所以通常并不需要重试。
WATCH
WATCH是个乐观锁,可为Redis事务提供check-and-set (CAS) 行为。可监控一或多个键,一旦有个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
MULTI命令用于开启一个事务,它总是返回0K。MULTI执行之后,客户端可以继续向服务器发送任意多条命
令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执
行。
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被
打断时,返回空值nil。
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。
UNWATCH命令可以取消watch对所有key的监控。
WATCH 使得 EXEC 命令需要有条件地执行:事务只能在所有被监视键都没有被修改的前提下执行, 如果这个前提不能满足的话,事务就不会被执行。
WATCH 命令可被调用多次。 对键的监视从 WATCH 执行之后开始生效, 直到调用 EXEC。
用户可以在单个 WATCH 命令中监视任意多个键,如下:
redis> WATCH key1 key2 key3
OK
当 EXEC 被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。
另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。
使用无参的 UNWATCH 命令可以手动取消对所有键的监视。 对于一些需要改动多个键的事务, 有时候程序需要同时对多个键进行加锁, 然后检查这些键的当前值是否符合程序的要求。 当值达不到要求时, 就可以使用 UNWATCH 命令来取消目前对键的监视, 中途放弃这个事务, 并等待事务的下次尝试。
使用 WATCH 实现 ZPOP
WATCH 可以用于创建 Redis 没有内置的原子操作。举个例子, 以下代码实现了原创的 ZPOP 命令, 它可以原子地弹出有序集合中分值(score)最小的元素:
WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC
程序只要重复执行这段代码, 直到 EXEC 的返回值不是nil-reply响应即可。
总结
Redis 中的脚本本身就是一种事务, 所以任何在事务可完成的事, 在脚本里面也能完成。
一般使用脚本还更简单更快。因为脚本 Redis 2.6 才引入, 所以导致 Redis 同时存在两种处理事务的方案。不过Redis并不打算在短时间内就移除事务, 因为事务提供了一种即使不使用脚本, 也可避免竞争条件的方法, 而且事务实现并不复杂。
以上是关于Redis事务和乐观锁原理详解的主要内容,如果未能解决你的问题,请参考以下文章