Redis03——Redis的事务

本文详细介绍了Redis的事务机制,包括MULTI-EXEC命令如何保证操作序列化和原子性,以及在并发场景下的数据一致性。讨论了Redis事务的回滚情况,指出其与数据库事务的区别。此外,还探讨了watch命令的乐观锁原理,用于在数据变更时决定事务执行或回滚。最后,提到了Redis的流水线技术,以提高批量命令执行的效率,减少网络延迟影响。
摘要由CSDN通过智能技术生成

一、Redis的事务
 和其他大部分的 NoSQL 不同,Redis 是存在事务的,尽管它没有数据库那么强大,但是它还是很有用的,尤其是在那些需要高并发的网站当中。使用 Redis 读/写数据要比数据库快得多,如果使用 Redis 事务在某种场合下去替代数据库事务,则可以在保证数据一致性的同时,大幅度提高数据读/写的响应速度。互联网系统面向的是公众,很多用户同时访问服务器的可能性很大,尤其在一些商品抢购、抢红包等场合,对性能和数据的一致性有着很高的要求,而存储系统的读/写响应速度对于这类场景的性能的提高是十分重要的。
 在 Redis 中,也存在多个客户端同时向 Redis 系统发送命令的并发可能性,因此同一个数据,可能在不同的时刻被不同的线程所操纵,这样就出现了并发下的数据一致的问题。为了保证一些数据的安全性,Redis 提供了事务方案。而 Redis 的事务是使用 MULTI-EXEC 的命令组合实现的,使用它可以提供两个重要的保证:
  ①事务是一个被隔离的操作,事务中的方法会被 Redis 进行序列化并按顺序执行,事务在执行的过程中不会被其他客户端发生的命令所打断
  ②事务是一个原子性的操作,它要么全部执行,要么全部不执行
 在一个 Redis 的连接中(请注意要求是一个连接,所以在Spring 中会使用 SessionCallback 接口进行处理)使用事务会经过 3 个过程:
  ①开启事务
  ②命令进入队列
  ③执行事务
 Redis 事务相关的命令:
在这里插入图片描述
在这里插入图片描述
Tip
  ①在 Redis 中开启事务是 multi 命令,而执行事务是 exec 命令。multi 到 exec 命令之间的 Redis 命令将采取进入队列的形式,直至 exec 命令的出现,才会一次性发送队列里的命令去执行,而在执行这些命令的时候其他客户端就不能再插入任何命令了,这就是 Redis 的事务机制。
  ②当我们使用了 discard 命令后,再使用 exec 命令时就会报错,因为 discard 命令已经取消了事务中的命令,而到了 exec 命令时,队列里面已经没有命令可以执行了,所以就出现了报错的情况。
  Spring中使用 SessionCallback 接口完成Redis的事务场景:

SessionCallback callBack = new SessionCallback() {
    @Override
    public Object execute(RedisOperations ops) throws DataAccessException {
        ops.multi();
        ops.boundValueOps("key1").set("value1");
        //注意由于命令只是进入队列,而没有被执行,所以此处采用get命令,而value却返回为null
        String value = (String) ops.boundValueOps("key1").get();
        System.out.println ("事务执行过程中,命令入队列,而没有被执行,所以value为空: value="+value);
        //此时list会保存之前进入队列的所有命令的结果
        List list = ops.exec(); //执行事务
        //事务结束后,获取value1
        value = (String) redisTemplate.opsForValue().get("key1");
        return value;
    }
};
//执行Redis的命令
String value = (String)redisTemplate.execute(callBack);
System.out.println(value);

注意
  ①使用 SessionCallBack 接口的目的是保证所有的命令都是通过同一个 Redis 连接进行操作的
  ②要特别注意的是,在使用 multi 命令后,使用 get 等有返回值的方法一律返回为空(即使get的key在之前就有值也会返回为空,也就是说在multi-exec之间不可以使用get命令获取已经保存在redis中的结果,需要在multi-exec之前或者之后才可以,如果在事务中需要用到某个值,则需要在multi-exec之前获取,为了保证这些值的可重复读,我们可以在事务开始前watch相应的key),因为在 Redis 中它只是把命令缓存到队列中,而没有去执行。使用 exec 后才会执行事务,执行完事务后,执行 get 命令才能正常返回结果。执行到 redisTemplate.execute(callBack); 就能执行我们在 SessionCallBack 接口定义的业务逻辑,并将获得其返回值
  ③如果我们希望得到 Redis 执行事务各个命令的结果,可以在SessionCallback 的内部在执行事务之后拿到结果,即这行代码:

