前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、引入

为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,Redis 也不 例外。Redis 的事务使用非常简单

**不同于关系数据库,我们无须理解那么多复杂的事务模 型,就可以直接使用。不过也正是因为这种简单性,它的事务模型很不严格,这要求我们不 能像使用关系数据库的事务一样来使用 Redis **

Redis事务不保证原子性,且没有回滚。存在事务中任意命令执行失败,其余的命令仍会被执行。

2、Redis 事务的基本使用

每个事务的操作都有 begincommitrollbackRedis 在形式上看起来也差不多,分别是 multi/exec/discard

multiexec这两个命令,一个代表事物开始,一个代表事物结束 。discard 命了可以用来放弃事务

2.1、命令正常

**redis是不支持回滚的特性,只要语法没有问题,中间即使有的命令报错,其他的命令也一定会执行**

举例:社交网站上用户A关注了用户B,那么需要再用户A的关注表中添加用户B,用户B的表中添加粉丝A 这是两个操作,只能同时失败或者成功

客户端1
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:A:flow user:B
QUEUED
127.0.0.1:6379> sadd user:B:fans user:A
QUEUED



客户端2
这个时候,如果其他客户端执行查找命令,则会提示在队列中,不能查找成功
127.0.0.1:6379> sismember user:A:flow user:B
QUEUED
127.0.0.1:6379> smembers user:A:flow 
QUEUED
127.0.0.1:6379> 




客户端1
只有执行exec 上面两条命令才能执行成功
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> 


2.1、命令错误

1.1.1、语法命令错误不会提交事务

事物中间命令出现语法错误,事物exec不能正常执行

127.0.0.1:6379> smembers user:A:flow user:B # 语法错误。此处应该使用 sismember
(error) ERR wrong number of arguments for 'smembers' command
127.0.0.1:6379> smembers user:A:flow 
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> MULTI

1.1.2、运行时错误会提交事务

没有语法错误,当命令格式正确,而因为操作数据结构引起的错误,则该命令执行出现错误,而其之前和之后的命令都会被正常执行。

127.0.0.1:6379> set num1 1
127.0.0.1:6379> set key healerjean
127.0.0.1:6379> set num2 2
QUEUED

127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr num1
QUEUED
127.0.0.1:6379> incr key
QUEUED
127.0.0.1:6379> incr num2
QUEUED
127.0.0.1:6379> exec
1) (integer) 2
2) (error) ERR value is not  an integer or out of range
3) (integer) 3

3、事务包含多条命令执行优化

问题1: Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO 时间也会线性增长。怎么能优化一下呢?

答案:通常 Redis 的客户端在执行 事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作。比如我 们在使用 PythonRedis 客户端时执行事务时是要强制使用 pipeline

pipe = redis.pipeline(transaction=true) 
pipe.multi()
pipe.incr("books")
pipe.incr("books")
values = pipe.execute()

4、Watch

考虑到一个业务场景,Redis 存储了我们的账户余额数据,它是一个整数。现在有两个 并发的客户端要对账户余额进行修改操作,这个修改不是一个简单的 incrby 指令,而是要对余额乘以一个倍数。。我们需要先取出余额然后在 内存里乘以倍数,再将结果写回 Redis

问题1:会出现并发问题,因为有多个客户端会并发进行操作。

答案:我们可以通过 Redis 的分布 式锁来避免冲突 ,这是一个很好的解决方案。分布式锁是一种悲观锁,那可以用乐观锁来解决吗?

问题2:如果使用乐观锁来解决上面这个问题呢?

答案:Redis 提供了这种 watch 的机制,它就是一种乐观锁。有了 watch 我们又多了一种可以 用来解决并发修改的方法。

Redis提供了watch命令来解决这个问题 ,作为WATCH命令的参数的键会受到Redis的监控,Redis能够检测到它们的变化。在执行EXEC命令之前,如果Redis检测到至少有一个键被修改了,那么整个事务便会中止运行,然后EXEC命令会返回一个nil值,提醒用户事务运行失败。

# 客户端1
> watch books OK
> incr books (integer) 1 # 被修改了



# 客户端2
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败 (nil)
(nil)

5、为什么 Redis 的事务不能支持回滚

首先需要明确的是:没有人能解决程序员自己的错误

exec执行后所产生的错误中,即使事务中有某个/某些命令在执行时产生了错误,事务中的其他命令仍然会继续执行。Redis在事务失败时不进行回滚,而是继续执行余下的命令。

Redis的这种设计原则是:失败的命令不是Redis所致,而是由编程错误造成的,这样错误应该在开发的过程中被发现,生产环境中不应出现的错误。而Redis能够保证正确的命令一定会被执行。

ContactAuthor