• redis6.2(三)Redis事务操作、Redis持久化(RDB、AOF)


    7、Redis事务操作

    (1)、Multi、Exec、Discard

    从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> set k1 v1
    QUEUED
    127.0.0.1:6379(TX)> set k2 v2
    QUEUED
    127.0.0.1:6379(TX)> keys *
    QUEUED
    127.0.0.1:6379(TX)> discard
    OK
    127.0.0.1:6379> keys *
    (empty array)
    
    
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> set k1 v1
    QUEUED
    127.0.0.1:6379(TX)> set k2 v2
    QUEUED
    127.0.0.1:6379(TX)> exec
    1) OK
    2) OK
    127.0.0.1:6379> keys *
    1) "k2"
    2) "k1"
    127.0.0.1:6379> 
    
    # 组队阶段报错,提交失败
    # 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> set k1 v1
    QUEUED
    127.0.0.1:6379(TX)> set k2
    (error) ERR wrong number of arguments for 'set' command
    127.0.0.1:6379(TX)> exec
    (error) EXECABORT Transaction discarded because of previous errors.
    
    # 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> set c1 v1
    QUEUED
    127.0.0.1:6379(TX)> incr c1
    QUEUED
    127.0.0.1:6379(TX)> set c2 v2
    QUEUED
    127.0.0.1:6379(TX)> exec
    1) OK
    2) (error) ERR value is not an integer or out of range
    3) OK
    127.0.0.1:6379> keys *
    1) "c1"
    2) "c2"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    (2)、悲观锁、乐观锁

    悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
    
    
    乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
    
    • 1
    • 2
    • 3
    • 4
    redis在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断(乐观锁)。
    
    
    unwatch
    取消WATCH命令对所有key的监视。
    如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH了。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    Redis事务三特性
    
    单独的隔离操作 
        事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 
    没有隔离级别的概念 
        队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
    不保证原子性
        事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (3)、秒杀案例

     /**
         * 秒杀案例: 超卖问题  出现库存为负数的情况
         *
         *ab测试:
         * ab -n 1000 -c 100 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.1.5:8888/redis/doseckill
         */
        @PostMapping("/doseckill")
        public String doSecKill(@RequestParam("userid") String userid,
                                @RequestParam("prodid") String prodid){
            
            userid = userid + ":" +  UUID.randomUUID();
    
            // 拼接商品的key
            String productKey = "sk_product:" + prodid ;
            // 拼接此商品所对应的user集合的key
            String userKey = "sk_user_of_" + prodid ;
    
            //开启事务支持
            redisTemplate.setEnableTransactionSupport(true);
            // 监视库存
            redisTemplate.watch(productKey);
    
            String proCountStr = redisTemplate.opsForValue().get(productKey) + "";
            if(proCountStr == null){
                return "商品" + prodid + "没有参与秒杀活动......";
            }
    
            int proCount = Integer.parseInt(proCountStr);
            if(proCount < 1){
                System.out.println("商品" + prodid + "已经秒杀结束......");
                return prodid + "秒杀结束......";
            }
    
            // 判断此用户是否参与过秒杀活动
             Boolean bool = redisTemplate.opsForSet().isMember(userKey, userid);
             if(bool){
                 return userid + "您好,您已经参加过此物品的秒杀活动......";
             }
    
            // 将此商品库存减一
            redisTemplate.opsForValue().decrement(productKey);
            // 将此用户加入到秒杀成功者清单
            redisTemplate.opsForSet().add(userKey,userid);
    
            
            return userid + "秒杀" + prodid + "成功......";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    /**
         * 秒杀案例: 超卖问题
         *
         *ab测试:
         * ab -n 1000 -c 100 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.1.5:8888/redis/doseckill
         */
        @PostMapping("/doseckill")
        public String doSecKill(@RequestParam("userid") String userid,
                                @RequestParam("prodid") String prodid){
    
            userid = userid + ":" +  UUID.randomUUID();
    
            // 拼接商品的key
            String productKey = "sk_product:" + prodid ;
            // 拼接此商品所对应的user集合的key
            String userKey = "sk_user_of_" + prodid ;
    
            //开启事务支持
            redisTemplate.setEnableTransactionSupport(true);
            // 监视库存
            redisTemplate.watch(productKey);
    
            String proCountStr = redisTemplate.opsForValue().get(productKey) + "";
            if(proCountStr == null){
                return "商品" + prodid + "没有参与秒杀活动......";
            }
    
            int proCount = Integer.parseInt(proCountStr);
            if(proCount < 1){
                System.out.println("商品" + prodid + "已经秒杀结束......");
                return prodid + "秒杀结束......";
            }
    
            // 判断此用户是否参与过秒杀活动
             Boolean bool = redisTemplate.opsForSet().isMember(userKey, userid);
             if(bool){
                 return userid + "您好,您已经参加过此物品的秒杀活动......";
             }
    
            // 利用redis的事务
            //开启事务
            redisTemplate.multi();
    
            // 将此商品库存减一
            redisTemplate.opsForValue().decrement(productKey);
            // 将此用户加入到秒杀成功者清单
            redisTemplate.opsForSet().add(userKey,userid);
    
            //执行事务
            redisTemplate.exec();
    
            return userid + "秒杀" + prodid + "成功......";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    127.0.0.1:6379>  get sk_product:0101
    "0"
    127.0.0.1:6379> smembers sk_user_of_0101
     1) "\"0001:1b3fabd6-01bf-4ec0-bce6-462312a20120\""
     2) "\"0001:29c078c0-e7c1-4328-814f-262460a13c1a\""
     3) "\"0001:afb944d9-e718-443c-a29e-6937bb28be52\""
     4) "\"0001:de43e62e-ad48-4a2b-b017-439666ce6fe8\""
     5) "\"0001:ee47656c-030c-4f4f-b298-30ba1941183f\""
     6) "\"0001:f8596032-d23c-4295-846c-87e58653a206\""
     7) "\"0001:05ffa771-ddbf-4c66-bfd8-1ec158e5ff67\""
     8) "\"0001:be020ba7-d7a9-445d-adf0-ee684e0465e6\""
     9) "\"0001:f816fed6-f8b2-46ae-ae20-f22a71eef9b1\""
    10) "\"0001:6d15ac61-b35c-4c1c-9767-c29a551d0352\""
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    当然,只是解决了超卖问题,还会存在库存遗留问题,可以利用lua脚本等解决。

    注:使用linux发送请求

    # 发送get请求
    # (1) 使用curl指令
    普通请求:
    curl -i/I/v http://www.yyds.cn 
    (如果请求的地址为文件,会直接下载至本地.其中,i代表显示全部信息,I代表显示头部信息,v代表解析请求全过程,默认为i)
    带参数请求:
    curl -v http://www.yyds.cn?param1=1\&param2=2
    (get请求携带的参数只到param1=1,”&”符号在linux系统中为后台运行的操作符,此处需要使用反斜杠”\”转义)
    
    # (2) 使用wget指令
    wget http://www.yanxiaohui.cn
    
    
    # 发送POST请求
    # (1) 使用curl指令
    curl -d "username=user1&password=123" "http://www.yyds.cn/login"
    (通过-d参数,把访问参数放在里面,如果没有参数,则不需要-d)
     
    发送格式化json请求
    curl -i -k  -H "Content-type: application/json" -X POST -d '{"version":"6.6.0", "from":"mu", "product_version":"1.1.1.0"}' http://www.yyds.cn
    
    # (2) 使用wget指令
    wget -post-data 'username=user1&password=123' http://www.yyds.cn
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    8、Redis持久化操作

    (1)RDB持久化

    Redis DataBase(RDB)在指定的时间间隔内将内存中的数据集快照写入磁盘。

    Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。最后一次持久化后的数据可能丢失

    相关配置

    # 在redis.conf中配置文件名称,默认为dump.rdb
    # The filename where to dump the DB
    dbfilename dump.rdb
    
    
    # rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下
    dir ./
    
    # 对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。
    # 如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes.
    rdbcompression yes
    
    # rdbchecksum 检查完整性
    # 在存储快照后,还可以让redis使用CRC64算法来进行数据校验,
    # 但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
    # 推荐yes.
    rdbchecksum yes
    
    
    # RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配置复合的快照触发条件,
    # 默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。
    # 禁用
    # Unless specified otherwise, by default Redis will save the DB:
    #   * After 3600 seconds (an hour) if at least 1 key changed
    #   * After 300 seconds (5 minutes) if at least 100 keys changed
    #   * After 60 seconds if at least 10000 keys changed
    # You can set these explicitly by uncommenting the three following lines.
    # 格式:save 秒钟 写操作次数
    save 3600 1
    save 300 100
    save 60 10000
    
    
    
    # save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。
    # bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    rdb的备份

    先通过config get dir  查询rdb文件的目录 
    将*.rdb的文件拷贝到别的地方
    
    rdb的恢复
      关闭Redis
      先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
      启动Redis, 备份数据会直接加载
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    rdb的优缺点

    # 优势
    适合大规模的数据恢复
    对数据完整性和一致性要求不高更适合使用
    节省磁盘空间
    恢复速度快
    
    # 劣势
    Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
    虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
    在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
    
    
    # 动态停止RDB:redis-cli config set save ""#save后给空值,表示禁用保存策略
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    (2)AOF持久化

    Append Only File(AOF)以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据。

    AOF默认不开启
    可以在redis.conf中配置文件名称,默认为 appendonly.aof
    AOF文件的保存路径,同RDB的路径一致。
    
    AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    AOF持久化策略

    appendfsync always
    始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
    appendfsync everysec
    每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
    appendfsync no
    redis不主动进行同步,把同步时机交给操作系统。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    AOF持久化流程

    (1)客户端的请求写命令会被append追加到AOF缓冲区内;
    (2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
    (3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
    (4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
    
    • 1
    • 2
    • 3
    • 4

    AOF启动/修复/恢复

    AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
    # 正常恢复
    修改默认的appendonly no,改为yes
    将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)
    恢复:重启redis然后重新加载
    
    # 异常恢复
    修改默认的appendonly no,改为yes
    如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复
    备份被写坏的AOF文件
    恢复:重启redis,然后重新加载
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    AOFRewrite压缩

    AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof
    
    
    AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
    
    如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
    如果 no-appendfsync-on-rewrite=no,  还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
    
    # 触发机制
    Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
    重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
    
    auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
    auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
    
    例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB
    系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
    如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    AOF优缺点

    # 优势
    备份机制更稳健,丢失数据概率更低。
    可读的日志文本,通过操作AOF稳健,可以处理误操作。
    
    # 劣势
    比起RDB占用更多的磁盘空间。
    恢复备份速度要慢。
    每次读写都同步的话,有一定的性能压力。
    存在个别Bug,造成恢复不能。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    总结

    如果对数据不敏感,可以选单独用RDB。
    不建议单独用 AOF,因为可能会出现Bug。
    如果只是做纯内存缓存,可以都不用。
    
    • 1
    • 2
    • 3

    redis其他知识可以参考:

    redis6.2(一)安装、配置、常用数据类型

    redis6.2(二)Redis的新数据类型、使用java语言操作Redis

  • 相关阅读:
    毕业设计:基于STM32与机智云平台的远程控制智能家居系统
    Linux命令之wc
    电脑重装系统后Win11扬声器无插座信息如何解决?
    力扣84 双周赛 t4 6144 和力扣305周赛t4 6138
    ChatGLM3-6B-32K 在linux(Ubuntu) GPU P100(16G)复现记录
    HTML课程简介
    1、拓扑排序 2、逆拓扑 3、i到j之间长度为k的路径 4、i到j之间包含顶点x的路径是否存在 5、如果边是带权的,求解 i 到 j 之间长度最长的路径
    img标签如何将<svg></svg>数据渲染出来
    Python 如何使用 MySQL 8.2 读写分离?
    本地部署开发环境过程和遇到的问题总结
  • 原文地址:https://blog.csdn.net/qq_44665283/article/details/128175705