List list = ops.exec();

  这段代码将返回之前在事务队列中所有命令的执行结果,并保存在一个 List 中,我们只要在 SessionCallback 接口的 execute 方法中将 list 返回,就可以在程序中获得各个命令执行的结果了。

二、事务的回滚
 对于 Redis 而言,不单单需要注意其事务处理的过程,其回滚的能力也和数据库不太一样,这也需要特别注意。分两种情况:
 1、Redis 事务遇到的命令格式正确而数据类型不符合
在这里插入图片描述
 我们将 key3 设置为字符串,而使用命令 incr 对其自增,但是命令只会进入事务队列,而没有被执行,所以它不会有任何的错误发生,而是等待 exec 命令的执行。当 exec 命令执行后,之前进入队列的命令就依次执行,当遇到 incr 时发生命令操作的数据类型错误,所以显示出了错误,而其之前和之后的命令都会被正常执行,相当于事务没有生效。
 2、Redis 事务遇到的命令格式错误
在这里插入图片描述
 我们使用的 incr 命令格式是错误的,这个时候 Redis 会立即检测出来并产生错误,在此之前我们设置的 k5和在此之后我们设置的 k6的操作都会回滚。当事务执行后,我们发现 k5和k6 的值都为空,说明错误命令之后的操作被 Redis 事务回滚了。可以看出在执行事务命令的时候,在命令入队的时候,Redis 就会检测事务的命令是否正确,如果不正确则会产生错误。无论之前和之后的命令都会被事务所回滚,就变为什么都没有执行,这和数据库的事务类似。但当命令格式正确,而因为操作数据结构引起的错误,则该命令执行时出现错误,而其之前和之后的命令都会被正常执行,这点和数据库很不一样,这是需要注意的地方。
因此对于一些重要的操作,我们必须通过程序去检测数据的正确性,以保证 Redis 事务的正确执行,避免出现数据不一致的情况,因为数据不一致不会引起事务的回滚
 这两种情况有点类似于Java中的编译时异常和运行时异常,对于命令格式错误的就类似于在向命令队列中放的时候编译没有通过,就会导致整个队列里面的命令都无法执行,这就相当于Java中编译不通过则无法启动程序一样,因此其他的命令也无法得到执行;而对于命令格式正确而数据结构不正确则相当于运行时异常,因为在向命令队列中放命令的时候不可能即时查询Redis库去校验该key的数据结构,因此可以正常的放入命令队列中,但在运行时出现异常又并不会影响到其他命令的执行,因为事务执行的时候会把命令队列中的所有命令一一执行完毕。
 事实上,Redis的事务并不是实际意义上的事务,而仅仅是一个命令队列,这些命令只要能成功放入队列,就会按照放入的顺序一一得到执行,但在执行的过程中出现异常并不会使其他命令回滚,而且队列中剩余的命令也会接着执行。但在向队列中放入命令的时候如果有一个放入失败则其之前及之后放入的命令也会放入失败,都得不到执行。

三、watch命令
 在 Redis 中使用 watch 命令可以决定事务是执行还是回滚。一般而言,可以在 multi 命令之前使用 watch 命令监控某些键值对,然后使用 multi 命令开启事务,执行各类对数据结构进行操作的命令,这个时候这些命令就会进入队列。当 Redis 使用 exec 命令执行事务的时候,它首先会去比对被 watch 命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么它不会执行任何事务中的命令,而会回滚事务。其实是一种乐观锁的思想,比较的是watch的键的值。无论事务是否回滚,Redis 都会取消执行事务前的 watch 命令。这里的watch命令应该是对键值对做了一个缓存操作用于事务过程中的值的比对。
在这里插入图片描述
 Redis 参考了多线程中使用的 CAS(比较与交换,Compare And Swap),在数据高并发环境的操作中,我们把这样的一个机制称为乐观锁。当一个线程去执行某些业务逻辑,但是这些业务逻辑操作的数据可能被其他线程共享了,这样会引发多线程中数据不一致的情况。为了克服这个问题,首先,在线程开始时读取这些多线程共享的数据,并将其保存到当前进程的副本中,我们称为旧值(old value),watch 命令就起到这样的一个作用。然后,开启线程业务逻辑,由 multi 命令提供这一功能。在执行更新前,比较当前线程副本保存的旧值和当前线程共享的值是否一致,如果不一致,说明该数据已经被其他线程操作过,此次更新失败。为了保持一致,线程就不去更新任何值,而将事务回滚;否则就认为它没有被其他线程操作过,执行对应的业务逻辑,exec 命令就是执行“类似”这样的一个功能。
 但是CAS 会产生 ABA 问题。所谓 ABA 问题来自于 CAS 原理的一个设计缺陷,它可能引发 ABA 问题:在这里插入图片描述
 仅仅去比较旧值是不足够的,还要通过其他方法避免 ABA 问题。常见的方法如 Hibernate 对缓存的持久对象(PO)加入字段 version ,当每次操作一次该 PO,则 version=version+1,这样采用 CAS 原理比对 version 字段,就能在多线程的环境中,排除 ABA 问题,从而保证数据的一致性。
 Redis 在执行事务的过程中,并不会阻塞其他连接的并发,而只是通过比较 watch 监控的键值对去保证数据的一致性,所以 Redis 多个事务完全可以在非阻塞的多线程环境中并发执行,而且Redis 的机制是不会产生 ABA 问题的,这样就有利于在保证数据一致的基础上,提高高并发系统的数据读/写性能。在这里插入图片描述
