今天就跟大家聊聊有关如何进行Redis深度分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。
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) 使用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) 将请求转移至延时队列,过一会再试 这种方式比较适合异步消息处理,将当前冲突的请求扔到另一个队列延后处理以避开冲突。
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实现延迟队列 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(); } } }
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
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就可以派 上用场了。
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请求,然后再去磁盘进 行查询。 邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中, 这个就是误判所致,概率很低。
除了控制流量,限流还有一个应用目的是用于控制用户行为,避免垃圾请求。比如在UGC社区,用户的发帖、回复、点赞等行为都要严格受 控,一般要严格限定某行为在规定时间内允许的次数,超过了次数那就是非法行为。对非法行为,业务必须规定适当的惩处策略。
//这个限流需求中存在一个滑动时间窗口,想想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)); } } }
Redis4.0提供了一个限流Redis模块,它叫redis-cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有了这个模块,限流问题就 非常简单了。 cl.throttle lwh:reply 15 30 60 1 上面这个指令的意思是允许「用户lwh回复行为」的频率为每 60s 最多 30 次(漏水速率),漏斗的初始容量为 15,也就是说一开始可以连 续回复 15 个帖子,然后才开始受漏水速率的影响。
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集合的大小。
在平时线上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
1) 定时任务 服务器处理要响应IO事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在select系统调用上,定时任务将无 法得到准时调度。那Redis是如何解决这个问题的呢? Redis的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis都会将最 小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参 数。因为Redis知道未来timeout时间内,没有其它定时任务需要处理,所以可以安心睡眠timeout的时间。 Nginx 和 Node 的事件处理原理和 Redis 也是类似的
Redis的作者认为数据库系统的瓶颈一般不在于网络流量,而是数据库自身内部逻辑处理上。所以即使Redis使用了浪费流量的文本协议, 依然可以取得极高的访问性能。 RESP(Redis Serialization Protocol). RESP是Redis序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简单,解析性能 极好。
当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的 页面是没有变化的,还是进程产生时那一瞬间的数据。子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了, 再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。 Redis4.0混合持久化 重启Redis时,我们很少使用rdb来恢复内存状态,因为会丢失大量数据。我们通常使用AOF日志重放,但是重放AOF日志性能相对rdb来说要 慢很多,这样在Redis实例很大的情况下,启动需要花费很长的时间。Redis4.0为了解决这个问题,带来了一个新的持久化选项——混合持久化。 将rdb文件的内容和增量的AOF日志文件存在一起。这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF日志,通常这部分AOF日志很小。 于是在Redis重启的时候,可以先加载rdb的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重启效率因此大幅得 到提升。
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?
1) CAP理论 C:Consistent,一致性 A:Availbility,可用性 P:Partition tolerance,分区容忍性 分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇 叫着「网络分区」。 在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的「一致 性」将无法满足,因为两个分布式节点的数据不再保持一致。除非我们牺牲「可用性」,也就是暂停分布式节点服务,在网络分区发生时,不 再提供修改数据的功能,直到网络状况完全恢复正常再继续对外提供服务。 一句话概括CAP原理就是——网络分区发生时,一致性和可用性两难全。
看完上述内容,你们对如何进行Redis深度分析有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注亿速云行业资讯频道,感谢大家的支持。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。