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"

(2)、悲观锁、乐观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。


乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
redis在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断(乐观锁)。


unwatch
取消WATCH命令对所有key的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH了。
Redis事务三特性

单独的隔离操作 
    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 
没有隔离级别的概念 
    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
不保证原子性
    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

(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 + "成功......";
    }
/**
     * 秒杀案例: 超卖问题
     *
     *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 + "成功......";
    }
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\""

当然,只是解决了超卖问题,还会存在库存遗留问题,可以利用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

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会在后台异步进行快照操作, 快照同时还可以响应客户端请求。

rdb的备份

先通过config get dir  查询rdb文件的目录 
将*.rdb的文件拷贝到别的地方

rdb的恢复
  关闭Redis
  先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  启动Redis, 备份数据会直接加载

rdb的优缺点

# 优势
适合大规模的数据恢复
对数据完整性和一致性要求不高更适合使用
节省磁盘空间
恢复速度快

# 劣势
Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。


# 动态停止RDB:redis-cli config set save ""#save后给空值,表示禁用保存策略

(2)AOF持久化

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

AOF默认不开启
可以在redis.conf中配置文件名称,默认为 appendonly.aof
AOF文件的保存路径,同RDB的路径一致。

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

AOF持久化策略

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

AOF持久化流程

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

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,然后重新加载

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进行重写。 

AOF优缺点

# 优势
备份机制更稳健,丢失数据概率更低。
可读的日志文本,通过操作AOF稳健,可以处理误操作。

# 劣势
比起RDB占用更多的磁盘空间。
恢复备份速度要慢。
每次读写都同步的话,有一定的性能压力。
存在个别Bug,造成恢复不能。

总结

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

redis其他知识可以参考:

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

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

你可能感兴趣的:(java,redis,数据库,nosql)