温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何进行Redis深度分析

发布时间:2021-10-18 10:31:51 来源:亿速云 阅读:138 作者:柒染 栏目:大数据

今天就跟大家聊聊有关如何进行Redis深度分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

0、基础:万丈高楼平地起

1) 当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

2) 如果value值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是signed long的最大最小值,超过了这个值,Redis会
报错,最大值 9223372036854775807
    
3) Redis的列表相当于Java语言里面的LinkedList,注意它是链表而不是数组。Redis的列表结构常用来做异步队列使用。将需要延后处理
的任务结构体序列化成字符串塞进Redis的列表,另一个线程从这个列表中轮询数据进行处理。
   右边进左边出--> 队列
   右边进右边出--> 栈
   
   如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的linkedlist,而是称之为快速链表quicklist的一个结构。首先在列表
元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连
续的内存。当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎
片化。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将链表和ziplist结合起来组成了
quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
   
4) 压缩列表ziplist是一种为节约内存而开发的顺序型数据结构,它被用作列表键和哈希键的底层实现之一。

5) Hash,Redis的字典的值只能是字符串,而且Redis采用渐进式Rehash策略

6) set,Redis的集合相当于Java语言里面的HashSet,内部的键值对是无序的唯一的,其内部实现相当于一个特殊的字典,字典中所有的
value都是一个值NULL。set结构可以用来存储活动中奖的用户ID,因为有去重功能,可以保证同一个用户不会中奖两次。
   
7) zset,它类似于Java的SortedSet和HashMap的结合体,一方面它是一个set,保证了内部value的唯一性,另一方面它可以给每个value
赋予一个score,代表这个value的排序权重。它的内部实现用的是一种叫着跳跃列表的数据结构。zset还可以用来存储学生的成绩,value
值是学生的ID,score是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。zset可以用来存粉丝列表,value值是粉丝的
用户ID,score是关注时间。我们可以对粉丝列表按关注时间进行排序。
//https://yq.aliyun.com/articles/666398
typedef struct zset {

    // 字典,键为成员,值为分值
    // 用于支持 O(1) 复杂度的按成员取分值操作
    dict *dict;

    // 跳跃表,按分值排序成员
    // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
    // 以及范围操作
    zskiplist *zsl;
} zset;

1、千帆竞发:Redis分布式锁

1) 使用setnx命令实现分布式锁
   
超时问题?
   Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这
时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程
逻辑执行完之间拿到了锁。
   
解决办法:
1.1) Redis分布式锁不要用于较长时间任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。
1.2) 有一个更加安全的方案是为set指令的value参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除key。但是匹配value
和删除key不是一个原子操作,这就需要使用Lua脚本来处理了,因为Lua脚本可以保证连续多个指令的原子性执行。

2) 单机Redis实现分布式锁的缺陷
   比如在Sentinel集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把
锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客
户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。
不过这种不安全也仅仅是在主从发生failover的情况下才会产生,而且持续时间极短,业务系统多数情况下可以容忍。
   
解决办法:RedLock
   加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点set成功,那就认为加锁成功。释放锁时,需要
向所有节点发送del指令。不过Redlock算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为Redlock需要向多个节点进行读写,意
味着相比单实例 Redis 性能会下降一些。
如果你很在乎高可用性,希望挂了一台redis完全不受影响,那就应该考虑 redlock。不过代价也是有的,需要更多的 redis 实例,性能
也下降了,代码上还需要引入额外的library,运维上也需要特殊对待,这些都是需要考虑的成本,使用前请再三斟酌。
   
3) 锁冲突处理
   我们讲了分布式锁的问题,但是没有提到客户端在处理请求时加锁没加成功怎么办。
   
一般有3种策略来处理加锁失败:
3.1) 直接抛出异常,通知用户稍后重试
     这种方式比较适合由用户直接发起的请求,用户看到错误对话框后,会先阅读对话框的内容,再点击重试,这样就可以起到人工延时
的效果。如果考虑到用户体验,可以由前端的代码替代用户自己来进行延时重试控制。它本质上是对当前请求的放弃,由用户决定是否重新
发起新的请求。
     
3.2) sleep一会再重试,不推荐

3.3) 将请求转移至延时队列,过一会再试
     这种方式比较适合异步消息处理,将当前冲突的请求扔到另一个队列延后处理以避开冲突。

2、缓兵之计:延时队列

1)异步消息队列   
   Redis的消息队列不是专业的消息队列,它没有非常多的高级特性,没有ack保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。
   Redis的list(列表)数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,使用lpop和rpop来出队列。
   
