Redis

作者: Exception_Cui | 来源:发表于2021-11-19 18:12 被阅读0次

    安装使用

        安装:     https://www.runoob.com/redis/redis-install.html (windos 和linux 都有)
        中文官网   http://www.redis.cn
        Redis底层使用的是 单线程 + 多路IO复用技术
        操作中不会被打断
        
        Redis默认端口 6379
        
        Redis 是基于 key -value 存储的
    
    服务 开启 和 关闭
        修改  redis.conf 里面 daemonize 为yes
        
        ./redis-server redis.conf  启动
        ./redis-cli shutdown  关闭
        
        ps aux| grep redis 查看redis的pid
        kill -9 ***(pid)   杀死id
    

    key 操作

        keys *       查看当前库的所有的key
                  
        exists key   判断key是否存在   1存在    0不存在
        type   key   查看key类型
        
        del    key   删除key指定的数据  1删除成功
        unlink key   删除key(非阻塞删除,后续异步删除)
        
        //时效 失效 
        expire key   10 给指定key设置过期时间 单位秒 (过期就删除)
        ttl    key    查看key还有多少秒过期  -1 永不过期 -2已经过期
        
        dbsize       查看key数量
    
        select       选择库
        
        flushdb      清空当前库(慎用)
        flushall     通杀全部库(慎用)
    

    Redis 数据类型

    string(字符串)

    最多可以存512M (图片也可以转换为2进制来存储)
    
    基本命令:
             key        value
    set       k1        v100          设置值 (设置相同key 会覆盖 value)
    get       k1                      取值  = v100 
    append    k1        abc           取值  = v100abc
    strlen    k1                      值长度  7 
        
    setnx     k3        123           k3 不存在才设置
         
    incr      k3                      k3 =124  增加 1 (数字类型)
    decr      k3                      k3 =123  减 1  
    incrby    k3        10            k3 =133  增加10
    decrby    k3        10            k3 =123  减 10  
        
    mset      k1 v1 k2 v2 k3 v3      设置多个k-v
    mget      k1 k2 k3                取多个key的值
        
    
    msetnx                            同setnx 可以设置多个,都不存在设置
        
    
    set       k5       123456
    getrange  k5   0  3               值为1234 和 string.substring()一样 1 23456取0-3的值    
    setrange  k5   3  aa              值 123 aa 456  在第三个位置插入
                                         
    //时效 失效    
    setex     k6  10 v5               设置值的同时 设置过期时间   k6 10秒后过期 
    
    数据结构
    底层类似于Stringbuffer  动态分配空间,第一次如果分配了100k,超过了就在分配 100k,
    如果超过了 1M 每次都只分配1M 最大为512M
    

    list(列表)

    list 存储的是单键多
    如: k1 = v1 v2 v3 
    值按照的插入顺序排序
    
    基本操作
              key        value      
    lpush      k1      v1  v2  v3         左增加     值为 v3 v2 v1  向左追加
    rpush      k2      v11 v22 v33        右增加     值为 v11 v22 v33  向右追加
        
    lpop       k1                         左弹出(取出删除) 值为 v1   k2值为v2 v3     
    rpop       k1                         左弹出(取出删除) 值为 v3   k2值为v2
                                               当 k2 值 取完了,k2也就不存在
                                
    rpoplpush  k2  k1                      值为 v33 v1 v2 v3
                                               从左边key取值插入到右边key
                                
    lrange     k1  0 -1                   值为 v1 v2 v3  取出全部的值 
        
    lindex     k2  0                      值为 v11 取出下标为0 的值(不删除)
    llen       k2                         列表长度 值为3
        
    linsert    k2 before v22  leftV22     在 v22 之前插入 leftV22
                  after  v22  rightV22    在 v22 之后插入 rightV22
        
    lrem       k2   2  leftv22            从左边开始删除2个值为 leftv22 的数据
        
    lset       k2   1  newV11             替换下标为1的值为 newV11   
    
    数据结构
    底层是双向链表 如 一个⚪ 形
    对两端的操作性能高,通过索引很慢,如linkedLisd
    

    set(集合 )

    和list区别
        set是无序的,自动排除重复的,底层是hash表,如果不需要重复数据,就用set
    用hash的 field 来作为value  hash的value 为null
    
    基本操作
                key          value
    sadd         k1          v1 v2 v3             增加k1 值为 v1 v2 v3 
    srem         k1          v2 v3                删除 k1 里面的v2和v3,删除集合中的某个元素
    smembers     k1                               取出k1 的值
    sismember    k1          v1                   表示k1是否存在 v1值       有1 没有0
    scard        k1                               返回 k1 值的数量
    spop         k1                               随机取出一个值,并且在原k 删除,k值取完,就删除key
    srandmember  k1          2                    随机从集合取出n个值,不会从集合删除  
    smove        k1 k2       v1                   将k1的v1值 移动到 k2
    
    sinter       k1 k2                            集合交集
    sunion       k1 k2                            集合并集
    
    

    hash(哈希)

    类似于java中的map<String,Object>
    key        value 
    user       id     1
    user       name zhangsan
    特别适合存储对象                          
        第一种:序列化 或者 用json存储对象 如 user  {id=2,name=zhangsan,age=20}  String类型的
                                              
                                              key   value(是  field value的方式)
        第二种:使用 hash                     user      id    2
                                              user      name  zhangsan
                                              user      age   20
    
    基本操作
                   key         value( field value)         
    hset           user               id    1                 
                   user               name  zhangsan                   增加 user 值为 id 1, name 张三的value
    
    hget           user               id                               取出 user 中 field 为 id 的值   为 1    
    
    hmset          user1            id 1 name lisi age 20              增加 user1的值 id  1 ,name lisi,age 20
    
    hexists        user                id                               查询 user 的id 是否存在?  1存在 0不存在
    
    hkeys          user                                                查询 集合的所有 field ,此处值为 id  name
    hvals          user                                                查询 集合中 的所有value
    
    hsetnx                                                             field 存在不添加,不存在添加                                                
    

    zset(有序集合)

    和 set 基本一样,不过是有顺序的
    
    基本操作
                           key          value(score  value)
    zadd                   k1            1 java   3 mysql  2 c++                 添加元素 到有序列表中,(排序根据score排序)
    
    
                
    zrange                 k1    0  -1                                            取出元素值 为  java  c++   mysql 已经排序了
    zrange                 k1    0  -1 whithscores                                取出元素值 也显示 scores 值为  java 1 c++ 2 mysql 3
            
    zrangebyscore          k1    1   3                                            从小到大取出score (1到3) 的值 java c++ ky
                                                                     
    zrevrangebyscore       k1    1   3                                            从大到小取出score  
    
    zubcrby                k1    50   java                                        将java的score 增加 50 现在就是 51
    
    zrem                   k1    java                                             删除集合中的java
    
    zcount                 k1    1    50                                          统计score (1-50) 的个数
    
    zrank                  k1    c++                                              查询 c++ 的排名
    
    数据结构
      跳跃表数据结构
        二分查找  + 索引 的方式
        
    

    redis 的详细配置

    注意:启动要以配置文件方式启动 ./redis-server redis.conf

    redis.conf

    port 6379               默认端口设置
    
    bind 127.0.0.1          可以访问redis的 ip 地址,默认只有本机,注掉就可以全部访问
    protected-mode no       保护模式,默认没有密码的情况下,只有本机可以访问,注掉没有密码的情况 其他地址也可以访问
    
    tcp-backlog 511         tcp 握手的次数
    
    timeout 0               表示redis的超时时间,0永不超时, 就说说 redis在没有地址访问的时候,过了多久,需要重新开启访问
    tcp-keepalive 0         就是用来定时向client发送tcp_ack包来探测client是否存活的。默认不探测,官方建议值为60秒 
     
    daemonize no            后台启动redis, 默认不允许,改为 yes 允许
    
    pidfile /var/run/redis.pid   当redis在后台运行的时候,Redis默认会把pid文件放在/var/run/redis.pid,你可以配置到其他地址。当运行多个redis服务时,需要指定不同的pid文件和端口
    
    loglevel                redis 日志的级别
    logfile "Logs/redis_log.txt"  log输出的文件路径
    
    databases 16            redis 默认有16个库,我只默认使用的是0 号库
    
    # requirepass foobared  密码设置,默认没有密码
                            也可以通过 config 
    
    

    redis 新数据类型

    bitmaps (位操作)

    hyperLogLog(基数 计数)

         快速计算不重复元素     
    
        ![image-20210917151023086](https://gitee.com/exception_cui/images/raw/master/202109171510679.png)
    
    基本使用:
    pfadd  k1  java  php  java c++ mysql c++
    pfcount  k1       统计基数的数量  为 4 
    

    Geospatial(经纬度)

    Jedis 操作redis

    快速入门

    1.引入pom文件

     <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>3.6.3</version>
     </dependency>
    

    2.连接测试

    public class JedisDemo1 {
        public static void main(String[] args) {
            Jedis jedis=new Jedis("127.0.0.1", Integer.parseInt("6379"));  //这里使用的是windows
            String ping = jedis.ping();
            System.out.println(ping);   //PONG 就连接成功
        }
    }
    
    Linux连接不上 请查看redis.conf
    • bind 127.0.0.1 是否注释掉了
    • protected-mode no 是否为null
    • linux的防火墙是否放行,或者关闭了

    3.操作jedis

    jedis 操作(其他的请查看api)https://blog.csdn.net/zhangguanghui002/article/details/78770071

        @Test
        public void demo1() {
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            Set<String> keys = jedis.keys("*");    //keys *
    
            jedis.set("name", "luck");    //set
    
            jedis.mset("k1","v1","k2","v2");  //mset
    
            System.out.println("name" + jedis.get("name"));  //get
    
            Boolean name = jedis.exists("name");  //key 是否存在
    
            jedis.expire("name", 10000);  //expire key
            System.out.println("失效时间" + jedis.ttl("name"));
    
            jedis.set("age", "20");
            jedis.incr("age"); //增加
    
            jedis.incrBy("age", 10);
            System.out.println("age" + jedis.get("age"));
    
            jedis.setnx("haha","test");
            
            jedis.close();//断开连接  有池的话,就是归还      
            
        }
    
    模拟验证码操作

    需求:

    案例分析

    public class CodeDemo {
        static String keyCode = "";
        static String keyCount = "";
        public static void main(String[] args) {
            System.out.println("请输入手机号:");
            Scanner scanner = new Scanner(System.in);
            String next = scanner.next();
            if (!sendCode(next)){
                return ;
            }
            String sendCode=getSendCode();
            System.out.println("已经向:" + next + "     发送了验证码:" +sendCode);
    
            System.out.println("请输入您的验证码:");
            String nextcode = scanner.next();
            if(nextcode.equals(sendCode) || nextcode==sendCode){
                System.out.println("验证成功!");
            }else{
                System.out.println("失败!");
            }
    
        }
    
        //获取验证码发送过了的验证码
        public static String getSendCode() {
            Jedis jedis = new Jedis();
            String code = jedis.get(keyCode);
            return code;
        }
    
        //模拟发送
        public static boolean sendCode(String phone) {
            Jedis jedis = new Jedis();
            keyCode = "keyCode:" + phone;
            keyCount = "keyCount:" + phone;
    
            /*实现只能发三次*/
            String count = jedis.get(keyCount);
            if (count == null) {
                jedis.setex(keyCount, getRemainSecondsOneDay(new Date()), "1");
                jedis.setex(keyCode, 120, getCode());
                return true;
            } else if (Integer.parseInt(count) < 3) {
                jedis.incr(keyCount);
                jedis.setex(keyCode, 120, getCode());
                return true;
            } else {
                System.out.println("当前发送已经上限!");
                jedis.close();
                return false;
            }
    
        }
        //模拟验证码
        public static String getCode() {
            Random random = new Random();
            String codes = "";
            for (int i = 0; i < 6; i++) {
                codes += random.nextInt(10);
            }
            return codes;
        }
    
        //当前时间 距离 明天还有多少秒
        public static Integer getRemainSecondsOneDay(Date currentDate) {
            LocalDateTime midnight = LocalDateTime.ofInstant(currentDate.toInstant(),
                            ZoneId.systemDefault()).plusDays(1).withHour(0).withMinute(0)
                    .withSecond(0).withNano(0);
            LocalDateTime currentDateTime = LocalDateTime.ofInstant(currentDate.toInstant(),
                    ZoneId.systemDefault());
            long seconds = ChronoUnit.SECONDS.between(currentDateTime, midnight);
            return (int) seconds;
        }
    
        //java 1.7
        public static Integer getRemainSecondsOneDay1_7(Date currentDate) {
            Calendar midnight = Calendar.getInstance();
            midnight.setTime(currentDate);
            midnight.add(midnight.DAY_OF_MONTH, 1);
            midnight.set(midnight.HOUR_OF_DAY, 0);
            midnight.set(midnight.MINUTE, 0);
            midnight.set(midnight.SECOND, 0);
            midnight.set(midnight.MILLISECOND, 0);
            Integer seconds = (int) ((midnight.getTime().getTime() - currentDate.getTime()) / 1000);
            return seconds;
        }
    

    redis事务

    • Redis事务是一个单独的隔离操作:事务的所有命令都会序列化,按顺序执行,不会被其他客户端请求打断
    • 主要作用就是 串联多个命令 防止别的命令插队(禁止插队)

    Multi 组队阶段

    • 开启事务 (类似 mysql 的 beginTX
      • 将所有得操作都放入 按顺序 放入事务队列中。但是还不执行

    Exec 执行阶段

    • 执行事务 (类似mysql 的 commit
      • 将事务队列中的 操作。执行

    Discard 放弃

    • 放弃事务 (类似mysql的 rollback
      • 取消执行

    事务出错

    • 组队阶段失败,执行阶段都会被取消
    • 执行阶段失败,该执行的执行,该失败的失败,不会回滚

    事务冲突

    什么是事务冲突

    • 比如,有三个 客户端 同时操作 一个key。 三个客户端都看见账户有10000元, 第一个客户端手快,消费了8000,但是第二个客户端没有重新查看,还是10000元,其实就只有2000了,第二个客户端又消费了5000,那么就成负的了,这就是事务冲突

    解决事务冲突

    悲观锁 (多写场景)

    - 案例 3个人上厕所 只有一个坑。(**java中Synchronized 一样**)
    
    • 顾名思义:很悲观,每次拿数据的时候 都认为别人修改
    • 所以每次操作都会上锁,只有这个锁释放了,第二个才会操作
    • 每一次操作之前,先上锁,然后在操作,操作完成后在解锁。然后在上锁,操作,解锁 (上锁-操作-解锁-上锁-操作-解锁)

    乐观锁 (多读场景)

    • 抢票场景  1张票 1000个人同时抢票,所有人都能抢到,但是在支付的时候,只有一个人会支付成功
      
    • 顾名思义:很乐观,每次拿数据的时候 都认为别认不会修改,所以不会上锁 会有一个版本号

    • 两个客户端 或者线程 在操作一个数据, 此时两个都 获取版本号都为1 ,第一个比较快,将数据提交了,顺便更新了版本号

    • 此时第二个想要提交修改数据,发现真实版本号 和 之前的版本号不一样,就不重新执行,这个就是乐观锁

    • 说白了,就是 在提交之间会检测 版本号是否一样,一样就提交,不一样就不提交

    WATCH

    • Redis 乐观锁的实现
    • 在 multi 之前,先执行 watch key1 key2.... 对key 进行监视,如果在在事务执行之前,这个key被发现改动,就将事务打断

    UNWATCH

    • 取消监视

    Redis的事务特性

    • 单独的隔离操作
      • 事务中的所有命令都会序列化,按顺序执行,事务执行过程中不会被其他客户端打断
    • 没有隔离级别
      • 队列中的命令没有提交之前都不会执行。
    • 不保证原子性 (使用LUA脚本可以保证)
      • 事务中如果有一条命令失败,其他命令仍然会执行,没有回滚

    Redis持久化

    RDB (Redis DataBases)

    • 在指定的时间间隔,将内存中的数据集 快照(某个时间点的数据)写入磁盘
      • Redis会单独fork一个子进程进行持久化,会先将数据写入到一个临时文件,待持久化过程结束了,然后再用 临时文件 替换 上次持久化好的文件
      • 利用linxu的 写时复制 功能
      • 最后一次持久化时候 数据可能会丢失

    RDB 配置文件

    • dbfilename dump.rdb 默认的持久化 文件名称 dump.rdb

    • dir ./ 持久化文件的路径

    • stop-writes-on-bgsave-error yes 当redis无法写入硬盘,直接关闭redis写操作(推荐yes)

    • rdbcompression yes 持久化的文件是否进行压缩存储(推荐yes)

    • rdbchecksum yes 持久化之前,数据是否完整,准确(推荐yes)

    • save 900 1 900秒内只少有1个key发生了改边就持久化 300 5

            60      10000                           
      

    命令save VS bgsave

    • save: save时候只管保存,其他不管,阻塞状态。手动保存(不建议)
    • bgsave:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求
    • lastsave 可以获取最后一次快照执行成功时间

    停止

    • redis-cli config set save "" 停止策略

    RDB备份与恢复

    • redis启动会自动恢复,已经持久化过的 rdb文件:名称需要和conf文件配置的一样
    • 直接将 conf文件配置的备份文件 复制一份就可以了

    优点:

    • 适合大规模数据恢复
    • 对数据完整性和一致性要求不高
    • 节省磁盘空间
    • 恢复的速度快

    缺点:

    • Fork的时候,内存中数据被克隆了一份,需要2倍的内存

    • 在一定时间才做一次备份,所以如果意外down掉,就会丢失最后一次快照的修改

    AOF (Append Only File)

    • 以日志的形式记录 每个写操作(读不记录),将Redis执行过的所有执行记录下来,只许追加文件,但不可以修改文件,redis启动后,会重新执行一次 已经记录的指令,完成恢复工作
    • 如果AOF 和 RDB 同时开启,系统默认取AOF的数据
      • 例如:之前rdb存了很多数据,修改aof策略后,aof如果之前没有存过指令,那么恢复回来的数据就是null

    AOP配置文件(aop默认不开启)

    • appendonly no 默认不开启 改为yes 默认开启

    • appendfulename "appendonly.aof" 默认配置文件的名称(默认在当前启动路径)

    • AOF 同步频率设置

      • appendfsync always 始终同步,redis的每次写入就会立即记录日志,性能差,数据完整性好
      • appendfsync everysec 每秒同步,每秒计入日志一次,如果down机,本本秒数据可能丢失
      • appendfsync no 不主动进行,把同步时机交给操作系统
    • Rewrite重写操作

      • no-appendfsync-on-rewrite
      • auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%的时候重写(是原来的文件的 2倍)
      • auto-aof-rewrite-min-size:设置重写基准值,最小64MB,文件达到128MB时候重写
      • AOF 文件持续增长过大,会Fork新进程来将文件重新(和rdb一样)

    AOF备份与恢复

    • redis启动会自动恢复,已经持久化过的 aof文件:名称需要和conf文件配置的一样
    • 直接将 conf文件配置的备份文件 复制一份就可以了

    AOF 异常恢复

    • 修改 appendonly no 为yes
    • 如果遇到AOF文件损坏,可以修复
      • redis-check-aof --fix appendonly.aof (备份文件)
    • 重启Redis 恢复就行

    优点:

    • 备份机制更稳健,丢失数据概率低
    • 可读的日志文本,通过操作aof,可以处理误操作

    劣势:

    • 比RDB占用更多磁盘空间
    • 恢复备份速度慢
    • 每次读写都同步,有一定的性能压力

    总结

    • 官方推荐两个都启用
    • 如果对数据不敏感,可以选单独的RDB
    • 不建议单独使用AOF,可能会出现BUG
    • 如果单纯的做 缓存,可以都不用

    Redis主从复制(master-slave)

    • 主的数据自动备份到从的服务器。Master(主)写为主,Slave(从)读为主

    配置主从(windows), linux是一样的

    1. 将redis的文件复制一份 为MyRedis

    2.复制三个 配置文件:

    3.启动服务器

    4.查看服务器启动状况:(linux 使用 ps -ef |grep redis

    5.连接服务器:

    6.查看每个服务的状态(info replication)

    目前来看,我们的redis没有任何的关系,都是master 配置

    7.配置主从关系(slaveof ip prot)

                                        slave0:ip=****,state=**online**(**表示当前的从服务器可以使用**)
    

    8.测试主从

    一主二仆

    从服务挂掉

    特点:

    从服务器挂掉重启后,是**单独的服务器**,和主服务没有任何关系 
    
    重新建立主从关系后, 数据会**恢复**到**主服务器的数据**
    

    主服务挂掉

    ![image-20210923135334528](https://gitee.com/exception_cui/images/raw/master/202109231354300.png)
    

    特点:

        主服务挂掉后: **从服务器还是从服务器**,**不会上位。** 
    
        主服务器重启后,**恢复主从关系**
    

    主从复制原理:

    烽火相传

    特点:

    **和一主二仆 一样。**
    

    反客为主(slaveof no one)

    当 主服务器 挂掉后,从服务器可以 作为主服务器  
    
    从服务器 :**slaveof  no  one** 就可以成为主服务器
    

    缺点

    还需要自己手动完成。
    
    其他的服务器,也得手动  更改主服务器
    

    哨兵模式(反客为主 自动版本)

    创建 哨兵的配置文件

    • 因为我们有两个从服务器,所以创建两个从的哨兵配置文件 26479 和 26579(如果只有一台 真正的服务器,以端口的方式模拟多个 redis服务器,就可以使用一个哨兵监控)
    port 26479
    sentinel monitor mymaster 127.0.0.1 6380 2
    sentinel down-after-milliseconds mymaster 5000
    sentinel failover-timeout mymaster 15000
    sentinel config-epoch mymaster 1
    

    启动哨兵监控

    • windows: redis-server.exe sentinel26479.conf --sentinel
    • liunx : redis-sentinel sentinel26479.conf

    测试

    自己指定 从->主的优先级(不指定,就按下面的规则)

    • 自己指定新的主(在服务器启动的时候):redis.conf

      • slave-priortiy 100 值越小 越容易 变为 主服务器 (redis 5.0 )

      • replice-priortiy 100 (redis 6.0 )

      • 如果从的配置一样的话

        • 那么会 先选择 偏移量大的(同步最全的从,例如,第一个从同步了10 条,第二个同步了 9条,就会选 10条 的从)
        • 还一样的话,就会以runid 来选择(每个redis 启动后都会系统生成一个runid)

    重启旧服务器

    特点:

    当新的 从服务器上位成 主服务器后旧的主服务重启 就会变成 服务器

    缺点:

    复制延迟。如果系统很繁忙,那么同步的时间会 延迟很久

    JAVA 端如何知道并且使用

    redis集群

    • redis 高并发 写操作,可以使用集群
    • 容量不够,也可以使用集群 (有N个集群,每个集群存储的数据都是 1/n)

    Redis集群详解(https://blog.csdn.net/miss1181248983/article/details/90056960/)

    java操作集群

    缓存穿透

    • 缓存穿透是指,redis 中没有请求的数据,穿过redis直接去请求数据库

    场景分析:

    数据库的id是自动增长的,所以不可能存在-1的id(那么redis也不可能会缓存这个key为-1的数据),这个时候假如黑客,一直使用-1的id一直去请求,那么每次请求都会穿过redis(redis没有-1的key),那么就会直接去请求数据库。请求多的话,会给数据库一定的压力、
    

    解决方案:

    • 业务层校验: 

      • 用户发过来的请求,根据请求参数进行校验,对于明显错误的参数,直接拦截返回。
    • 不存在数据设置短过期时间

      • 不存在的数据 也 放入到redis中:设置短期失效,值为null
        • 缺点:如果黑客,大量的数据不存在,不可能所有的数据都放入存redis。会导致redis压力剧增
    • 布隆过滤器:

      • 在redis和 数据库 中间添加 一层过滤器(将数据库中所有的查询条件放入布隆过滤器中)。 不存在的数据直接过滤掉,就减少了对数据库的压力。
        • 缺点:布隆过滤器会有偏差,不一定所有的数据都能过滤掉

    缓存击穿

    • 缓存击穿是指,某一个热点的key,在一个时间突然失效,然后大量的请求直接去请求数据库

    现象:

    数据库突然的访问量增大,redis是正常的

    解决方案

    • 使用互斥锁

      • 就是当 大量的请求访问数据库的时候添加 一个互斥锁,第一个请求拿到锁后,直接查询数据库,然后将数据重新放回到redis中,然后其他请求就去redis中拿数据
    • 可以设置永不过期

    • 实时监控,即将过期的数据,然后重新放入到redis中

    缓存雪崩

    • 缓存雪崩是值 大量的缓存key,在同一时间大量的失效, 导致大量请求访问数据库。

    解决方案

    • 缓存数据的过期时间设置我随机。防止同一时间的 缓存大量失效
    • 设置缓存永不过期
    • 如果是 redis 宕机了,就使用redis持久化,快速恢复

    缓存预热:

    • 将可能会成为热点的数据 提前放到缓存中

    分布式锁(专业的请看redission)

    因为锁值存在一单台机器,其他的机器不认识你这个锁,还是可以继续执行,所以就出现了分布式锁(就是共享锁

    实现方式

    • 基于数据库
    • 基于redis(性能最好)
    • 基于zookeeper(可靠性最好)

    redis实现分布式锁

    setnx

    • setnx 上锁, del 释放锁

    问题:

    • 如果忘记 释放锁,那么其他的就一直等待
    • 解决方案:
      • 设置锁的过期时间
    • 上锁之后,突然服务器异常了,就无法设置 锁的过期时间
    • 解决方案:
      • 上锁的时候同时设置过期时间

    redis 和 java对应关系

    setnx  == java中的lock.lock()方法              这个就是一个悲观锁
    
    {
        具体的业务:
    }
    
    del    ==  java中的lock.release()
    

    存在问题:

    A 和 B 两台 服务器使用锁
    
    1. A 先 设置锁,并且设置过期时间为10s
    2. A 然后执行具体的业务
    3. A 的服务器突然压力剧增,具体业务缓慢执行
    4. A 10s过后,释放了锁。业务还没有执行完成。
    5. B 开始拿到锁
    6. B 执行业务
    7. A的服务器好了,然后执行完了业务。
    8. A 手动释放了锁。(del )
    9. 锁本来时在B的手里,但是被A 给释放了。
    
    

    解决问题(UUID):

    UUID解决问题:
    
    在设置锁之前 使用UUID来判断
    
    set lock  "uuid" nx ex 10
    然后释放锁之前,通过获取uuid 和当前的是否一样,
    一样就释放,不一样就不释放
    
    

    UUID解决存在问题:

    • 就是A比较通过后,还没有删除锁,这时候锁自动过期了,然后B已经拿到锁了,A也开始删除锁,这时候A又把B的锁给释放了

    解决UUID的问题:

    使用LUA脚本
    
     public static boolean releaseLock(Jedis jedis, String lockKey, String requestId) {
    
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    

    相关文章

      网友评论

          本文标题:Redis

          本文链接:https://www.haomeiwen.com/subject/ueizgltx.html