redis为什么这么快?
- 采用的是多路复用io:在多路复用 IO模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。
- 数据结构简单,操作快。使用了简单动态字符串(SDS)。Redis的链表为双端链表,链表节点带有perv和next指针,链表还带有head和tail指针,使得获取链表某节点前后置节点的时间复杂度都是O(1)。
- 单线程,不需要线程上下文的切换。计算机通过CPU寄存器和程序计数器记录了进程、线程等任务的位置,从而实现保存任务的位置,以及再次运行任务,让任务看起来是连续运行的每次上下文切换都需要花费几十纳秒到数微秒的CPU时间,也就是说如果频繁的进行上下文切换会导致CPU大部分时间被浪费。也不用考虑锁的问题,不存在加锁释放锁的操作,没有因为可能出现死锁而导致的性能消耗。Redis为了近可能的减少客户端等待,使用WATCH命令对数据加锁,只会在数据被其他客户端修改时,才会通知执行WATCH的客户端,之后的事务不会执行。这种加锁方式被称为乐观锁,极大的提升了Redis的性能。
- 完全基于内存,在内存中执行,操作内存中的数据,不需要过多的IO和磁盘交互。
Redis作为缓存时的双写一致性问题如何解决?(推荐文章)
- 1、给缓存设置过期时间,过期就读数据库的数据。
- 2、先更新数据库,然后删除缓存。(延时双删)
缓存雪崩问题
雪崩问题的意思就是大量缓存的过期时间设为一致,然后同一时间失效,这样就会同时读数据库,
给数据造成巨大的压力。
所以,我们在设置缓存过期时间的时候,要加一个随机值来保证过期时间的不一致。
缓存穿透问题
缓存穿透就是恶意查询不存在的数据,然后去查询数据库,导致数据库连接异常。
- 采用异步更新,如果缓存不存在,异步起一个线程去读数据库,然后更新缓存。
- 利用互斥锁,当缓存失效的时候,不能直接访问数据库,而是要先获取到锁,才能去请求数据库。没得到锁,则休眠一段时间后重试。
redis字典结构,hash冲突怎么办,rehash,负载因子?(参考)
哈希对象的底层编码是ziplist或者hashtable(字典)
当哈希对象保存的所有键对值的键和值的长度都是小于64字节并且键对值数量小于512个的时候,使用ziplist。
保存键对值的时候,现将键压至栈底,再将值压至栈底。
不满足条件的时候使用hash表。
Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。
随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)【哈希表已保存的节点数/哈希表的大小】维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。
-
扩容:当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
-
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
服务器目前正在执行BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ; -
收缩: 当负载因子小于等于0.1时,会执行收缩操作。
redis的hash字典扩容的时候,为了防止redis里面的数据很多,导致一次扩容复制元素的时间太长,导致服务在一段时间内停止,采取了渐进式hash,也就是说并不是一次将原来的数组的内容全部转移到新的数组上面,而是同时维护的两个数组,同时操作两个数组,同时也在不断地复制,复制完成之后就删除原来的数组。如果在扩容的时候访问的下标大于新的数组已经完成了复制的部分的最大下标就返回原来的数组下标位置的值。
redis的list类型和set类型的底层实现(参考文章)
list底层实现是一个双向链表。
set底层实现是intset或者hashtable。
集合对象使用intset编码需要满足两个条件:一是所有元素都是整数值;二是元素个数小于等于512个;不满足任意一条都将使用hashtable编码。
以上第二个条件可以在Redis配置文件中修改et-max-intset-entries选项。
redis字符串实现,sds和c区别,空间预分配
Redis字符串实现: Redis没有直接通过C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串的抽象类型,sds是redis自己实现的一种数据结构,用来作为redis底层默认字符串,与c语言的字符串区别开来。并将SDS用作Redis的默认字符串表示。
两者的比较:传统c字符串与sds比较:
- sds数据结构中也是用字符数组存储字符串,但是带有两个额外参数:len(记录字符串长度)和free(未使用空间)想要获得传统c字符串的长度不得不遍历整个字符串,然而sds则可直接读取len值。降低了时间复杂度
- 两者的字符数组都是以空字符’/0’结尾,在sds中此空字符不计入len中但是也同样分配一个字节空间,空字符的相关操作都是由sds的API自动完成的,所以对于开发者来说此空字符是透明的。sds保持和c字符串一致以空字符作为结尾是为了能够复用c语言的字符串函数库里的函数。
- 传统c字符串如果在对字符串操作没有注意空间剩余有可能会出现内场溢出的现象,而sds的API中执行拼接操作的函数sdscat,拼接时会先判断空间是否足够,如果不够则会先执行扩容操作,从而杜绝内场溢出.
- 避免频繁内存重分配:传统c字符串的长度为n+1(空字符),每一次append时需要重新分配内存,否则内存溢出;如果trim字符串,后面不需要的空间也要释放,否则内存泄露。内存重分配设计复杂算法且可能需要系统调度,不符合redis的速度要求。而sds通过free-未使用空间来解除了底层数组长度与字符串长度间的关联,sds拥有空间预分配与惰性空间释放两钟优化策略。
- 空间预分配
-
用于字符串增长操作,当字符串增长时,程序会先检查需不需要对SDS空间进行扩展,如果需要扩展,程序不仅会为SDS分配修改所必要的空间,还会为SDS分配额外的未使用空间,额外分配的未使用空间公式如下:
① 如果对SDS修改之后,SDS的长度(修改之后len属性的值)小于1MB,那么则分配和len属性同样大小的未使用空间,这时SDS的len属性和free属性的值相同。如:如果修改之后SDS的len将变为10字节,那么程序也会分配10字节的未使用空间,SDS的buf数组实际长度变为10 + 10 + 1 = 21(额外一个字节用于保存结束符\n)
② 如果对SDS修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间。如:修改之后的len将变为10MB,那么程序会分配1MB的未使用空间,SDS的bug数组长度为10MB + 1MB + 1byte - 惰性空间释放
-
惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
Redis的Zset为什么不一直使用zipList或者skiplist?为什么选怎跳表而不是红黑树?
之所以使用ziplist是因为他的优点,它被设计为一块内存紧凑的数据结构,占用连续内存,可以利用cpu缓存,同时对不同的数据进行不同的编码,节省内存的开销。
之所以不一致使用,是应为两个缺点:
- 不能保存过多的元素,否则查询效率就会降低,O(n);
- 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。
当有序集合中元素小于128个并且所有元素的长度都小于64字节,使用ziplist,ziplist保存的方式也是先保存键,再保存值,键和值是挨着的,元素是按照值由小变大排序的。
当不满足ziplist的两个条件的时候,使用的是skiplist,skiplist,这样查找的时间复杂度是logn,虽然没有ziplist节省内存,但是查询效率高。
- 插入、删除、查找、迭代输出的复杂度和红黑树一样。
- 但是,范围查找数据比红黑树要快。
- 跳表更灵活,可通过改变索引构建策略,有效平衡执行效率和内存消耗
Redis的大key对系统的影响