Redis 事务
1、介绍:
redis 事务 :可以一次执行多个命令,本质上是一组命令的集合。具有如下两个特性:
- 一个事务中的所有命令都会被序列化,按顺序串行化执行,
- 在执行的过程中,不会被其它客户端发送的命令所打断。
- 事务是一个原子操作,要么全部执行,要么全部不执行。
EXEC
命令负责触发并执行事务中的所有命令:
- 如果客户端的使用
MULTI
开启一个事务后,因为断开而没有成功执行EXEC
,那么事务中的所有命令都不会被执行; - 如果客户端在开启事务之后,执行
EXEC
,那么事务中的所有命令都会执行。
当使用 AOF 做持久化时,Redis 会使用单个Write(2)命令将事务写入到磁盘中。
如果Redis服务器因为某种原因被管理员杀死,或者遇到某种故障,可能只有部分事务命令被写入磁盘中。
2、事务常用命令:
命令 | 开始版本 | 描述 |
---|---|---|
MULTI | 1.2.0 | 标记一个事务块的开始。 随后的指令将在执行EXEC时作为一个原子执行 |
EXEC | 1.2.0 | 执行事务中所有在排队等待的指令并将链接状态恢复到正常,当使用WATCH 时,只有当被监视的键没有被修改,且允许检查设定机制时,EXEC会被执行 |
DISCARD | 2.0.0 | 刷新一个事务中所有在排队等待的指令,并且将连接状态恢复到正常 |
WATCH | 2.2.0 | 标记所有指定的key 被监视起来,在事务中有条件的执行(乐观锁) |
UNWATCH | 2.2.0 | 刷新一个事务中已被监视的所有key |
2.1、正常执行
1 | [root@huangkai ~]# redis-cli |
2.2、放弃事务
使用 DISCARD
放弃事务后,EXEC
将抛出错误1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> KEYS *
(empty list or set)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 k1
QUEUED
127.0.0.1:6379> set k2 k2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379>
2.3、全体连坐
当执行错误的命令时,redis抛出异常信息,再执行 EXEC
时,也会抛出执行失败的错误信息,此时事务会回滚。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17127.0 :6379> FLUSHDB
OK
127.0 :6379> MULTI
OK
127.0 :6379> set k1 k1
QUEUED
127.0 :6379> set k2 k2
QUEUED
127.0 :6379> sets k3
(error) ERR unknown command 'sets'
127.0 :6379> set k4 k4
QUEUED
127.0 :6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0 :6379> get k1
(nil)
127.0 :6379>
2.4、冤头债主
先设置 k1 值为 k1,k2 值为 k2,再对 k1执行原子加1操作,显示 (k1 + 1)无法运算,但是在进行 INCR
时,并没有抛出异常,而在 EXEC
时有异常,此时事务并没有回滚,只是异常出现的地方没有执行而已。
可知 Redis事务并不是很严谨,可以说Redis只是支持部分事务。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 k1
QUEUED
127.0.0.1:6379> set k2 k2
QUEUED
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) "k1"
127.0.0.1:6379>
2.5、Watch监控实现乐观锁
WATCH
命令可以为 Redis 事务提供 check-and-set (CAS)行为。
被 WATCH
的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消。
什么是悲观锁?
悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据时都会认识别人会修改,所以每次去拿数据时都会加锁,这样别人拿到数据就会block直到它拿到锁。传统的关系型数据库就用到很多这种锁机制,如表锁,行锁,读锁,写锁等,都是在操作之前先上锁。常用场景如备份数据库表时,锁整个表。
什么是乐观锁?
乐观锁(Optimistic Lock) :就是很乐观,每次去拿数据时都会认识别人不会修改,所以不会加锁,但是在更新的时候,会判断别人在此期间是否有更新数据,可以使用版本号等机会,乐观锁适用于多读的应用类型,这样可以提高系统吞吐量。
乐观锁策略:提交版本号必须大于记录当前版本号才能执行更新。
CAS(Check And Set)
这里引用官网的例子说明, 假设我们需要原子性地为某个值进行增 1 操作(假设 INCR 不存在)
首先我们可能会这样做:1
2
3val = GET mykey
val = val + 1
SET mykey val
上面的这个实现在只有一个客户端的时候可以很好地执行。 但是, 当多个客户端同时对同一个键进行这样的操作时, 就会产生竞争条件。举个例子, 如果客户端 A 和 B 都读取了键原来的值, 比如 10 , 那么两个客户端都会将键的值设为 11 , 但正确的结果应该是 12 才对。
有了 WATCH , 我们就可以轻松地解决这类问题了:
1 | WATCH mykey |
使用上面的代码, 如果在 WATCH
执行之后, EXEC
执行之前, 有其他客户端修改了 mykey 的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。
这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。
WATCH
使得 EXEC
命令需要有条件地执行: 事务只能在所有被监视键都没有被修改的前提下执行, 如果这个前提不能满足的话,事务就不会被执行。WATCH
命令可以被调用多次。 对键的监视从 WATCH
执行之后开始生效, 直到调用 EXEC
为止。
用户还可以在单个 WATCH
命令中监视任意多个键, 就像这样:1
2127.0.0.1:6379> WATCH key1 key2
OK
当 EXEC
被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。
另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。
使用无参数的 UNWATCH
命令可以手动取消对所有键的监视。 对于一些需要改动多个键的事务, 有时候程序需要同时对多个键进行加锁, 然后检查这些键的当前值是否符合程序的要求。 当值达不到要求时, 就可以使用 UNWATCH
命令来取消目前对键的监视, 中途放弃这个事务, 并等待事务的下次尝试。