Redis事务和传统事务的区别
事务我们都不陌生,对于关系型数据库来说,事务必须满足:
1.作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行
2.一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。
但是在Redis中,事务的定义有所不同:Redis 事务可以一次执行多个命令, 并且带有以下重要保证:
1.批量操作在发送 EXEC 命令前被放入队列缓存
2.收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
3.在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中
Redis事务命令详解
在Redis中一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
MULTI、EXEC 、 DISCARD 和 WATCH是 以上三个阶段的核心命令。对应的Jedis操作请参考GitHup:https://github.com/granett/Redis
MULTI
标记一个事务块的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC命令原子性(atomic)地执行。
返回值:总是返回 OK 。
redis> MULTI # 标记事务开始
OK
redis> INCR user_id # 多条命令按顺序入队
QUEUED
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC # 执行
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
EXEC
执行所有事务块内的命令。
返回值:
事务块内所有命令的返回值,按命令执行的先后顺序排列。
当操作被打断时,返回空值 nil 。
# 事务被成功执行
redis> MULTI
OK
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
WATCH key [key ...]
监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
返回值:总是返回 OK 。
# 监视 key ,且事务成功执行
redis> WATCH lock lock_times
OK
redis> MULTI
OK
redis> SET lock "huangz"
QUEUED
redis> INCR lock_times
QUEUED
redis> EXEC
1) OK
2) (integer) 1
# 监视 key ,且事务被打断
redis> WATCH lock lock_times
OK
redis> MULTI
OK
redis> SET lock "joe" # 就在这时,另一个客户端修改了 lock_times 的值
QUEUED
redis> INCR lock_times
QUEUED
redis> EXEC # 因为 lock_times 被修改, joe 的事务执行失败
(nil)
UNWATCH
取消 WATCH命令对所有 key 的监视。
注意:如果在执行 WATCH命令之后, EXEC命令或 DISCARD命令先被执行了的话,那么就不需要再执行 UNWATCH了。
因为 EXEC命令会执行事务,因此 WATCH命令的效果已经产生了;而 DISCARD命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH了。
返回值:总是 OK 。
redis> WATCH lock lock_times
OK
redis> UNWATCH
OK
DISCARD
取消事务,放弃执行事务块内的所有命令。
如果正在使用 WATCH命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH。
返回值:总是返回 OK
redis> MULTI
OK
redis> PING
QUEUED
redis> SET greeting "hello"
QUEUED
redis> DISCARD
OK
事务中的错误
使用事务时可能会遇上以下两种错误:
- 1.事务在执行EXEC之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足
- 2.命令可能在 EXEC调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。
第一种:对于发生在 EXEC执行之前的错误,客户端的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。
不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXE命令时,拒绝执行并自动放弃这个事务。
第二种:至于那些在 EXEC命令执行之后所产生的错误, 并没有对它们进行特别处理:即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。即使事务中有某条/某些命令执行失败了, 事务队列中的其他命令仍然会继续执行 —— Redis 不会停止执行事务中的命令。可以看出来redis里的事务并不完全等同于传统事务。
为什么 Redis 不支持回滚(roll back)
对比关系式数据库的事务处理方式, “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点low。
官方解释如下:
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了2 , 又或者对错误类型的键执行了 INCR, 回滚是没有办法处理这些情况的。
鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以Redis 选择了更简单、更快速的无回滚方式来处理事务。
使用watch实现CAS
WATCH命令可以为 Redis 事务提供 check-and-set (CAS)行为。
被 WATCH的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC执行之前被修改了, 那么整个事务都会被取消,EXEC 返回空多条批量回复(null multi-bulk 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的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。
这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。