美文网首页
Redis 事务

Redis 事务

作者: code_solve | 来源:发表于2020-03-09 16:57 被阅读0次

    原理

    为了确保连续多个操作的原子性,
    一个成熟的数据库通常都会有事务支持,
    Redis 也不例外。
    Redis 的事务使用非常简单,
    不同于关系数据库,
    我们无须理解那么多复杂的事务模型,
    就可以直接使用。
    不过也正是因为这种简单性,
    它的事务模型很不严格,
    这要求我们不能像使用关系数据库的事务一样来使用 Redis。

    Redis 事务的基本使用

    每个事务的操作都有 begin、commit 和 rollback,
    begin 指示事务的开始,commit 指示事务的提交,rollback 指示事务的回滚。
    它大致的形式如下。

    begin();
    try {
    command1();
    command2();
    ....
    commit();
    } catch(Exception e) {
    rollback();
    }
    

    Redis 在形式上看起来也差不多,
    分别是 multi/exec/discard。
    multi 指示事务的开始,
    exec 指示事务的执行,
    discard 指示事务的丢弃。

    >multi
    OK
    > incr books
    QUEUED
    > incr books
    QUEUED
    > exec
    (integer) 1
    (integer) 2

    上面的指令演示了一个完整的事务过程,
    所有的指令在 exec 之前不执行,
    而是缓存在服务器的一个事务队列中,
    服务器一旦收到 exec 指令,
    才开执行整个事务队列,
    执行完毕后一次性返回所有指令的运行结果。
    因为 Redis 的单线程特性,
    它不用担心自己在执行队列的时候被其它指令打搅,
    可以保证他们能得到的「原子性」执行。

    QUEUED 是一个简单字符串,
    同 OK 是一个形式,
    它表示指令已经被服务器缓存到队列里了。

    原子性

    事务的原子性是指要么事务全部成功,
    要么全部失败,
    那么 Redis 事务执行是原子性的么?

    下面我们来看一个特别的例子。
    > multi
    OK
    > set books iamastring
    QUEUED
    > incr books
    QUEUED
    > set poorman iamdesperate
    QUEUED
    > exec
    1) OK
    2) (error) ERR value is not an integer or out of range
    3) OK
    > get books
    "iamastring"
    > get poorman
    "iamdesperate

    上面的例子是事务执行到中间遇到失败了,
    因为我们不能对一个字符串进行数学运算,
    事务在遇到指令执行失败后,
    后面的指令还继续执行,
    所以 poorman 的值能继续得到设置。

    到这里,你应该明白 Redis 的事务根本不能算「原子性」,
    而仅仅是满足了事务的「隔离性」,
    隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。

    discard(丢弃)

    Redis 为事务提供了一个 discard 指令,
    用于丢弃事务缓存队列中的所有指令,
    在 exec 执行之前。

    > get books
    (nil)
    > multi
    OK
    > incr books
    QUEUED
    > incr books
    QUEUED
    > discard
    OK
    > get books
    (nil)

    我们可以看到 discard 之后,
    队列中的所有指令都没执行,
    就好像 multi 和 discard 中间的所有指令从未发生过一样。

    优化

    上面的 Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,
    当一个事务内部的指令较多时,
    需要的网络 IO 时间也会线性增长。
    所以通常 Redis 的客户端在执行事务时都会结合 pipeline 一起使用,
    这样可以将多次 IO 操作压缩为单次 IO 操作。
    比如我们在使用 Python 的 Redis 客户端时执行事务时是要强制使用 pipeline 的。

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

    Watch

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

    这就会出现并发问题,
    因为有多个客户端会并发进行操作。
    我们可以通过 Redis 的分布式锁来避免冲突,
    这是一个很好的解决方案。
    分布式锁是一种悲观锁,
    那是不是可以使用乐观锁的方式来解决冲突呢?

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

    while True:
    do_watch()
    commands()
    multi()
    send_commands()
    try:
    exec()
    break
    except WatchError:
    continue
    

    watch 会在事务开始之前盯住 1 个或多个关键变量,
    当事务执行时,
    也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时,
    Redis 会检查关键变量自 watch 之后,
    是否被修改了 (包括当前事务所在的客户端)。
    如果关键变量被人动过了,
    exec 指令就会返回 null 回复告知客户端事务执行失败,
    这个时候客户端一般会选择重试。

    > watch books
    OK
    > incr books # 被修改了
    (integer) 1
    > multi
    OK
    > incr books
    QUEUED
    > exec # 事务执行失败
    (nil)
    

    当服务器给 exec 指令返回一个 null 回复时,
    客户端知道了事务执行是失败的,
    通常客户端 (redis-py) 都会抛出一个 WatchError 这种错误,
    不过也有些语言 (jedis) 不会抛出异常,
    而是通过在 exec 方法里返回一个 null,
    这样客户端需要检查一下返回结果是否为 null 来确定事务是否执行失败。

    注意事项
    Redis 禁止在 multi 和 exec 之间执行 watch 指令,
    而必须在 multi 之前做好盯住关键变量,
    否则会出错。

    思考题

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

    本文转载自 Redis 深度历险:核心原理与应用实践 - 老錢 - 掘金小册

    相关文章

      网友评论

          本文标题:Redis 事务

          本文链接:https://www.haomeiwen.com/subject/cdbpdhtx.html