redis学习-24- Redis Transaction事务

Redis的事务是命令队列,按顺序执行,不保证原子性,不支持回滚。乐观锁通过WATCH命令实现,如果在事务执行前被修改则事务不会执行。Redis使用单线程模型,避免了隔离性问题。文章还介绍了悲观锁、乐观锁和CAS的概念及优缺点,并通过示例展示了Redis如何利用WATCH实现乐观锁。
摘要由CSDN通过智能技术生成

23.Transaction事务

  • Redis 事务基于Commands队列实现,目的是方便用户一次执行多个命令,本质是一组命令的集合。一个事务中的全部命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不准加塞。执行 Redis 事务可分为三个阶段:

    • 开始事务
    • 命令入队(不会立刻执行,排入队列)
    • 执行事务
  • Redis 事务具有两个重要特性:

    • 1.单独的隔离操作:事务中的所有命令都会被序列化,它们将按照顺序执行,并且在执行过的程中,不会被其他客户端发送来的命令打断,即:一个队列中,一次性、顺序性、排他性的执行一系列命令。
    • 2.不保证原子性:在 Redis 的事务中,如果存在命令执行失败的情况,那么其他命令依然会被执行,不支持事务回滚机制。

注意:Redis 不支持事务回滚,原因在于 Redis 是一款基于内存的存储系统,其内部结构比较简单,若支持回滚机制,则让其变得冗余,并且损耗性能,这与 Redis 简单、快速的理念re不相符合。

redis没有隔离级别的概念,不会出现关系型数据库中的脏读、幻读、不可重复读等问题。因为Redis是单线程的,其实就等同于SQL的串行化隔离级别,SQL在串行化隔离级别下其实也不存在隔离性问题

redis单条命令是保持原子性的,如:setnx命令保证原子性。但 Redis 没有在事务上增加任何维持原子性的机制

  • 事务默认状态
    • Redis默认关闭

    • 没开启事务:立即执行并返回结果,直接写入内存

    • 开启事务:不会立刻执行,排入队列,并返回队列状态,调用exec才会执行commands中的命令

23.1 Redis事务命令
命令说明
MULTI开启一个事务
EXEC执行事务中commands队列的所有命令,返回队列中各条命令的结果,执行完后该组事务结束,下次另开事务。
WATCH key [key …]实现乐观锁。在开启事务之前用来监视一个或多个key,在事务发生之前是否被修改,若被修改则回滚 。标记要监视的给定密钥,以便有条件地执行事务。
DISCARD结束事务,队列中的命令不会执行,并消除commands队列
UNWATCH取消 WATCH 命令对 key 的监控。
  • 把事务可以理解为一个批量执行 Redis 命令的脚本,但这个操作并非原子性操作,也就是说,如果中间某条命令执行失败,并不会导致前面已执行命令的回滚,同时不会中断后续命令的执行(不包含监听 key 的情况)。
    在这里插入图片描述

  • 示例如下:

remote:0>multi
"OK"
remote:0>set num 10 
"QUEUED"
remote:0>incrby num 20
"QUEUED"
remote:0>exec			#执行完后该组事务结束,下次另开事务。
 1)  "OK"
 2)  "30"
remote:0>get num
"30"
#若在事务开启之前监听了某个 key,以便有条件地执行事务。
remote:0>watch num
"OK"
remote:0>multi
"OK"
remote:0>set num 50
"QUEUED"
remote:0>incrby num 10
"QUEUED"
remote:0>get num
"QUEUED"
remote:0>exec
 1)  "OK"
 2)  "60"
 3)  "60"
remote:0>unwatch
"OK"

#放弃事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set num 88
QUEUED
127.0.0.1:6379(TX)> get num
QUEUED
127.0.0.1:6379(TX)> incrby num 5
QUEUED
127.0.0.1:6379(TX)> discard		#放弃事务,队列中的命令不会执行,并消除commands队列
OK
127.0.0.1:6379> get num
"88"

redis事务出现异常时:

  • 若事务队列中存在命令错误(类似java编译型错误),执行exec,所有命令都不会执行
  • 若事务中存在语法错误(类似java 1/0的运行时异常),执行 exec,正确命令会被执行,错误命令抛出异常
  • Redis的“事务”,可以理解成“批处理”,而非我们熟知的事务。
  • 示例:两个异常:用了错误的命令,错误的用了命令
#编译型异常,代码编译不通过
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> get num
QUEUED
127.0.0.1:6379(TX)> getset num		#错误命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set num 99
QUEUED
127.0.0.1:6379(TX)> get num
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379(TX)> exec			#执行事务报错,所有的命令未被执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get num
"88"

