因为 Redis 并没有提供能够一次弹出多个列表元素的命令,所以为了方便地执行这一任务,用户可能会写出代码清单 13-3 所示的代码。


    代码清单 13-3 不安全的 mlpop() 实现:/pipeline-and-transaction/unsafe_mlpop.py

    1. def mlpop(client, list_key, number):
    2. # 用于储存被弹出元素的结果列表
    3. items = []
    4. for i in range(number):
    5. # 执行 LPOP 命令,弹出一个元素
    6. poped_item = client.lpop(list_key)
    7. # 将被弹出的元素追加到结果列表末尾
    8. items.append(poped_item)
    9. # 返回结果列表
    10. return items

    mlpop() 函数通过将多条 LPOP 命令发送至服务器来达到弹出多个元素的目的。遗憾的是,这个函数并不能保证它发送的所有 LPOP 命令都会被服务器执行:如果服务器在执行多个 LPOP 命令的过程中下线了,那么 mlpop() 发送的这些 LPOP 命令将只有一部分会被执行。

    举个例子,如果我们执行调用 mlpop(client, "lst", 3) ,尝试从 "lst" 列表中弹出三个元素,那么 mlpop() 将向服务器连续发送三个 LPOP 命令,但如果服务器在顺利执行前两个 命令之后因为故障下线了,那么 "lst" 列表将只有两个元素会被弹出。

    需要注意的是,即使我们使用上一节介绍的流水线特性,把多条 LPOP 命令打包在一起发送,也不能保证所有命令都会被服务器执行:这是因为流水线只能保证多条命令会一起被发送至服务器,但它并不保证这些命令都会被服务器执行。

    为了实现一个正确且安全的 mlpop() 函数,我们需要一种能够让服务器将多个命令打包起来一并执行的技术,而这正是本节将要介绍的事务特性:

    • 事务可以将多个命令打包成一个命令来执行,当事务成功执行时,事务中包含的所有命令都会被执行;

    • 相反地,如果事务没有成功执行,那么它包含的所有命令都不会被执行。

    通过使用事务,用户可以保证自己想要执行的多个命令要么全部都被执行,要么就一个都不执行。以 mlpop() 函数为例,通过使用事务,我们可以保证被调用的多个 LPOP 命令要么全部都执行,要么就一个都不执行,从而杜绝了只有其中一部分 LPOP 命令被执行的情况出现。

    本节接下来的内容将会介绍 Redis 事务特性的使用方法以及相关事项,至于事务版本 mlpop() 函数的具体实现则会留到下一节再行介绍。

    用户可以通过执行 MULTI 命令来开启一个新的事务,这个命令在成功执行之后将返回 OK

    1. MULTI

    在一般情况下,除了少数阻塞命令之外,用户键入到客户端里面的数据操作命令总是会立即执行:

    1. redis> SET title "Hand in Hand"
    2. OK
    3.  
    4. (integer) 3
    5.  
    6. redis> RPUSH numbers 123 456 789
    7. (integer) 3

    比如说,以下代码就展示了在 MULTI 命令执行之后,将一个 SET 命令、一个 SADD 命令和一个 RPUSH 命令放入到事务队列里面的例子:

    正如代码所示,服务器在把客户端发送的命令放入到事务队列之后,会向客户端返回一个 QUEUED 作为结果。

    其他信息

    EXEC:执行事务

    在使用 MULTI 命令开启事务并将任意多个命令放入到事务队列之后,用户就可以通过执行 EXEC 命令来执行事务了:

    1. EXEC

    当事务成功执行时,EXEC 命令将返回一个列表作为结果,这个列表会按照命令的入队顺序依次包含各个命令的执行结果。

    作为例子,以下代码展示了一个事务从开始到执行的整个过程:

    1. redis> MULTI -- 1) 开启事务
    2. OK
    3.  
    4. redis> SET title "Hand in Hand" -- 2) 命令入队
    5. QUEUED
    6.  
    7. redis> SADD fruits "apple" "banana" "cherry"
    8. QUEUED
    9.  
    10. redis> RPUSH numbers 123 456 789
    11. QUEUED
    12.  
    13. redis> EXEC -- 3) 执行事务
    14. 1) OK -- SET 命令的执行结果
    15. 2) (integer) 3 -- SADD 命令的执行结果
    16. 3) (integer) 3 -- RPUSH 命令的执行结果

    其他信息

    如果用户在开启事务之后,不想要执行事务而是想要放弃事务,那么只需要执行以下命令即可:

    1. DISCARD

    DISCARD 命令会清空事务队列中已有的所有命令,并让客户端退出事务模式,最后返回 OK 表示事务已被取消。

    以下代码展示了一个使用 DISCARD 命令放弃事务的例子:

    其他信息

    事务的安全性

    在对数据库的事务特性进行介绍时,人们一般都会通过数据库对 ACID 性质的支持程度去判断数据库的事务是否安全。

    具体来说,Redis 的事务总是具有 ACID 性质中的 A、C、I 性质:

    • 原子性(Atomic):如果事务成功执行,那么事务中包含的所有命令都会被执行;相反,如果事务执行失败,那么事务中包含的所有命令都不会被执行。

    • 一致性(Consistent):Redis 服务器会对事务及其包含的命令进行检查,确保无论事务是否执行成功,事务本身都不会对数据库造成破坏。

    除此之外,当 Redis 服务器运行在特定的持久化模式之下时,Redis 的事务也具有 ACID 性质中的 D 性质:

    稍后的《持久化》一章将对事务的耐久性做补充说明。

    因为事务在执行时会独占服务器,所以用户应该避免在事务里面执行过多命令,更不要将一些需要大量计算的命令放入到事务里面,以免造成服务器阻塞。

    流水线与事务

    正如前面所言,流水线与事务虽然在概念上有些相似,但是在作用上却并不相同:流水线的作用是将多个命令打包然后一并发送至服务器,而事务的作用则是将多个命令打包然后让服务器一并执行它们。

    因为 Redis 的事务在 EXEC 命令执行之前并不会产生实际效果,所以很多 Redis 客户端都会使用流水线去包裹事务命令,并将入队的命令缓存在本地,等到用户键入 EXEC 命令之后,再将所有事务命令通过流水线一并发送至服务器,这样客户端在执行事务时就可以达到“打包发送,打包执行”的最优效果。

    本书使用的 redis-py 客户端就是这样处理事务命令的客户端之一,当我们使用 pipeline() 方法开启一个事务时,redis-py 默认将使用流水线包裹事务队列中的所有命令。

    举个例子,对于以下代码来说:

    1. >>> from redis import Redis
    2. >>> client = Redis(decode_responses=True)
    3. >>> transaction = client.pipeline() # 开启事务
    4. >>> transaction.set("title", "Hand in Hand") # 将命令放入事务队列
    5. Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
    6. >>> transaction.sadd("fruits", "apple", "banana", "cherry")
    7. Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
    8. >>> transaction.rpush("numbers", "123", "456", "789")
    9. Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
    10. >>> transaction.execute() # 执行事务
    11. [True, 3, 3L]

    在执行 transaction.execute() 调用时,redis-py 将通过流水线向服务器发送以下命令:

    1. MULTI
    2. SET title "Hand in Hand"
    3. SADD fruits "apple" "banana" "cherry"
    4. RPUSH numbers "123" "456" "789"
    5. EXEC

    这样的话,无论事务包含了多少个命令,redis-py 也只需要与服务器进行一次网络通讯。

    另一方面,如果用户只需要用到流水线特性而不是事务特性,那么可以在调用 pipeline() 方法时通过 transaction=False 参数显式地关闭事务特性,就像这样:

    1. >>> pipe = client.pipeline(transaction=False) # 开启流水线
    2. >>> pipe.set("download_counter", 10086) # 将命令放入流水线队列
    3. Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
    4. >>> pipe.get("download_counter")
    5. Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
    6. >>> pipe.hset("user::123::profile", "name", "peter")
    7. Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>
    8. >>> pipe.execute() # 将流水线队列中的命令打包发送至服务器
    9. [True, '10086', 1L]

    在执行 pipe.execute() 调用时,redis-py 将通过流水线向服务器发送以下命令:

    因为这三个命令并没有被事务包裹,所以客户端只保证它们会一并被发送至服务器,至于这些命令在何时会以何种方式执行则由服务器本身决定。