队列空了怎么办?
   可是如果队列空了,客户端就会陷入pop的死循环,不停地pop,没有数据,接着再pop,又没有数据。这就是浪费生命的空轮询。空轮询不但拉
高了客户端的CPU,redis的QPS也会被拉高,如果这样空轮询的客户端有几十来个,Redis的慢查询可能会显著增多。
   
解决办法:
1.1) 使用sleep来解决这个问题,让线程睡一会
2.1) 使用blpop/brpop,阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。

空闲连接自动断开怎么办?
解决办法:
   如果线程一直阻塞在哪里,Redis的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候
blpop/brpop会抛出异常来。所以编写客户端消费者的时候要小心,注意捕获异常,还要重试。
   
2) 延时队列
   延时队列可以通过Redis的zset(有序列表)来实现。我们将消息序列化成一个字符串作为zset的value,这个消息的处理时间作为score,
然后用多个线程轮询zset获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处理。因为有
多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行。

如何进行Redis深度分析

//Redis实现延迟队列
public class RedisDelayingQueue<T> {

    static class TaskItem<T> {
        public String id;
        public T msg;
    }

    private Type TaskType = new TypeReference<TaskItem<T>>(){}.getType();

    private Jedis jedis;

    private String queueKey;

    public RedisDelayingQueue(Jedis jedis, String queueKey){
        this.jedis = jedis;
        this.queueKey = queueKey;
    }

    public void delay(T msg){
        TaskItem taskItem = new TaskItem();
        taskItem.id = UUID.randomUUID().toString();
        taskItem.msg = msg;
        String s = JSON.toJSONString(taskItem);
        jedis.zadd(queueKey, System.currentTimeMillis() + 5000, s);
    }

    public void loop(){
        while (!Thread.interrupted()){
            //只取一条数据
            Set values = jedis.zrangeByScore(queueKey,0,System.currentTimeMillis(),0,1);
            
            if(values.isEmpty()){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    break;
                }
                continue;
            }

            String s = (String) values.iterator().next();
            //zrem用于移除有序集中一个或多个成员
            if(jedis.zrem(queueKey, s) > 0){
                TaskItem task = JSON.parseObject(s, TaskType);
                this.handleMsg(task.msg);
            }
        }
    }

    private void handleMsg(Object msg) {
        System.out.println(msg);
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        RedisDelayingQueue queue = new RedisDelayingQueue(jedis, "test-queue");

        Thread producer = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++){
                    queue.delay("lwh" + i);
                }
            }
        };

        Thread consumer = new Thread(){
            @Override
            public void run() {
                queue.loop();
            }
        };

        producer.start();
        consumer.start();

        try {
            producer.join();
            Thread.sleep(6000);
            consumer.interrupt();
            consumer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3、节衣缩食: 位图

1) 位图 
   位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位
图的内容,也可以使用位图操作 getbit/setbit等将 byte 数组看成「位数组」来处理。
   redis位图可以实现零存整取、零存零取等。「零存」就是使用 setbit 对位值进行逐个设置,「整存」就是使用字符串一次性填充所有
位数组,覆盖掉旧值。
   命令形如
   1) setbit s 1 1
   2) getbit s
   3) get s
   4) set s h
   
2) 统计和查找
   Redis提供了位图统计指令bitcount和位图查找指令bitpos,bitcount用来统计指定位置范围内1的个数,bitpos用来查找指定范围内出现
的第一个0或1。比如我们可以通过bitcount统计用户一共签到了多少天,通过bitpos指令查找用户从哪一天开始第一次签到。如果指定了范围
参数[start,end],就可以统计在某个时间范围内用户签到了多少天,用户自某天以后的哪天开始签到。
   
3) bitfield

4、四两拨千斤: HyperLogLog

1) HyperLogLog使用
   统计PV:每个网页一个独立的Redis计数器
   统计UV?
解决办法:
1.1) set去重,页面访问量大的情况下,耗费太多存储空间
1.2) 使用HyperLogLog,不精确去重,标准误差0.81%
    
   HyperLogLog提供了两个指令pfadd和pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd用法和set集合的sadd
是一样的,来一个用户ID,就将用户ID塞进去就是。pfcount和scard用法是一样的,直接获取计数值。
   pfadd test-log user1
   pfadd test-log user2
   
   pfcount test-log
   
   HyperLogLog 除了上面的pfadd和pfcount之外,还提供了第三个指令pfmerge,用于将多个pf计数值累加在一起形成一个新的pf值。比如在
网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。其中页面的UV访问量也需要合并,那这个时候pfmerge就可以派
上用场了。

5、层峦叠嶂:布隆过滤器