#运行时异常(给字符串+10)
127.0.0.1:6379> get name
"zs"
127.0.0.1:6379> get num
"88"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby name 10	#压队正常,执行失败
QUEUED
127.0.0.1:6379(TX)> incrby num 10
QUEUED
127.0.0.1:6379(TX)> get num
QUEUED
127.0.0.1:6379(TX)> exec			#虽然第一条命令报错了,但是依旧正常执行成功了!
1) (error) ERR value is not an integer or out of range
2) (integer) 98
3) "98"
  • 特性

    • 单独的隔离操做:事务中的全部命令都会序列化、按顺序地执行。事务在执行的过程当中,不会被其余客户端发送来的命令请求所打断

    • 没有隔离级别的概念:队列中的命令没有提交以前都不会实际的被执行,由于事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个问题

    • 不保证原子性:redis同一个事务中若是有一条命令执行失败,其后的命令仍然会被执行,没有回滚—部分支持事务

23.2 锁机制总结
  • 实现并发控制的主要手段分为乐观并发控制和悲观并发控制两种。悲观和乐观其实是对数据修改持有的一种态度。
  • 锁种类:锁主要有3种类型:悲观锁、乐观锁、CAS(Check And Set)
    • 悲观锁:又分为共享锁和排它锁;
    • 乐观锁:
    • CAS锁;
  • 悲观锁(Pessimistic Lock):总是持悲观态度,认为每次都会出现最坏的情况,认为每次在他读取数据的时候其他线程都会修改这个数据,因此他需要在读取数据时加锁控制,保证他想取的数据是未被他人修改过的。如:synchronized。用完之后解锁,但是加锁是影响性能的。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁

官方意思:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在数据修改之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

synchronized是互斥锁, 同一时间只有一个并发流可以access共享变量, 而悲观锁可以允许多个读线程。

共享锁:多个事务或者线程可以共享一把锁,他们都能访问到这个数据,但是都不能修改它(共享锁只能读不能改

排它锁:多个事务或者线程不能共用一把锁,其中一个事务或者线程操作一条数据时,其他线程就不能对该数据进行独读取和修改,只有获得排它锁的事务和线程才能对数据进行读取和修改。(排它锁能读能改)。

  • 乐观锁:总是持乐观态度,他认为每次在他读取数据的时候其他线程不会修改这个数据,因此不会上锁,但是他认为在提交更新时其他线程才会更新他的数据,所以他提交的时候进行校验注意:不是加锁)可使用版本号等机制,从而保证更新正确性。乐观锁并未在真正意义上加锁,更侧重于校验。乐观锁适用于多读的应用类型,这样能够提升吞吐量
  • 乐观锁策略:
    • 更新之前先获取version
    • 提交版本必须大于记录当前版本才能执行更新(也解决cas的ABA问题)

**官方解释:**乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交修改时,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。在数据修改时校验的方式被称之为乐观并发控制。

  • CAS锁:也叫做自旋,全称是compare and swap(比较并交换),意思就是CAS通过比较内存中的一个数据是否是预期值,如果是就将它修改成新值,如果不是则进行自旋,重复比较的操作,直到某一刻内存值等于预期值再进行修改.这个问题某些时候不会带来任何影响,某些时候却是影响很大的。

  • 悲观锁和乐观锁有啥区别?

    • 悲观锁侧重于查出来就是错误的数据,所以他侧重于查询时加控制;乐观锁不认为查出来是错的,认为更新时数据是错的,所以他侧重于提交时加控制
  • 悲观锁由于想在数据根源保证数据的准确性,所以悲观锁是和数据库打交道的,也就是说添加悲观锁必须依靠数据库的锁机制。乐观锁不需要借助数据库锁机制,在提交的时候加控制,添加乐观锁其实就是添加版本比较,依靠版本比较来确定数据的准确性。

  • 悲观锁和乐观锁的适用场景是啥?

  • 因为悲观锁认为读的时候有问题,修改的时候没问题,所以适合少读多写乐观锁认为读取的时候没问题,修改的时候有问题,所以适合多读少写

  • 悲观锁和乐观锁的缺点是什么呢?

    • 悲观锁的缺点:

      • 因为悲观锁是和数据库打交道,所以锁机制加大数据库的开销。

      • 增加了死锁的概率。

      • 降低了程序的并行性,如果一个事务或者线程锁定了一行数据,那么其他数据就必须等这个事务或线程处理完了才可以处理那条数据。

      • 解决方法使用for update加锁策略,但需要注意锁的级别。

    • **乐观锁的缺点:**乐观锁缺点,即使用CAS的弊端。

      • 线程竞争激烈会导致自旋次数过多,过度消耗CPU。

      • 引发ABA问题。

      • 解决方法:使用LongAdder原子类,代价是消耗更多的空间,以空间换时间。

    package com.zk.controller;
    
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    /**
     * @author CNCLUKZK
     * @create 2022/10/2-17:52
     */
    public class TestABA {
    
        static AtomicReference<Integer> atomicReference  = new AtomicReference<>(100);
        static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
    
        public static void main(String[] args) {
            System.out.println("******以下是ABA问题的解决****");
    
            new Thread(()->{
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+": 首次版本号为"+stamp);
    
                try {
                    Thread.sleep(1000);
    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedReference.compareAndSet(100,
                        101,
                        atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName()+": 二次版本号为"+atomicStampedReference.getStamp());
    
                atomicStampedReference.compareAndSet(101,
                        100,
                        atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName()+
                        ": 三次版本号为"+atomicStampedReference.getStamp());
            },"t3").start();
    
            new Thread(()->{
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+": 首次版本号为"+stamp);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean flag = atomicStampedReference.compareAndSet(100,
                        2023,
                        1,
                        stamp + 1);
                System.out.println(Thread.currentThread().getName()+
                        ":是否修改成功:"+flag+
                        ":此时的版本号为:"+ atomicStampedReference.getStamp());
                System.out.println(Thread.currentThread().getName()+
                        ":现在最新值为:"+atomicStampedReference.getReference());
            },"t4").start();
        }
    }
    
    ---结果---
    ******以下是ABA问题的解决****
    t3: 首次版本号为1
    t4: 首次版本号为1
    t3: 二次版本号为2
    t3: 三次版本号为3
    t4:是否修改成功:false:此时的版本号为:3
    t4:现在最新值为:100
  • ABA问题:多线程并发的情况下,发生的一种现象。指数据从A被修改到B再从B修改到A引发的问题。

