单线程的Redis为何如此之快?
Redis是一款使用C语言编写、可基于内存也可以持久化的日志型、Key-Value型开源数据库。它可以用作:数据库、缓存和消息中间件,也是目前最受欢迎的一款缓存中间件,总所周知,redis的核心操作是单线程实现的,那么为什么他还能有如此高的效率?本文将从数据结构,进程线程模型等方面进行简要叙述,顺便提一下redis的常见问题与算法
简单但是高效的数据结构
redis支持的5种基本数据类型
Redis效率如此高的原因之一是他专门设计了一套KV数据结构,常用的数据结构主要有线面五种:
数据类型 | 定义 | 使用场景 |
---|---|---|
字符串(String) | Redis的基本数据类型,一个Key对应一个Value | 缓存、计数器、分布式锁等 |
哈希(Hash) | Hash是一个string类型的key和value的映射表,特别适合用于存储对象 | 用户信息、Hash 表等 |
列表(List) | List是简单的字符串列表,按照插入顺序排序。可以添加一个元素导列表的头部(左边)或者尾部(右边)。相当于链表 | 链表、队列、微博关注人时间轴列表等 |
集合(Set) | Set是string类型的无序集合捕鱼必须重复,通过哈希表实现,添加,删除,查找的复杂度都是O(1) | 去重、赞、踩、共同好友等 |
有序集合(Zset) | zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。通过分数来为集合中的成员进行从小到大的排序 | 访问量排行榜、点击量排行榜等 |
以及,范围查询,Bitmaps,Hyperloglogs 和地理空间(Geospatial)索引半径查询
Redis为什么快?
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,官方提供的数据是可以达到 100000+ 的QPS(每秒内查询次数)
原因分析:
-
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
-
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的,如SDS,跳跃表等等
-
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
-
使用多路I/O复用模型,非阻塞IO;
-
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis其他常见知识点简单总结
Redis与Memcached区别
- Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
- 内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
- 性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。
- 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是redis目前是原生支持cluster模式的,redis官方就是支持redis cluster集群模式的,比memcached来说要更好。
缓存雪崩
redis缓存的key在同一时间大量的失效,导致大量的请求同事打到数据库,造成数据库压力过大 解决方案:
- 合理地设置缓存的过期时间,过期时间加上随机值
- 热点数据永不过期,定时刷新缓存数据
- 互斥锁,拿到所再去访问数据库,性能有影响
缓存穿透
大量请求Redis并不存在的数据,导致请求大量的打到数据库,可能是攻击页可能是非法参数 解决方案:
- 互斥锁,拿到所再去访问数据库,性能有影响
- 返回null,异步更新
- 合法参数校验, 布隆过滤器
- 数据库查询为空的对象也放入缓存,但是过期时间设置的比较短
缓存击穿
热点key过期,导致大量请求直接达到数据库,造成数据库压力剧增 解决方案:
- 互斥锁,分布式锁,只有一个线程能够抢到这个锁,也就只有一个线程能够进入到数据库,然后将数据放到缓存,其他线程就能从缓存拿到这个数据
- 永不过期,定时刷新缓存
持久化
bgsave 手动,太麻烦
rdb配置文件配置持久化策略
省心 还是可能会会丢失数据
AOF
配置appendonly yes 有点:实时记录命令 文件大,时间长文件会很大
可以和rdb结合使用
主从同步
主从服用,读写分离
发布订阅模式
哨兵模式
主服务器挂掉之后,重新选出一个主服务器进行读写,主服务器恢复之后还是能作为集群的节点
key的过期淘汰机制
虽然Redis可以对缓存的key设置过期时间,但是并不是过期时间到了缓存的数据就一定会被淘汰
定期删除
默认每秒钟扫描10次,也就是每100ms扫描一次,过期扫描不会扫描全部数据(扫描全部性能小消耗太大),而是采用一种很简单的贪心策略:
- 从过期字典中随机选择20个key
- 删除这些key中的过期key
- 如果删除的元素超过1/4就继续重复步骤1和2
惰性删除
查询的时候会看key是否过期,过期的话删除数据,不返回任何内容 弥补了定期删除的不足,可能有很多已经过期的key在定期删除的时候并没有被成功地删掉 定期删除是集资红处理,惰性删除则是零散处理
内存淘汰策略
不管是定期删除还是惰性删除,都不是一个完全精准的删除,还是会有key没有被删除的情况存在,于是就需要内存淘汰策略
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
- volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的
- allkeys-random:加入键的时候如果过限,从所有key随机删除
- volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-lfu:从所有键中驱逐使用频率最少的键
【参考】 【1】《Redis设计与实现》
转载自:https://juejin.cn/post/7031916434972213285