1) 布隆过滤器
   讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。
问题来了,新闻客户端推荐系统如何实现推送去重的?
   当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
   
基本指令:布隆过滤器有二个基本指令,bf.add添加元素,bf.exists查询元素是否存在,它的用法和set集合的sadd和sismember差不多。
注意bf.add只能一次添加一个元素,如果想要一次添加多个,就需要用到bf.madd指令。同样如果需要一次查询多个元素是否存在,就需要用到
bf.mexists指令。
   
   bf.add test-filter user1
   bf.add test-filter user2
   
   bf.exists test-filter user1
   
   Redis其实还提供了自定义参数的布隆过滤器,需要我们在add之前使用bf.reserve指令显式创建。如果对应的 key已经存在,bf.reserve
会报错。bf.reserve有三个参数,分别是key,error_rate和initial_size。错误率越低,需要的空间越大。initial_size参数表示预计放入
的元素数量,当实际数量超出这个数值时,误判率会上升。

2) 布隆过滤器的原理
   每个布隆过滤器对应到Redis的数据结构里面就是一个大型的位数组和几个不一样的无偏hash函数。所谓无偏就是能够把元素的hash值算得
比较均匀。向布隆过滤器中添加key时,会使用多个hash函数对 key进行hash算得一个整数索引值然后对位数组长度进行取模运算得到一个位
置,每个hash函数都会算得一个不同的位置。再把位数组的这几个位置都置为1就完成了add操作。

3) 布隆过滤器的其他应用
   在爬虫系统中,我们需要对URL进行去重,已经爬过的网页就可以不用爬了。但是URL太多了,几千万几个亿,如果用一个集合装下这些URL
地址那是非常浪费空间的。这时候就可以考虑使用布隆过滤器。它可以大幅降低去重存储消耗,只不过也会使得爬虫系统错过少量的页面。布
隆过滤器在NoSQL数据库领域使用非常广泛,我们平时用到的HBase、Cassandra还有LevelDB、RocksDB内部都有布隆过滤器结构,布隆过滤器
可以显著降低数据库的IO请求数量。当用户来查询某个row时,可以先通过内存中的布隆过滤器过滤掉大量不存在的row请求,然后再去磁盘进
行查询。
   邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中,
这个就是误判所致,概率很低。

6、断尾求生:简单限流

   除了控制流量,限流还有一个应用目的是用于控制用户行为,避免垃圾请求。比如在UGC社区,用户的发帖、回复、点赞等行为都要严格受
控,一般要严格限定某行为在规定时间内允许的次数,超过了次数那就是非法行为。对非法行为,业务必须规定适当的惩处策略。

如何进行Redis深度分析

//这个限流需求中存在一个滑动时间窗口,想想zset数据结构的score值,是不是可以通过score来圈出这个时间窗口来。而且我们只需要
//保留这个时间窗口,窗口之外的数据都可以砍掉。那这个zset的value填什么比较合适呢?它只需要保证唯一性即可,用uuid会比较浪费
//空间,那就改用毫秒时间戳吧。

//但这种方案也有缺点,因为它要记录时间窗口内所有的行为记录,如果这个量很大,比如限定60s内操作不得超过100w次这样的参数,它
//是不适合做这样的限流的,因为会消耗大量的存储空间。
public class SimpleRateLimiter {

    private final Jedis jedis;

    public SimpleRateLimiter(Jedis jedis){
        this.jedis = jedis;
    }

    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) throws IOException {
        String key = String.format("hist:%s:%s", userId, actionKey);
        long nowTs = System.currentTimeMillis();

        Pipeline pipeline = jedis.pipelined();
        //开启一个事务
        pipeline.multi();
        //value和score都用毫秒时间戳
        pipeline.zadd(key, nowTs, "" + nowTs);
        //移除时间窗口之外的行为记录,剩下的都是时间窗口内的
        pipeline.zremrangeByScore(key, 0, nowTs - period * 1000);
        //获得[nowTs - period * 1000, nowTs]的key的数量
        Response<Long> count = pipeline.zcard(key);
        //每次设置都更新key的过期时间
        pipeline.expire(key, period);

        //在事务中执行上述命令
        pipeline.exec();
        pipeline.close();

        return count.get() <= maxCount;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Jedis jedis=new Jedis("localhost",6379);
        SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
        for (int i = 0; i < 20; i++) {
            //每个用户在1秒内最多能做五次动作
            System.out.println(limiter.isActionAllowed("lwh","reply",1,5));
        }
    }
}

7、一毛不拔:漏斗限流

   Redis4.0提供了一个限流Redis模块,它叫redis-cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有了这个模块,限流问题就