在这里插入图片描述

Tip:注意检测watch的key的值的时机是在exec执行之前,检测到被其他线程改变则不会提交事务,则命令队列中的命令都不会得到执行,就相当于事务的回滚。
 示例:
在这里插入图片描述在这里插入图片描述
Tip:示例中在第5个时间点设置的key1的值即使和另一个客户端中一样,也设置为value1,最终事务也会回滚,同样获取不到key2的值,就是说Redis的事务可以避免ABA问题。

四、流水线
 在事务中 Redis 提供了队列,这是一个可以批量执行任务的队列,这样性能就比较高,但是使用 multi…exec 事务命令是有系统开销的,因为它会检测对应的锁和序列化命令。有时候我们希望在没有任何附加条件的场景下去使用队列批量执行一系列的命令,从而提高系统性能,这就是 Redis 的流水线(pipelined)技术。而现实中 Redis 执行读/写速度十分快,而系统的瓶颈往往是网络通信中的延时。
在这里插入图片描述
 在实际的操作中,往往会发生这样的场景,当命令 1 在时刻 T1 发送到 Redis 服务器后,服务器很快执行完了命令 1,而命令 2 在 T2 时刻却没有通过网络送达 Redis 服务器,这样就变成了 Redis 服务器在等待命令 2 的到来,当命令 2 送达,被执行后,而命令 3 又没有送达 Redis,Redis 又要继续等待,依此类推,这样 Redis 的等待时间就会很长,很多时候在空闲的状态,而问题出在网络的延迟中,形成了系统瓶颈。
 为了解决这个问题,可以使用 Redis 的流水线,但是 Redis 的流水线是一种通信协议,没有办法通过客户端演示,不过我们可以通过 Java API 或者使用 Spring 操作它,先使用 Java API 去测试一下它的性能,代码如下:

Jedis jedis = pool.getResource();
long start = System.currentTimeMillis();
// 开启流水线
Pipeline pipeline = jedis.pipelined();
// 这里测试10万条命令的读/写操作
for (int i = 0; i < 100000; i++) {
    int j = i + 1;
    pipeline.set("pipeline_key_" + j, "pipeline_value_" + j);
    pipeline.get("pipeline_key_" + j);
}
// pipeline.sync(); //只执行同步,但是不返回结果
// pipeline.syncAndReturnAll ();将返回执行过的命令返回的List列表结果
List result = pipeline.syncAndRetrunAll();
long end = System.currentTimeMillis();
// 计算耗时
System.err.println("耗时:" + (end - start) + "毫秒");

 通过pipelined批量执行命令的性能要高出数倍,因此可以用来提升性能。执行过的命令的返回值都会放入到一个 List 中。当要执行很多的命令并返回结果的时候,需要考虑 List 对象的大小,因为它会“吃掉”服务器上许多的内存空间,严重时会导致内存不足,引发 JVM 溢出异常,所以在工作环境中,是需要自己去评估的,可以考虑使用迭代的方式去处理。
 在Spring中使用redis的pipelined:

SessionCallback callBack = new SessionCallback() {
    @Override
    public Object execute(RedisOperations ops) throws DataAccessException {
        for (int i = 0; i < 100000; i++) {
            int j = i + 1;
            ops.boundValueOps("pipeline_key_" + j).set("pipeline_value_" + j);
            ops.boundValueOps("pipeline_key_" + j).get();
        }
        return null;
    }
};
long start = System.currentTimeMillis();
//执行Redis的流水线命令
List resultList = redisTemplate.executePipelined(callBack);
System.out.println(resultList);//["pipeline_value_1","pipeline_value_2",...]
long end = System.currentTimeMillis();
System.err.println(end-start);

Tip:注意在执行完executePipelined(callBack)之后拿到的结果并不是SessionCallback接口中的execute()方法的返回值,而是pipeline中所有的有返回值的命令的执行的结果集

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值