什么是事务
对事务的说法已经说得不能再多了,简单来说就是示一组动作,要么全部执行,要么全部不执行。
如在社交网站上用户 A 关注了用户 B,那么需要在用户 A 的关注表中加入用户 B,并且在用户 B 的粉丝表中添加用户 A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。
Redis 中的事务
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
即 Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis 事务没有隔离级别的概念,所以它不能保证原子性,即:
Redis 中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis 事务相关命令
Redis 事务包含了三个阶段:
主要有以下几个命令:
命令 | 说明 |
---|
multi | 开启事务 | exec | 执行事务 | watch key1 key2 … | 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 ) | discard | 取消事务,放弃事务块中的所有命令 | unwatch | 取消 watch 对所有 key 的监控 |
如用上述关注的问题:
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> sadd user:a:follow b
QUEUED
127.0.0.1:6379(TX)> sadd user:b:fans a
QUEUED
127.0.0.1:6379(TX)>
可以看到 sadd 命令此时的返回结果是 QUEUED,代表命令并没有真正执行,而是暂时保存在 Redis 中的一个缓存队列。
若此时另外一个客户端执行以下命令,实际上是没有数据的,应该返回 0:
127.0.0.1:6379> SISMEMBER user:a:follow b
(integer) 0
只有当 exec 执行后,用户 A 关注用户 B 的行为才算完成,如下所示 exec 返回的两个结果对应 sadd 命令:
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> sadd user:a:follow b
QUEUED
127.0.0.1:6379(TX)> sadd user:b:fans a
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 1
2) (integer) 1
127.0.0.1:6379>
此时对于另外一个客户端:
127.0.0.1:6379> SISMEMBER user:a:follow b
(integer) 0
127.0.0.1:6379> SISMEMBER user:a:follow b
(integer) 1
127.0.0.1:6379>
当然,如果事务中的命令出现错误,Redis 的处理机制也不同,我们在分以下一个案例来熟悉一下。
正常执行
取消事务
命令错误
若在事务执行过程中,存在命令错误,则执行EXEC命令时,所有命令都不会执行。
语法错误
若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),如对字符类型自增,则执行 EXEC 命令时,其他正确命令会被执行,错误命令抛出异常。
可以看到,尽管存在失败的命令,但是其它命令还是执行成功了,也就是说部分成功,部分失败,而整个事务并没有回滚。
watch
有些应用场景需要在事务之前,确保事务中的 key 没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis 提供了 watch 命令来解决这类问题。
最常见的就是以下 2 种情况:
- 多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;
- 或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。
watch 指令在 redis 事务中提供了 CAS 的行为。为了检测被 watch 的 keys 在是否有多个客户端同时改变引起冲突,这些 keys 将会被监控。如果至少有一个被监控的 key 在执行 exec 命令前被修改,整个事物将会回滚,不执行任何动作,从而保证原子性操作,并且执行 exec 会得到 null 的回复。
语法
WATCH key [key …]
1?? 不使用 watch
client1 | client2 | 说明 |
---|
127.0.0.1:6379> get name "ayue" 127.0.0.1:6379> get age "20" | 127.0.0.1:6379> get name "ayue" 127.0.0.1:6379> get age "20" | 初始值 | 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> INCR age QUEUED 127.0.0.1:6379(TX)> set name a QUEUED | | 客户端 1 开启事务 1. age + 1 2. name 改为 a 此时未提交 | | 127.0.0.1:6379> incr age (integer) 21 | 此时客户端 2 age自增 | 127.0.0.1:6379(TX)> exec 1) (integer) 22 2) OK 127.0.0.1:6379> get name "a" | | 客户端 1 age 因 21 , 但实际为 22, 因为被客户端 2 抢先修改了 因此导致了数据的不一致性 |
为了解决上面的问题,redis 引入了乐观锁的机制,即 watch 命令。
2?? 使用 watch
client1 | client2 | 说明 |
---|
127.0.0.1:6379> get name "ayue" 127.0.0.1:6379> get age "20" | 127.0.0.1:6379> get name "ayue" 127.0.0.1:6379> get age "20" | 初始值 | 127.0.0.1:6379> watch name age OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> incr age QUEUED 127.0.0.1:6379(TX)> set name a QUEUED | | 客户端 1 使用命令监视 name age | | 127.0.0.1:6379> incr age (integer) 21 | 此时客户端 2 age自增 | 127.0.0.1:6379(TX)> exec (nil) 127.0.0.1:6379> get age "21" 127.0.0.1:6379> get name "ayue" | | 可以发现客户端1 的事务回滚了 并返回了 nil |
Pipeline 和事务的区别
前面讲过 Pipeline 也是对一组命令的打包,它们的区别如下:
-
pipeline 是客户端的行为,对于服务器来说是透明的,可以认为服务器无法区分客户端发送来的查询命令是以普通命令的形式还是以 pipeline 的形式发送到服务器的; -
事务则是实现在服务器端的行为,用户执行 MULTI 命令时,服务器会将对应这个用户的客户端对象设置为一个特殊的状态,在这个状态下后续用户执行的查询命令不会被真的执行,而是被服务器缓存起来,直到用户执行 EXEC 命令为止,服务器会将这个用户对应的客户端对象中缓存的命令按照提交的顺序依次执行。 -
应用 pipeline 可以提服务器的吞吐能力,并提高 Redis 处理查询请求的能力。 但是这里存在一个问题,当通过 pipeline 提交的查询命令数据较少,可以被内核缓冲区所容纳时,Redis 可以保证这些命令执行的原子性。然而一旦数据量过大,超过了内核缓冲区的接收大小,那么命令的执行将会被打断,原子性也就无法得到保证。因此 pipeline 只是一种提升服务器吞吐能力的机制,如果想要命令以事务的方式原子性的被执行,还是需要事务机制,或者使用更高级的脚本功能以及模块功能。 -
可以将事务和 pipeline 结合起来使用,减少事务的命令在网络上的传输时间,将多次网络 IO 缩减为一次网络 IO。
总结
Redis 提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了 Redis 的keepit simple的特性,也可以通过 Lua 脚本实现事务的相关功能,且功能要强大很多。
|