非常简单了。
   
   cl.throttle lwh:reply 15 30 60 1
       
   上面这个指令的意思是允许「用户lwh回复行为」的频率为每 60s 最多 30 次(漏水速率),漏斗的初始容量为 15,也就是说一开始可以连
续回复 15 个帖子,然后才开始受漏水速率的影响。

8、近水楼台:GeoHash

   Redis在3.2版本以后增加了地理位置GEO模块,意味着我们可以使用Redis来实现摩拜单车「附近的 Mobike」、美团和饿了么「附近的
餐馆」这样的功能了。
   业界比较通用的地理位置距离排序算法是GeoHash算法,Redis也使用GeoHash算法。GeoHash算法将二维的经纬度数据映射到一维的整数,
这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算「附近的人时」,首先
将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。
   在Redis里面,经纬度使用52位的整数进行编码,放进了zset里面,zset的value是元素的key,score是GeoHash的52位整数值。zset的
score虽然是浮点数,但是对于52位的整数值,它可以无损存储。在使用Redis进行Geo查询时,我们要时刻想到它的内部结构实际上只是一
个zset(skiplist)。通过zset的score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一些,不过这样理解足够了),通过将score
还原成坐标值就可以得到元素的原始坐标。
   
   1) 添加,geoadd指令携带集合名称以及多个经纬度名称三元组
       geoadd company 116.48105 39.996794 juejin
       geoadd company 116.514203 39.905409 ireader
   
   2) 距离,geodist指令可以用来计算两个元素之间的距离
       geodist company juejin ireader km
       
   3) 获取元素位置,geopos指令可以获取集合中任意元素的经纬度坐标
	   geopos company juejin
	   
   我们观察到获取的经纬度坐标和geoadd进去的坐标有轻微的误差,原因是geohash对二维坐标进行的一维映射是有损的,通过映射再还原
回来的值会出现较小的差别。对于「附近的人」这种功能来说,这点误差根本不是事。
   
   4) 附近的公司,georadiusbymember指令是最为关键的指令,它可以用来查询指定元素附近的其它元素
       //范围20公里以内最多3个元素按距离正排,它不会排除自身
   	   georadiusbymember company ireader 20 km count 3 asc
   	   
   	   //三个可选参数 withcoord withdist withhash 用来携带附加参数
   	   //withdist可以显示距离
   	   //withcoord显示坐标
   	   georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc

   5) Redis还提供了根据坐标值来查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近的餐馆」等。
它的参数和georadiusbymember基本一致,除了将目标元素改成经纬度坐标值。
	   georadius company 116.514202 39.905409 20 km withdist count 3 asc   
	
注意事项:
   在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用Redis的Geo数据结构,它们将全部放在一个zset
集合中。在Redis的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个key的数据过大,会对集群的迁移工作造成较大的影响,
在集群环境中单个key对应的数据量不宜超过1M,否则会导致集群迁移出现
卡顿现象,影响线上服务的正常运行。

   所以,这里建议Geo的数据使用单独的Redis实例部署,不使用集群环境。如果数据量过亿甚至更大,就需要对Geo数据进行拆分,按国家
拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个zset集合的大小。

9、大海捞针:Scan

   在平时线上Redis维护工作中,有时候需要从Redis实例成千上万的key中找出特定前缀的key列表来手动处理数据,可能是修改它的值,也
可能是删除key。这里就有一个问题,如何从海量的key中找出满足特定前缀的key列表来?
   Redis提供了一个简单暴力的指令keys用来列出所有满足特定正则字符串规则的key。缺点:没有offset、limit参数,事件复杂度O(n),key
过多时会导致卡顿
   
   Redis为了解决这个问题,它在2.8版本中加入了大海捞针的指令——scan。scan相比keys具备有以下特点:
1)、复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程
2)、提供limit参数,可以控制每次返回结果的最大条数,limit只是一个hint,返回的结果可多可少
3)、同keys一样,它也提供模式匹配功能
4)、服务器不需要为游标保存状态,游标的唯一状态就是scan返回给客户端的游标整数
5)、返回的结果可能会有重复,需要客户端去重复,这点非常重要
6)、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的
7)、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零

   scan提供了三个参数,第一个是cursor整数值,第二个是key的正则模式,第三个是遍历的limit hint。第一次遍历时,cursor值为0,然
后将返回结果中第一个整数值作为下一次遍历的cursor。一直遍历到返回的cursor值为0时结束。
    scan 0 match key99* count 1000     --> 返回cursor13796作为下次遍历的cursor
    scan 13976 match key99* count 1000

    scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨
