zoukankan      html  css  js  c++  java
  • redis基础知识篇

    1.常见数据类型、常见指令、内部数据结构以及用途?

    拓展:SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。

    String的实际应用场景:缓存,阅读访问量之类的计数器,限流,共享用户Session,分布式锁,全局ID,

    Hash的实际应用场景:结构化的数据,比如简单的实体类对象(但前提是这个对象没嵌套其他的对象)

    List的实际应用场景:文章的评论列表,文章列表或者数据分页展示,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

    Set的实际应用场景:玩交集、并集、差集的操作,所以可以共同好友啥的。

    Sorted set的实际应用场景:去重但可以排序,所以可以用于排行榜,按照时间、按照播放量、按照获得的赞数等,

                     也可以来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

    2.redis过期策略?

     (1)定期删除:每隔0.1s检查一次,从设置过期时间的key中,随机测试20个有过期时间的key,然后删除已过期的key,如果有25%的key被删除,则重复执行整个流程。

     (2)惰性删除:尝试获取某个key时进行检查,进行删除操作。

    3.redis数据淘汰策略?

           no-eviction(默认):当内存不足时,新写入操作会报错,这个一般没人用

           allkeys-lru(常用):尝试回收使用最少的键,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。如不设置过期时间,高效利用内存,可以选用allkeys-lru 策略

      volatile-lru:在过期集合的键中,尝试回收使用最少的键

      allkeys-random:回收随机的键,如果所有数据访问概率大致相等时,可以选择allkeys-random

      volatile-random:在过期的键中,回收随机的键

      volatile-ttl:在过期的键中,优先回收存活时间最短的键

    4.redis 持久化

    4.1 概念:redis会把内存的中的数据异步的写入到硬盘中,使得数据在Redis重启之后仍然存在

    4.2 实现方式:     

          RDB持久化:将Redis某一时刻存在的所有数据都写入硬盘,操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

               save模式(同步,阻塞服务器进程,已废弃,手动触发)

                       过程:客户端向Redis发送save命令来创建一个快照文件,如果存在老的快照文件,新的将会替换老的。
                       缺点:save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求。

               bgSave模式(异步,手动触发)

                             过程: 客户端向Redis发送bgsave命令,Redis调用fork创建一个子进程,子进程负责将快照写入硬盘,而父进程则继续处理命令请求。
           缺点:bgsave在fork创建子进程时,需要增加内存服务器开销,如果内存不够,会使用虚拟内存,导致阻塞Redis运行。所以,需要保证空闲内存足够。
           解决方法:可以控制单个Redis实例的最大内存,来尽可能降低Redis在fork时的事件消耗。

               save m n(自动触发 )指定当m秒内发生n次变化时,会触发bgsave

         AOF持久化:以日志的形式记录redis写操作,追加写入硬盘中的AOF文件

                流程:所有的写命令会追加到aof-buf缓冲区,aof-buf缓冲区会向磁盘同步,当aof文件越来越大时,定期对文件rewrite重写,压缩文件,当Redis重启时,load加载aof文件即可恢复,

                同步策略: always:每条Redis写命令都同步写入硬盘。优点:不丢失数据,缺点:IO开销较大。

                                   everysec(默认 建议):每秒执行一次同步,将多个命令写入硬盘。优点:每秒一次,缺点:可能会丢1s数据。

              no:由操作系统决定何时同步。优点:不用管,缺点:不可控。

                文件重写:定期重写AOF文件,过期的数据,无效的命令不再写入文件,多条命令可以合并为一个,从而减小AOF文件的体积。

                      过程:1:执行bgrewriteaof命令后,会检查是否存在子进程重写操作。
            2:没有的话,主线程会fork个子线程进行重写操作,子进程根据内存快照,按照命令合并规则写入到新的aof文件
            3:主线程会继续响应其他命令,修改命令继续写入aof缓冲区,写入到旧aof文件中
            4:子进程重写完成后,会通知主线程,然后主线程把写到aof重写缓冲区的数据,追加到新的aof文件中

                      触发方式:手动触发:bgrewriteaof命令

                                         自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定触发时机

    AOF 优点:支持秒级持久化、兼容性好,有灵活的同步策略。

             缺点:恢复大文件时,恢复速度慢、对性能影响大。

    RDB  优点:某个时间点上的数据快照,所以适用于备份与全量复制。

             缺点:快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用, 不能实时持久化数据。兼容性不太好,老版本的Redis无法兼容新版本的RDB文件。

    5.redis主从复制

    工作原理:基于RDB方式的持久化实现。

    作用:把redis的数据复制多个副本,部署在不同服务器上,当一台服务器出问题后,其他服务器还能提供服务。

    过程:全量复制:1.从服务器连接主服务器,发送SYNC命令

                                 2.主服务器开始执行BGSAVE命令生成RDB文件,向所有从服务器发送快照文件,并使用缓冲区记录此后执行的所有写命令
                                 3.从服务器收到快照文件后丢弃所有旧数据,载入收到的快照
                                 4.主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
                                 5.从服务器执行来自主服务器缓冲区的写命令

              增量复制:主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令

              无硬盘复制:不需要通过RDB文件去同步,直接发送数据

    策略:主从服务器刚连接时,进行全量同步,全同步结束后,再进行增量同步。

    缺点:非高可用,主服务器挂断,从服务器需人为干预

    6.redis哨兵机制?

    作用:监控 Redis 集群中Redis系统的运行状况,当被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器.

    实现原理:

       定时监控任务:  每隔1秒哨兵向主从服务器+其他哨兵,发送ping命令做一次心跳检测,哨兵用来判断节点是否正常.

                               每隔2秒哨兵通过监控的主节点的指定频道交换信息,发送该哨兵对主节点的判断及自身哨兵信息.

             每隔10秒哨兵向主从节点发送info命令,获取拓扑结构图,当哨兵判断主节点客观下线时,就会向下线的主服务器下的所有从服务器,发送info命令频率改为1秒一次.

         哨兵选举:  主观下线:由一个哨兵节点判定主节点down掉是主观下线.

                          客观下线:多个哨兵节点交换主观判定结果,超过半数任务下线,才会判定主节点客观下线

                          流程:   1.如果需要从redis集群选举一个节点为主节点,首先需要从哨兵集群中选举一个哨兵节点作为Leader

              2.每一个哨兵节点都可以成为Leader,当一个哨兵节点确认redis集群的主节点主观下线后,会请求其他哨兵节点要求将自己选举为Leader被请求的哨兵节点如果没有同意过其他哨兵节点的选举请求,则同意该请求

              3.一个哨兵节点获得的选举票数达到Leader最低票数时,当选为leader,然后决定决定新的redis主节点
              4.没有选出,则进行下一轮选举

          心跳机制:从服务器默认以每秒一次的频率,向主服务器发送命令。

                     作用:检测主服务器的网络连接状态;辅助实现min-slaves选项;检测命令丢失。

    7.redis缓存雪崩?

    定义:redis缓存挂掉或集中失效,全部请求都跑去数据库.

    解决方法:    1.将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机时间,这样就很难引发集体失效。

                        2.用加阻塞式的排它锁来实现,在缓存查询不到的情况下,每此只允许一个线程去查询DB,这样可避免同一个ID的大量并发请求都落到数据库中。

    8.redis缓存穿透?

    定义:大量查询redis缓存中,不存在的数据,导致查询数据库

    解决方法:a:使用布隆过滤器,请求数据不合法就不让这个请求到数据库层。

                      布隆过滤器的使用方法,类似java的SET集合,用来判断某个元素(key)是否在某个集合中。但布隆过滤器不需存储key的值。

                      b:查询数据库结果为空时,将null或“” 设值给redis

                      jedis.set("key","60*3",Json.toString(""));//3分钟后过期

           //将List数据装载入布隆过滤器中
        private BloomFilter<String> bf =null;
        
        //PostConstruct注解对象创建后,自动调用本方法
        @PostConstruct
        public void init(){
            //在bean初始化完成后,实例化bloomFilter,并加载数据
            List<Entity> entities= initList();
            //初始化布隆过滤器
            bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), entities.size());
            for (Entity entity : entities) {
                bf.put(entity.getId());
            }
        }
    
        //访问经过布隆过滤器,存在才可以往数据库中查询
    @Cacheable(value = "province")
    public Provinces query(String id) {
            //先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库
            if(!bf.mightContain(id)){
                Log.info("非法访问"+System.currentTimeMillis());
                return null;
            }
            Log.info("数据库中得到数据"+System.currentTimeMillis());
            Entity entity= super.query(id);
            return entity;
        }
    View Code

    9.redis缓存击穿?

    定义:如果某个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到数据库上,则称为缓存击穿。

    区别:雪崩是很多key集体失效,击穿是一个热点key失效

    解决方式:分布式锁

      //核心代码
    String token = UUID.randomUUID().toString(); String lock = jedis.set(key, token, "NX", "EX",20);

    10.分布式锁

    controller

    @RestController
    public class LockController {
        private static long count = 20;//黄牛
        private CountDownLatch countDownLatch = new CountDownLatch(5);
    
        @Resource(name="redisLock")
    //    @Resource(name="mysqlLock")
        private Lock lock;
    //http://www.itzhai.com/the-introduction-and-use-of-a-countdownlatch.html
        @ApiOperation(value="售票")
        @RequestMapping(value = "/sale", method = RequestMethod.GET)
        public Long query() throws InterruptedException {
            count = 20;
            countDownLatch = new CountDownLatch(5);
    
            System.out.println("-------共20张票,分五个窗口开售-------");
            new PlusThread().start();
            new PlusThread().start();
            new PlusThread().start();
            new PlusThread().start();
            new PlusThread().start();
            return count;
        }
        // 线程类模拟一个窗口买火车票
        public class PlusThread extends Thread {
            private int amount = 0;
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始售票");
                countDownLatch.countDown();
                if (countDownLatch.getCount()==0){
                    System.out.println("----------售票结果------------------------------");
                }
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                while (count > 0) {
                    lock.lock();
                    try {
                        if (count > 0) {
                            //模拟卖票
                            amount++;
                            count--;
                        }
                    }finally{
                        lock.unlock();
                    }
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "售出"+ (amount) + "张票");
            }
        }
    }
    View Code

    redisLock

    @Service
    public class RedisLock implements Lock {    
        private static final String  KEY = "LOCK_KEY";
        
        @Autowired
        private JedisConnectionFactory factory;
    
        private ThreadLocal<String> local = new ThreadLocal<>();
        
        @Override
        //阻塞式的加锁
        public void lock() {
            //1.尝试加锁
            if(tryLock()){
                return;
            }
            //2.加锁失败,当前任务休眠一段时间
            try {
                Thread.sleep(10);//性能浪费
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //3.递归调用,再次去抢锁
            lock();
        }
        @Override
        //阻塞式加锁,使用setNx命令返回OK的加锁成功,并生产随机值
        public boolean tryLock() {
            //产生随机值,标识本次锁编号
            String uuid = UUID.randomUUID().toString();
            Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
    
            String ret = jedis.set(KEY, uuid,"NX","PX",1000);
    
            //设值成功--抢到了锁
            if("OK".equals(ret)){
                local.set(uuid);//抢锁成功,把锁标识号记录入本线程--- Threadlocal
                return true;
            }
    
            //key值里面有了,我的uuid未能设入进去,抢锁失败
            return false;
        }
        //错误解锁方式
        public void unlockWrong() {
            //获取redis的原始连接
            Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
            String uuid = jedis.get(KEY);//现在锁还是自己的
    
            //uuid与我的相等,证明这是我当初加上的锁
            if (null != uuid && uuid.equals(local.get())){//现在锁还是自己的
                //锁失效了 删锁
                jedis.del(KEY);
            }
        }
        //正确解锁方式
        public void unlock() {
            //读取lua脚本
            String script = FileUtils.getScript("unlock.lua");
            //获取redis的原始连接
            Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
            //通过原始连接连接redis执行lua脚本
            jedis.eval(script, Arrays.asList(KEY), Arrays.asList(local.get()));
        }
        //-----------------------------------------------
    }
    View Code

    unlock.lua

    if redis.call("get",KEYS[1]) == ARGV[1] then 
        return redis.call("del",KEYS[1]) 
    else 
        return 0 
    end
    View Code

    11.redis挂了怎么办?

    redis挂掉      事发前:实现redis高可用(主从架构+sentine哨兵 或者Redis  cluster),避免redis挂掉.

                         事发时:设置本地缓存(ehcache)+限流(hystrix)

                         事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据

     12.Redis 单线程为什么还能这么快?

    因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题。

    正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。

    拓展:生产禁用keys,但可使用SCAN cursor [MATCH pattern] [COUNT count] 
     scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是一次遍历的key的数量,并不是符合条件的结果数量。
     第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

     示例: scan cursor match *91300 count 100    模糊查询91300工号的100条数据 

    13.Redis 单线程如何处理那么多的并发客户端连接?

    Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到
    文件事件分派器,事件分派器将事件分发给事件处理器。
    Nginx也是采用IO多路复用原理解决C10K问题

     

  • 相关阅读:
    struts2中<s:select>标签的使用
    正则表达式(括号)、[中括号]、{大括号}的区别小结
    解读邮箱正则表达式:^w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*$
    Struts2国际化-getText()方法
    Eclipse的Tomcat热部署,免重启的方法
    hdu 5056Boring count
    纸板上的虚拟现实和代码中的Cardboard
    OC-Protocol实现业务代理
    UVA 11237
    mysql相关日志汇总
  • 原文地址:https://www.cnblogs.com/fuyublog/p/11350302.html
Copyright © 2011-2022 走看看