在Redis中,事务是以multi/exec/discard进行的, 其中multi表示事务的开始, exec表示事务的执行,discard表示丢弃事务。
3 127.0.0.1:6379> multi # 事务的开始
4 OK
5 127.0.0.1:6379> set 1 1 # 添加命令
6 QUEUED
7 127.0.0.1:6379> set 2 2
8 QUEUED
9 127.0.0.1:6379> exec # 执行事务
10 1) OK
11 2) OK
12 127.0.0.1:6379> get 1
13 "1"
14 127.0.0.1:6379> get 2
15 "2"
16 127.0.0.1:6379>
1 127.0.0.1:6379> multi # 事务的开始
2 OK
3 127.0.0.1:6379> set 3 3 # 添加命令
4 QUEUED
5 127.0.0.1:6379> set 4 4
6 QUEUED
7 127.0.0.1:6379> discard #丢弃事务
8 OK
9 127.0.0.1:6379> exec # 此时在执行exec命令会报错。
10 (error) ERR EXEC without MULTI
11 127.0.0.1:6379>
上面演示了一个完整的事务过程,所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,它不用担心自己在执行队列的时候被其它指令打断,可以保证他们能得到的“原子性”执行。另外QUEUED没什么特别的意味,就像OK一样,他只是申明命令已经放入缓存队列中了。
redis原子性?
在mysql中事务会有一个原子执行,如果在事务中一个命令执行时发生错误,其他命令也不会得到执行,但是在redis中是否存在这样的设置?
1 127.0.0.1:6379> multi # 事务的开始
2 OK
3 127.0.0.1:6379> set 1 's'
4 QUEUED
5 127.0.0.1:6379> INCR 1
6 QUEUED
7 127.0.0.1:6379> set 2 'ss'
8 QUEUED
9 127.0.0.1:6379> exec
10 1) OK
11 2) (error) ERR value is not an integer or out of range # 因为键为1的值为字符串,不能对其自增,因此报错。
12 3) OK
13 127.0.0.1:6379> mget 1 2 # 键值对2 ‘ss'还是设置成功。
14 1) "s"
15 2) "ss"
从上面我们可以看出,即使在Redis事务中存在错误的命令,也不会妨碍后面的命令执行。
redis中管道
在上面 Redis 事务例子中,在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以通常 Redis 的客户端在执行事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作。来增加性能。
1 1 import redis
2 2
3 3 re = redis.Redis('localhost', 6379, db =0)
4 4
5 5 p = re.pipeline(transaction=True)
6 6 p.multi()
7 7 p.set('test_1', 'test——1')
8 8 p.set('test_2', 'test_2')
9 9 p.execute()
10 10
11 11
12 12 # redis 客户端中查询结构
13 13 127.0.0.1:6379> get test_1
14 14 "testxe2x80x94xe2x80x941"
15 15 127.0.0.1:6379> get test_2
16 16 "test_2"
17 17 127.0.0.1:6379>
watch命令
假如有多个客户端会并发进行操作。我们可以通过 Redis 的分布式锁来避免冲突,这是一个很好的解决方案。但是分布式锁是一种悲观锁,那是不是可以使用乐观锁的方式来解决冲突呢?
有这样一种情况,假如多个用户进行并发操作时,需要将值取出然后操作再回写到对应键的值上,这可能会导致最终的值不是正确的值。这里一种解决办法是分布式锁(悲观所),另一种就是Watch(乐观锁)。
watch命令会在事务开始之前看住 1 个或多个指定变量,当服务器收到了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec 指令就会返回 null 回复告知客户端事务执行失败,这个时候客户端一般会选择重试。(watch必须再事务之前执行,不能再事务中执行)
1 1 127.0.0.1:6379> set 'test' 1
2 2 OK
3 3 127.0.0.1:6379> watch test
4 4 OK
5 5 127.0.0.1:6379> INCR test # watch之后改变test的值
6 6 (integer) 2
7 7 127.0.0.1:6379> multi # 开始事务
8 8 OK
9 9 127.0.0.1:6379> INCR test # 改变test的值
10 10 QUEUED
11 11 127.0.0.1:6379> exec # 执行事务
12 12 (nil) # 返回空值,事务执行失败。
13 13
14 14
15 15 127.0.0.1:6379> set 'test_2' 1 # 设置test-2
16 16 OK
17 17 127.0.0.1:6379> watch test_2 # 监听test_2
18 18 OK
19 19 127.0.0.1:6379> multi
20 20 OK
21 21 127.0.0.1:6379> INCR test_2 #修改test_2的值
22 22 QUEUED
23 23 127.0.0.1:6379> exec # 执行成功
24 24 1) (integer) 3
在py客户端中对于一个值的修改我们可以采用这样形式
1 import redis
2
3 def modify_test(client, key):
4 while True:
5 client.watch(key)
6 value = int(client.get(key))
7 value += 1 # 修改值
8 pipe = client.pipeline(transaction=True)
9 pipe.multi()
10 pipe.set(key, value)
11 try:
12 pipe.execute()
13 break # 修改成功
14 except redis.WatchError:
15 continue # 值已经被修改,执行失败
16 return int(client.get(key)) # 重新获取值
17
18
19 client = redis.StrictRedis()
20 key = "test"
21 client.setnx(key, 5) # setnx 做初始化
22 print (modify_test(client, key))