个遍历就行了。limit参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能
是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将limit数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一
次性返回给客户端。
    
    scan 的遍历顺序非常特别。它不是从第一维数组的第0位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式
进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
    
    在平时的业务开发中,要尽量避免大key的产生。有时候会因为业务人员使用不当,在Redis实例中会形成很大的对象,比如一个很大的
hash,一个很大的zset这都是经常出现的。这样的对象对Redis的集群数据迁移带来了很大的问题,因为在集群环境下,如果某个key太大,
会数据导致迁移卡顿。另外在内存分配上,如果一个key太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果
这个大key被删除,内存会一次性回收,卡顿现象会再一次产生。
    不过Redis官方已经在redis-cli指令中提供了大key扫描功能。第二条指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,
但是扫描的时间会变长。
    redis-cli -h 127.0.0.1 -p 7001 –-bigkeys
    redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1

如何进行Redis深度分析

如何进行Redis深度分析

10、鞭辟入里:线程IO模型

1) 定时任务
   服务器处理要响应IO事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在select系统调用上,定时任务将无
法得到准时调度。那Redis是如何解决这个问题的呢?
   Redis的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis都会将最
小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参
数。因为Redis知道未来timeout时间内,没有其它定时任务需要处理,所以可以安心睡眠timeout的时间。
   Nginx 和 Node 的事件处理原理和 Redis 也是类似的

11、交头接耳:通信协议

   Redis的作者认为数据库系统的瓶颈一般不在于网络流量,而是数据库自身内部逻辑处理上。所以即使Redis使用了浪费流量的文本协议,
依然可以取得极高的访问性能。
   RESP(Redis Serialization Protocol). RESP是Redis序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简单,解析性能
极好。

12、未雨绸缪:持久化

   当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的
页面是没有变化的,还是进程产生时那一瞬间的数据。子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,
再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。
   
   Redis4.0混合持久化
   重启Redis时,我们很少使用rdb来恢复内存状态,因为会丢失大量数据。我们通常使用AOF日志重放,但是重放AOF日志性能相对rdb来说要
慢很多,这样在Redis实例很大的情况下,启动需要花费很长的时间。Redis4.0为了解决这个问题,带来了一个新的持久化选项——混合持久化。
将rdb文件的内容和增量的AOF日志文件存在一起。这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量
AOF日志,通常这部分AOF日志很小。
   于是在Redis重启的时候,可以先加载rdb的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重启效率因此大幅得
到提升。

13、开源节流:小对象压缩

1) 小对象压缩
   如果Redis内部管理的集合数据结构很小,它会使用紧凑存储形式压缩存储。如果它存储的是hash结构,那么key和 value会作为两个entry
相邻存在一起。如果它存储的是zset,那么value和score会作为两个entry相邻存在一起。
   存储界限 当集合对象的元素不断增加,或者某个value值过大,这种小对象存储也会被升级为标准结构。
   
2) 内存回收机制
   Redis并不总是可以将空闲内存立即归还给操作系统。
   如果当前Redis内存有10G,当你删除了1GB的key后,再去观察内存,你会发现内存变化不会太大。原因是操作系统回收内存是以页为单位,
如果这个页上只要有一个key还在使用,那么它就不能被回收。Redis虽然删除了1GB的key,但是这些key分散到了很多页面中,每个页面都还有
其它key存在,这就导致了内存不会立即被回收。
   不过,如果你执行flushdb,然后再观察内存会发现内存确实被回收了。原因是所有的key都干掉了,大部分之前使用的页面都完全干净了,
会立即被操作系统回收。
   Redis虽然无法保证立即回收已经删除的key的内存,但是它会重用那些尚未回收的空闲内存。这就好比电影院里虽然人走了,但是座位还在,
下一波观众来了,直接坐就行。而操作系统回收内存就好比把座位都给搬走了。这个比喻是不是很6?

14、有备无患:主从同步

1) CAP理论
   C:Consistent,一致性
   A:Availbility,可用性
   P:Partition tolerance,分区容忍性
   
   分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇
叫着「网络分区」。
   在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的「一致
性」将无法满足,因为两个分布式节点的数据不再保持一致。除非我们牺牲「可用性」,也就是暂停分布式节点服务,在网络分区发生时,不
再提供修改数据的功能,直到网络状况完全恢复正常再继续对外提供服务。
   一句话概括CAP原理就是——网络分区发生时,一致性和可用性两难全。

看完上述内容,你们对如何进行Redis深度分析有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注亿速云行业资讯频道,感谢大家的支持。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI