zoukankan      html  css  js  c++  java
  • 5. 详解Redis中的事务

    什么是事务?

    事务指的是提供一种将多个命令打包放在一个队列里,然后一次性按顺序执行的机制,并且保证服务器只有在执行完事务中的所有命令后,才会继续处理此客户端的其他命令,不会被其他的命令插队。

    一句话总结就是:一个队列中,一次性、顺序性、排他性地执行一系列命令

    事务也是其他关系型数据库所必备的基础功能,以支付的场景为例,正常情况下只有正常消费完成之后,才会减去账户余额。但如果没有事务的保障,可能会发生消费失败了,但依旧会把账户的余额给扣减了,我想这种情况应该任何人都无法接受吧?所以事务是数据库中一项非常重要的基础功能。

    事务基本使用

    事务一般分为以下三个阶段:

    • 1. 开启事务:Begin Transaction
    • 2. 正确执行业务代码,提交事务:Commit Transaction
    • 3. 业务处理中出现异常,回滚事务:Rollback Transaction

    Redis 中的事务从开始到结束也是要经历三个阶段:

    • 1. 开启事务
    • 2. 命令依次进入队列
    • 3. 提交事务/放弃事务

    redis的事务的相关命令有以下几种:

    • 1. multi:标记一个事务块的开始,或者说开启一个事务。然后输出的所有命令都不会立刻执行,而是会按照顺序进入队列中。
    • 2. exec:按照顺序执行队列中的所有命令。
    • 3. discard:取消事务,放弃执行事务块内的所有命令
    • 4. watch:监视一个或者多个key,如果在事务执行前(多个key的任意一个)key被其他命令所改动,那么事务将被打断。
    • 5. unwatch:取消对所有key的监视。

    Redis中的基本事务操作

    开启一个事务

    multi 命令用于开启事务,实现代码如下:

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379>
    

    multi 命令可以让客户端从非事务模式状态,变为事务模式状态,如下图所示:

    注意:multi 命令不能嵌套使用,如果已经开启了事务的情况下,再执行 multi 命令,会提示如下错误:(error) ERR MULTI calls can not be nested

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> multi
    (error) ERR MULTI calls can not be nested
    127.0.0.1:6379> 
    

    当客户端是非事务状态时,使用 multi 命令,客户端会返回结果 OK,如果客户端已经是事务状态,再执行 multi 命令会 multi 命令不能嵌套的错误,但不会终止客户端为事务的状态,如下图所示:

    此时依旧是处于事务开启的一个状态。

    命令入队

    客户端进入事务状态之后,执行的所有常规 Redis 操作命令(非触发事务执行或放弃和导致入列异常的命令)会依次入列,命令入列成功后会返回 QUEUED,如下代码所示:

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> multi
    (error) ERR MULTI calls can not be nested  # 不会终止事务
    127.0.0.1:6379> set name hanser
    QUEUED
    127.0.0.1:6379> get name
    QUEUED
    127.0.0.1:6379>
    

    执行流程如下图所示:

    注意:命令会按照先进先出(FIFO)的顺序出入列,也就是说事务会按照命令的入列顺序,从前往后依次执行。

    提交/放弃 事务

    提交或者说执行事务的命令是exec,放弃事务的命令是discard。

    exec提交事务:

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> multi
    (error) ERR MULTI calls can not be nested
    127.0.0.1:6379> set name hanser
    QUEUED
    127.0.0.1:6379> get name
    QUEUED
    127.0.0.1:6379> exec  # 队列中的命令依次执行
    1) OK
    2) "hanser"
    127.0.0.1:6379> 
    

    另外在事务中命令在提交事务之后,如果成功执行,那么影响是全局的,我们再举个栗子:

    127.0.0.1:6379> set name hanser  # 设置name为hanser
    OK
    127.0.0.1:6379> get name  # 获取name,显然没问题
    "hanser"
    127.0.0.1:6379> multi   # 开启事务
    OK
    127.0.0.1:6379> set name yousa  # 在事务中设置name为yousa
    QUEUED
    127.0.0.1:6379> get name
    QUEUED
    127.0.0.1:6379> exec  # 执行事务,get name的结果为yousa显然没问题
    1) OK
    2) "yousa"
    127.0.0.1:6379> get name  
    "yousa"  # 但是我们说事务中的命令的影响是全局的,即便事务结束,里面执行的命令在外部也是生效的
    127.0.0.1:6379> 
    

    discard放弃事务:

    127.0.0.1:6379> set name hanser
    OK
    127.0.0.1:6379> multi 
    OK
    127.0.0.1:6379> set name yousa
    QUEUED
    127.0.0.1:6379> get name
    QUEUED
    127.0.0.1:6379> discard  # 取消事务,里面的命令根本没有执行
    OK
    127.0.0.1:6379> get name  # 所以外部的name还是hanser
    "hanser"
    127.0.0.1:6379> 
    

    执行流程如下图所示:

    事务错误&回滚

    事务执行中的错误分为以下三类:

    • 1. 执行时才会出现的错误(简称:执行时错误);
    • 2. 入队时错误,不会终止整个事务;
    • 3. 入队时错误,会终止整个事务。

    1. 执行时错误:

    127.0.0.1:6379> set name hanser  # 设置name
    OK
    127.0.0.1:6379> get name  # 获取name
    "hanser"
    127.0.0.1:6379> multi   # 开启事务
    OK
    127.0.0.1:6379> incr name  # name自增1,显然这是不合法的,因为name不是数字
    QUEUED
    127.0.0.1:6379> set name yousa  # 再次设置name
    QUEUED
    127.0.0.1:6379> exec  # 我们看到事务里面第一条命令执行失败,但是第二条执行成功了
    1) (error) ERR value is not an integer or out of range
    2) OK 
    127.0.0.1:6379> get name  # 事务结束后,获取name发现被修改了
    "yousa"
    127.0.0.1:6379> 
    

    从以上结果来看,即使事务队列中某个命令在执行期间出现了错误,事务也会继续执行,直到事务队列中所有命令都执行完成。

    2. 不会导致事务结束的入队时错误:

    127.0.0.1:6379> set name hanser
    OK
    127.0.0.1:6379> get name
    "hanser"
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> multi  # 在入队时就已经出现了错误,但是事务依旧没有结束
    (error) ERR MULTI calls can not be nested
    127.0.0.1:6379> set name yousa  # 修改name
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    127.0.0.1:6379> get name  # name被修改
    "yousa"
    127.0.0.1:6379> 
    

    可以看出,重复执行 multi 会导致入列错误,但不会终止事务,最终查询的结果表示事务执行成功了。除了重复执行 multi 命令,还有在事务状态下执行 watch 也是同样的效果,下文会详细讲解关于 watch 的内容。

    3. 会导致事务结束的入队时错误:

    127.0.0.1:6379> multi  # 开启一个事务
    OK
    127.0.0.1:6379> set name1 hanser  # 设置name1
    QUEUED
    127.0.0.1:6379> dadsadsadsa  # 输入一条不存在的命令
    (error) ERR unknown command `dadsadsadsa`, with args beginning with: 
    127.0.0.1:6379> set name2 yousa  # 设置name2
    QUEUED
    127.0.0.1:6379> exec  # 执行,提示我们由于前面的错误,导致整个事务被取消了
    (error) EXECABORT Transaction discarded because of previous errors.
    127.0.0.1:6379> get name1  # name1为nil
    (nil)
    127.0.0.1:6379> get name2  # name2为nil,所以不管错误在事务的哪个地方,只要出现了,整个事务就完蛋了
    (nil)
    127.0.0.1:6379> 
    

    所以我们看到错误主要可以分为两种:一种是事务执行时才会发现的错误;另一种是在入队的时候就能发现的错误。

    • 执行时出现的错误,不会影响事务队列中其它的命令;即使某条命令失败,但其它命令依旧可以正常执行。
    • 入队发现的错误,如果是multi、watch这种错误也不会终止事务,只是不会让它入队;但如果是命令不符合Redis的规则,那么这种错误就属于类似于编程语言的语法错误,直接编译时报出语法错误,没必要等到执行了,所以在Redis中的表现就是整个事务都废弃掉,里面的命令一条也不会执行。

    从执行时错误的例子中我们可以看到,Redis是不支持事务回滚的。

    而不支持事务回滚的原因,Redis作者提出了两个理由:

    • 作者认为Redis事务在执行时,错误通常是编程错误造成的,这种错误通常只会出现在开发环境中,而很少在生产环境中出现,所以它认为没有必要为Redis开发事务回滚功能。
    • 不支持事务回滚是因为这种复杂的功能和Redis追求的简单高效的设计主旨不符合。

    监控

    redis的监控会使用到锁机制,而锁分为悲观锁和乐观锁。

    类似于mysql里面的"表锁"和"行锁"。"表锁"就是为了保证数据的一致性,将整张表锁上,这样就只能一个人修改,好比进卫生间,进去之后就把大门锁上了,但这样的结果也可想而知,虽然数据的一致性、安全性好,但是并发性会极差,因为其他人进不去了。比如一张有20万条记录的表,但是你只修改第520行,而另一个哥们修改第250行,本来两者不冲突,但是你把整个表都锁了,那就意味这后面的老铁只能排队了,这样显然效率不高。于是就出现了"行锁","行锁"在mysql中,就类似于表中有一个版本号的字段,假设有一条记录的版本号为1,A和B同时修改这条记录,那么一旦提交,就会改变那个版本号,假设变为2。如果A先提交了,那么数据库中对应记录的版本号已经变了,但是B对应的版本号还是之前的,那么提交之后会立即报错,这样就知道这条记录被人修改了,需要重新获取对应版本号的记录。

    悲观锁:

    pessimistic lock,顾名思义,就是很悲观,每次拿数据的时候都会认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿到这个数据就会block住,直到拿到锁。

    乐观锁:

    optimistic lock,顾名思义,就是很乐观,每次拿数据的时候都会认为别人不会修改,所以每次拿数据的时候都不会上锁。但是在更新数据的时候会判断一下在此期间别人有没有去更新这条数据,可以使用版本号等机制。乐观锁使用于多读的应用类型,这样可以提高吞吐量。乐观锁策略就是:提交版本必须大于记录的当前版本才能更新

    而watch 命令则是用于客户端并发情况下,为事务提供一个乐观锁(CAS,Check And Set),也就是可以用 watch 命令来监控一个或多个变量,如果在事务的过程中,某个监控项被修改了,那么整个事务就会终止执行

    下面就来演示一下,首先watch是需要搭配multi事务来使用的。一般是先watch key,然后开启事务对key操作。

    127.0.0.1:6379> set money 100
    OK
    127.0.0.1:6379> watch money  # 监控
    OK
    127.0.0.1:6379> multi  # 开启事务
    OK
    127.0.0.1:6379> decrby money 20  # money自减20
    QUEUED
    127.0.0.1:6379> exec  # 执行
    1) (integer) 80
    127.0.0.1:6379> get money  # 获取
    "80"
    127.0.0.1:6379>
    

    上面执行的结果显然没有问题,但是往下看。

    127.0.0.1:6379> flushdb  # 清空db
    OK
    127.0.0.1:6379> set money 100  # 设置money为100
    OK
    127.0.0.1:6379> watch money  # 监控
    OK
    127.0.0.1:6379> set money 200  # 但是在开启事务之前将money修改了
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> incr money
    QUEUED
    127.0.0.1:6379> exec  # 此时执行会返回一个nil
    (nil)
    127.0.0.1:6379> get money  # money是我们开启事务之前修改的200
    "200"
    127.0.0.1:6379> 
    127.0.0.1:6379> 
    127.0.0.1:6379>   
    127.0.0.1:6379> get name
    (nil)
    127.0.0.1:6379> watch name  # 监控一个不存在的key也是可以的
    OK
    127.0.0.1:6379> set name hanser  # 开启事务之前设置
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set name yousa
    QUEUED
    127.0.0.1:6379> exec  # 执行已经不会成功
    (nil)
    127.0.0.1:6379> get name  # name依旧是之前的hanser
    "hanser"
    127.0.0.1:6379>
    

    因此我们可以得出一个结论,那就是一旦监视了key,那么这个key如果想改变,则需要开启一个事务,在事务中修改,然后exec执行来改变这个key。如果在事务没有执行之前,将watch监视的key修改了,那么不好意思,事务都会失效。

    那如果是,先开启的事务,再在另一个终端中把key修改了,会怎么样呢?我们来试一下。

    127.0.0.1:6379> flushdb
    OK
    127.0.0.1:6379> set money 100  # 设置money为100
    OK
    127.0.0.1:6379> watch money  # 开启监控
    OK
    127.0.0.1:6379> multi  # 开启事务
    OK
    127.0.0.1:6379> set money 120  # 设置money为120
    QUEUED
    127.0.0.1:6379> exec  # 但是在事务开启后、事务提交前,我在另一个终端将money设置成了250
    (nil)  # 看到此时结果依旧为nil
    127.0.0.1:6379> get money  # 获取money,是我们在另一个终端中设置的250。
    "250"
    127.0.0.1:6379>
    

    正如mysql的行锁一样,两个人都可以对同一条记录做修改,但是一个人先改好之后,另一个人提交就会失败,必须查找到对应的版本号,然后重新查找对应记录,修改才能提交。这在redis中如何实现呢,答案很简单,如果开始事务之前被修改了,那么把取消监视就好了。

    127.0.0.1:6379> flushdb
    OK
    127.0.0.1:6379> set name hanser  # 设置name
    OK
    127.0.0.1:6379> watch name  # 监控name
    OK
    127.0.0.1:6379> set name yousa  # 再次设置name
    OK
    127.0.0.1:6379> get name # 从结果来看,这个name对应的值已经被修改了。如果此时开启事务,那么事务必然无效。
    "yousa"
    127.0.0.1:6379> unwatch  # 因此先取消监视
    OK
    127.0.0.1:6379> watch name  # 然后重新监视
    OK
    127.0.0.1:6379> multi  # 开启事务
    OK
    127.0.0.1:6379> set name marblue  # 设置name
    QUEUED
    127.0.0.1:6379> exec  # 提交事务
    1) OK
    127.0.0.1:6379> get name  # 执行成功
    "marblue"
    127.0.0.1:6379> 
    

    另外记住一点:一个watch对应一个事务,如果watch之后,执行了事务,那么对这个key的监视就算结束了。如果想继续监视,那么必须再次watch key。

    127.0.0.1:6379> flushdb
    OK
    127.0.0.1:6379> watch name  # 监视name
    OK
    127.0.0.1:6379> set name hanser  # 开始事务之前将其修改
    OK
    127.0.0.1:6379> multi  # 开启事务,显然此时如果设置name的话必然不会成功,因为name在被监视的时候就已经被修改
    OK
    127.0.0.1:6379> exec  # 直接提交事务
    (nil)
    127.0.0.1:6379> multi  # 再次开启事务
    OK
    127.0.0.1:6379> set name yousa  # 设置
    QUEUED
    127.0.0.1:6379> exec  # 提交
    1) OK
    127.0.0.1:6379> get name  # 发现执行成功
    "yousa"
    127.0.0.1:6379> 
    

    所以原因就在于一个watch对应一个事务,watch之后只要执行了事务,不管里面的命令是成功还是失败,这个watch就算是结束了。再次开启事务,设置的key就是不被监视的key了。

    但如果在事务中使用了watch,那么会报错:(error) ERR WATCH inside MULTI is not allowed,但事务不会终止。所以watch只可以在开启事务之前使用。

    Python实现Redis中的事务和监控

    下面看看如何使用Python实现Redis中的事务和监控

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 设置key
    client.set("name", "古明地觉")
    
    # 开启事务, Python操作Redis开始事务需要创建一个管道
    pipe = client.pipeline()
    
    # 监视key 
    pipe.watch("name")
    pipe.multi()  # 此时事务算是开启了
    pipe.set("name", "古明地恋")
    # 退出事务的话,使用pipe.exit()
    pipe.execute()  # 执行事务
    
    # 获取name
    print(client.get("name"))  # 古明地恋
    

    小结

    最后总结一下Redis中关于事务的特性:

    • 单独的隔离操作:事务中所有的命令都会被序列化,按照顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
    • 没有隔离级别的状态:队列中的命令在没有提交之前(exec),都不会被实际地执行,因为开启事务之后、事务提交之前,任何指令都不会被实际地执行。也就不存在"事务内的查询要看到更新,事务外查询无法看到"这个让人头疼的问题。
    • 不保证原子性:我们之前演示过,如果是在运行时出错,那么后面的命令会继续执行,不会回滚。

    正常情况下 Redis 事务分为三个阶段:开启事务、命令入队、执行事务。Redis 事务并不支持运行时错误的事务回滚,但在某些入队错误,如 dasdasda等命令本身错误 或者是 watch 监控项被修改时,提供整个事务回滚的功能(或者说直接就把事务给取消了)

  • 相关阅读:
    BFS(双向) HDOJ 3085 Nightmare Ⅱ
    BFS+Hash(储存,判重) HDOJ 1067 Gap
    BFS(判断状态) HDOJ 3533 Escape
    三进制状压 HDOJ 3001 Travelling
    BFS(八数码) POJ 1077 || HDOJ 1043 Eight
    Codeforces Round #332 (Div. 2)
    BFS HDOJ 2102 A计划
    if语句
    shell脚本编程测试类型下
    shell脚本编程测试类型上
  • 原文地址:https://www.cnblogs.com/traditional/p/13298599.html
Copyright © 2011-2022 走看看