撩一撩Redis:Redis的事务

事务

在mysql中说过事务,主要特性就是ACID,如下

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

Redis中的事务

先来看看用法和常用命令

  • MULTI:开启一个事务
  • EXEC:执行一个事务
  • DISCARD:取消一个事务
MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 所有队列中的命令才会被执行。另一方面, 通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。EXEC 命令的回复是一个数组, 数组中的每个元素都是执行事务中的命令所产生的回复。 其中, 回复元素的先后顺序和命令发送的先后顺序一致。当客户端处于事务状态时, 所有传入的命令都会返回一个内容为 QUEUED 的状态回复(status reply), 这些被入队的命令将在 EXEC 命令被调用时执行。

例子:

在这里插入图片描述

在这里插入图片描述

事务中发生错误怎么办

我们直到事务执行要么全部完成,在执行过程中发生错误就会回滚,这是事务的原子性。但是我们针对事务的错误,定义了两种错误的解决方式:一种就是连坐,另一种就是谁错谁负责。

在Redis所有命令中,并不是所有的命令都会干扰事务,也就是说,有些命令是不会阻断事务的执行的。

像下面的这种语法错误,导致事务失败,事务是直接失败的。是属于连坐
在这里插入图片描述
但是像下面这种情况,我已经把k1-v1键值对建立好,但是又使用的自增让v1+1,但是v1是字符串,根本不可能,像这种就是谁错谁负责,并不会阻断事务执行,事务中的其他命令还是会执行的。返回的状态数组中显示了那条命令失败,其他成功,执行完后检查k1的值,确实被改变了。
在这里插入图片描述
归纳上面两种错误:

  • 连坐:事务在执行 EXEC 之前入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。

那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。

或许你会想,这也太坑了吧,连回滚都没有,那有些命令是一部分成功一部分不成功了。

在事务运行期间,虽然Redis命令可能会执行失败,但是Redis仍然会执行事务中余下的其他命令,而不会执行回滚操作。然而,这种行为也有其合理之处:
只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。也就是说这些错误应该是程序员自己应该发现的错误,而不应该到服务器才发现。这些错误没必要在服务器那边执行完事务命令后再执行回滚。

Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力。对于Redis事务的这种行为,有一个普遍的反对观点,那就是程序有可能会有缺陷(bug)。但是,你应当注意到:事务回滚并不能解决任何程序错误。

乐观锁和悲观锁

关于乐观锁和悲观锁详情见文章乐观锁与悲观锁

使用 check-and-set 操作实现乐观锁

WATCH 命令可以为 Redis 事务提供 check-and-set(CAS) 行为。
被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。这个nil-reply在撩一撩Redis:Redis协议中已经说过了。
在这里插入图片描述
像上面这种情况,多个客户端同时读取到了mykey,假设都是100,但是在set之前,各自+1,又同时set成了101,本来的结果是102的,那么就会出现错误

用watch

在这里插入图片描述
使用上面的代码, 如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 mykey 的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。

这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。

看下面例子

我首先使用watch命令,监视了balance
在这里插入图片描述
然后在另一个客户端代码之中改变balance

 jedis.set("balance","12");

这时候,再在原来的客户端,分别执行一下命令
在这里插入图片描述
可以看到,exec执行结果返回了nil,也就是没有执行成功。balance的结果还是12
那么说明watch监视成功,它起到了乐观锁的作用,就是在你提交程序之前,有其他人也提交了修改了数据,从而导致版本和你之前看到的不一样了。那么就会执行失败。

程序代码操作事务

在这里插入图片描述

public static void main(String[] args) {
        Jedis jedis = new Jedis("114.116.219.97",5000);
        int balance;//可用余额
        int debt;//欠额
        int amtTosubtract = 10;//实例额度

        jedis.watch("balance");
     
        balance = Integer.parseInt(jedis.get("balance"));
        if(balance<amtTosubtract){
            jedis.unwatch();
            System.out.println("the ddata has nbe updated before you commit");

        }else{
            Transaction transaction = jedis.multi();
            transaction.decrBy("balance",amtTosubtract);
            transaction.incrBy("debt",amtTosubtract);
            transaction.exec();
            balance = Integer.parseInt(jedis.get("balance"));
            debt = Integer.parseInt(jedis.get("debt"));

            System.out.println("balance:"+balance);
            System.out.println("debt:" + debt);

        }

        jedis.set("haha","ii");
        jedis.close();
    }

在这里插入图片描述

public static void main(String[] args) {
        Jedis jedis = new Jedis("114.116.219.97",5000);
        int balance;//可用余额
        int debt;//欠额
        int amtTosubtract = 10;//实例额度

        jedis.watch("balance");
        //加入线程沉睡,让另一个客户端去修改
        try {
            Thread.sleep(8000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        balance = Integer.parseInt(jedis.get("balance"));
        if(balance<amtTosubtract){
            jedis.unwatch();
            System.out.println("the ddata has nbe updated before you commit");

        }else{
            Transaction transaction = jedis.multi();
            transaction.decrBy("balance",amtTosubtract);
            transaction.incrBy("debt",amtTosubtract);
            transaction.exec();
            balance = Integer.parseInt(jedis.get("balance"));
            debt = Integer.parseInt(jedis.get("debt"));

            System.out.println("balance:"+balance);
            System.out.println("debt:" + debt);

        }

        jedis.set("haha","ii");
        jedis.close();
    }

在这里插入图片描述

输出:the ddata has nbe updated before you commit

CAS(Check And Set)

发布了66 篇原创文章 · 获赞 31 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43272605/article/details/104333211