Redis 事务与锁 机制
Redis的事务定义
Redis主要使用MULTI, EXEC, DISCARD 和 WATCH 命令来实现事务功能。
事务可以一次执行多个命令,并带有两个重要的保证:
- 事务中的所有命令都被序列化并按顺序执行。Redis执行事务期间,不会被其它客户端发送的命令打断,事务中的所有命令都作为一个隔离操作顺序执行。
- Redis事务是原子操作,或者执行所有命令或者都不执行。 EXEC 命令触发一个事务中所有命令的执行,所以,如果一个客户端断在调用EXEC 命令前丢失连接,那么所有的命令不会被执行,相反,如果EXEC 被调用,那么所有命令会被执行。
Multi、Exec、discard
Redis使用 MULTI 命令标记事务开始,它总是返回OK。MULTI执行之后,客户端可以发送多条命令,Redis会把这些命令保存在队列当中,而不是立刻执行这些命令。所有的命令会在调用EXEC命令之后执行。保存命令的过程中(没有调用EXEC命令之前)可以通过DISCARD来放弃这部分操作。
案例:
下面是提交成功的情况:
假如在MULTI阶段发生错误,那么整个提交也不会成功:
假如MULTI阶段成功后,EXEC阶段出现错误,那么成功的部分将会被提交,而失败部分(语义错误)将不会被执行:
上面的错误是由incr m1所触发的,因为不能对字符串进行incr操作。
事务的错误处理
从上面的使用案例中可以看到,Redis事务对于错误的回滚,所采用的方式跟以往的关系型数据库不一致。
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
事务冲突的问题
例子
想想一个场景:有很多人拥有同一个的账户,他们尝试使用同一个账户,同时去参加双十一抢购。
假如此时账户只有10000块钱,而一个请求想买8000的商品、一个请求想买5000的商品、一个请求想买1000的商品。
他们的之间的请求几乎是同时进行。那所有人都能实现自己的想法吗?很显然,这在现实中是不被允许的:
于是我们要想办法限制某些人的行动,以达成符合正常事务逻辑的目的。对于程序员来说也就是给用户的操作”上锁“。
悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它能重新拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新数据之前会判断一下在此期间该数据是否被被人更新。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
可以使用版本号等机制实现乐观锁
WATCH
在 Redis 中使用 watch 命令可以决定事务是执行还是回滚。一般而言,可以在 multi 命令之前使用 watch 命令监控某些键值对,然后使用 multi 命令开启事务,执行各类对数据结构进行操作的命令,这个时候这些命令就会进入队列。
当 Redis 使用 exec 命令执行事务的时候,它首先会去比对被 watch 命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。无论事务是否回滚,Redis 都会去取消执行事务前的 watch 命令.
可以看到的是,这其实就是乐观锁
这个过程如图所示:
为了更直观的演示,首先打开两个命令行界面,同时连接上redis。随后使用watch观察同一个变量,在分别multi后,对其执行incrby n 20的操作,随后观察两个界面分别返回的结果:
可以看到的是,先执行exec的客户端返回了正常的返回追,而后执行的客户端则返回了nil(空)。
unwatch
取消 WATCH 命令对所有 key 的监视。
如果在执行WATCH命令之后,EXEC命令或DISCARD命令先被执行,那么就不需要再执行UNWATCH了。
http://doc.redisfans.com/transaction/exec.html
Redis事务三特性
从上面介绍的几小节知识来看,Redis对于事务的支持不用于以往使用的关系型数据库,其主要有如下三个特点:
- 单独的隔离操作
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