假设有3个线程

线程1,期望值为A,欲更新为B

线程2,期望值为A,欲更新为B(sleep 10s)

线程3,期望值为B,欲更新为A

解释:线程1抢先获得CPU时间片执行,A被改为B,接着执行线程2发现线程2因为某种原因阻塞,便先去执行线程3,B被改为A,此时线程2恢复正常,开始执行,再次取A更新为B,但是此时线程2却不知道此时的A其实是经历了(A→B→A)的变化而不是他想更新的那个最开始未经变化的A,由此异常产生。 在CAS比较前会读取原始数据,随后进行原子CAS操作。这个间隙之间由于并发操作,最终可能会带来问题。

相当于是只关心共享变量的起始值和结束值,而不关心过程中共享变量是否被其他线程动过。
有些业务可能不需要关心中间过程,只要前后值一样就行,但是有些业务需求要求变量在中间过程不能被修改。

  • ABA问题解决方案:

    • 加版本号控制。一般是在数据库表中加一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version的值会+1。当线程需要更新数据时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等才去更新,否则重新获取版本号重试更新操作,直到更新成功。
    • 加时间戳。原理同版本控制。
  • 如何选择悲观锁和乐观锁,主要看适用场景。

    • **响应效率:**如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
    • **冲突频率:**如果冲突频率比较高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
    • 重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁机制,效率低。更新失败的概率比较低。
    • **乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。悲观锁则会等待前一个更新完成。**这也是区别。

随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。

23.4 Redis实现乐观锁
  • redis用WATCH 命令实现乐观锁。在开启事务之前用来监视一个或多个key,在事务发生之前监控key是否被别的客户端修改,若被别的客户端修改则队列不会被执行,EXEC命令执行的事务都将被放弃,同时返回Nullmulti-bulk应答以通知调用者事务执行失败 。标记要监视的给定密钥,以便有条件地执行事务。

  • redis监测测试

#正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money		#监视money对象,获取值
OK
127.0.0.1:6379> multi			#以MULTI开始一个事务
OK
127.0.0.1:6379(TX)> decrby money 30	  #将多个命令入队到事务中,接到这些命令并不会当即执行,而是放到等待执行的事务队列里面
QUEUED
127.0.0.1:6379(TX)> incrby out 30
QUEUED
#更新的时候会比较拿到的值和数据库现有的值是否相同
127.0.0.1:6379(TX)> exec		#事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
1) (integer) 70
2) (integer) 30
  • 测试多线程修改值,使用watch可以当做redis的乐观锁操作
#当前客户端执行事务操作
127.0.0.1:6379> watch money			#监视money,获取值
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
#更新的时候会比较拿到的值和数据库现有的值是否相同
127.0.0.1:6379(TX)> exec		#执行更前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失败!EXEC命令执行的事务都将被放弃,同时返回Nullmulti-bulk应答以通知调用者事务执行失败
(nil)

#另一客户端发起修改money操作
127.0.0.1:6379> get money
"270"
127.0.0.1:6379> incrby money 200
(integer) 470
  • 解决该监控值被修改问题:当 EXEC 和discard被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消,所以再另起事务去操作该操作时,不用提前进行unwatch解锁操作,只需重新监控watch最新值即可。

  • 不会产生cas的aba问题,这个不是基于值的比对,可以去了解Jedis中间件的watch命令源码。测试如下:

    #一线程正常执行共享值的事务操作
    127.0.0.1:6379> get money
    "470"
    127.0.0.1:6379> watch money
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> decrby money 10
    QUEUED
    127.0.0.1:6379(TX)> incrby out 10
    QUEUED
    127.0.0.1:6379(TX)> exec	#在执行前让另一客户端执行aba操作,发现该共享值无法更新成功
    (nil)
    127.0.0.1:6379> 
    
    #另一客户端执行aba操作
    127.0.0.1:6379> get money
    "470"
    127.0.0.1:6379> incrby money 200
    (integer) 670
    127.0.0.1:6379> decrby money 200
    (integer) 470
    
    
下一篇:redis学习-25- Redis Benchmark性能测试
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值