Redis 常见面试知识点小结
互联网数据目前基本使用两种方式来存储,关系数据库或者key value。但是这些互联网业务本身并不属于这两种数据类型,比如用户在社会化平台中的关系,它是一个list,如果要用关系数据库存储就需要转换成一种多行记录的形式,这种形式存在很多冗余数据,每一行需要存储一些重复信息。如果用key value存储则修改和删除比较麻烦,需要将全部数据读出再写入。Redis在内存中设计了各种数据类型,让业务能够高速原子的访问这些数据结构,并且不需要关心持久存储的问题,从架构上解决了前面两种存储需要走一些弯路的问题。
纯内存数据存储,并且支持多种持久化 支持丰富的数据结构,比如string,hash,list,set,sorted set,bitmap,hyperloglog, geospatial index等 支持复制、Lua脚本、LRU淘汰、事务 基于Sentinel实现高可用 Cluster模式支持自动分区
Redis使用场景主要有缓存、排行榜、计数器、分布式会话、分布式锁、社交网络、新列表、消息系统等。
▐ 缓存
对于有状态的服务而言,数据库往往会成为系统的瓶颈所在。在用户活跃的高峰期,或者由于PUSH、活动等引发的请求突增,都会给后端的数据库造成巨大的压力。
由存储系统的特性我们知道,从内存读一个数据,比从一般的磁盘读要快10000倍左右,基于这样的原因,数据库本身也会有一定的内存cache。但是当热数据集比较大的时候,本地cache会频繁淘汰,此时会触发大量磁盘IO,性能急剧下降,往往也会伴随有大量的慢日志。另外,有些数据是需要通过复杂的查询或计算后得到且又不会频繁变化的。
虽说数据库可以通过读写分离来扩展读的能力,但存在增加slave实例的成本、主从延迟导致数据不一致等问题。于是我们考虑在系统中再增加一个cache层,此时Redis就能够帮我们解决这样的缓存需求。
▐ 排行榜
什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得+1,并发量高时如果每次都请求数据库操作无疑会对数据库提出挑战。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。
▐ 分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
▐ 分布式锁
Redis虽然是单进程单线程模型,但是读写性能非常优异,单机可支持10wQPS,原因主要有以下几点:
纯内存操作,避免了与磁盘的交互
用hash table作为键空间,查找任意的key只需O(1)
单线程,天生的队列模式,避免了因多线程竞争而导致的上下文切换和抢锁的开销
事件机制,Redis服务器将所有处理的任务分为两类事件,一类是采用I/O多路复用处理客户端请求的网络事件;一类是处理定时任务的时间事件,包括更新统计信息、清理过期键、持久化、主从同步等;
当然这种单线程事件机制也是有缺陷的,由于所有的事件都是串行执行,一旦某个事件比较重就会阻塞其它事件,从而导致整个系统的吞吐率下降。比如某个客户端执行了一个比较重的lua函数、或者使用了诸如keys*、zrange(0,-1)、hgetall等全集合扫描的操作,又或者删除的过期键是个big key,又或者使用了较多内存的redis实例进行bgsave时,都会导致服务器一定程度的阻塞,一般伴随会有相应的慢日志。所以我们在实际使用redis的过程中,必须要给每一次的操作分配合理的时间片。
对于内存型数据库,比如redis和memcache,如果数据状态不落盘,一旦服务器进程退出,那么这些数据状态也就会全部消失不见。数据状态的重建需要从后端数据库回源,这会给后端数据库造成非常大的压力,坏的情况可能会把数据库压垮,导致服务不可用。
为了解决这个问题,Redis提供了RDB和AOF两种持久化方式。前者会生成一份内存快照--RDB文件,该文件是经过压缩的二进制格式,记录的是键值对数据;后者则是以Redis的命令请求协议格式来保存,记录的是命令操作;
RDB的特点,文件体积小,加载速度快;但因为是对整个实例的内存生成快照,所以操作比较重,一般持久化的间隔不宜太快,所以保存的数据相对比较旧一些;
AOF的特点,文件体积较大(可以用AOF重写进行覆盖);所有的写操作会追加到AOF缓冲区,持久化的行为可配置,分为三种,always(每次刷盘)、everysec(异步线程每隔1秒刷一次)和no(只写到page cache,交给操作系统来刷盘);相对来说,AOF文件数据保存的比较新一些,所以如果开启了AOF,那么Redis服务器恢复的时候会优先加载AOF文件。
由于RDB SAVE和AOF重写会阻塞主线程,所以都支持BG模式执行,至于持久化的具体实现这里就不展开讨论了。
比较巧妙的是,Redis并没有使用固定的数据结构来存储各种类型的数据,而是创建了一套对象系统,对于同一个对象,可以对应一个或多个不同的底层数据结构(或者叫做编码方式),某些特定的编码方式在时空间的效率上有所优化,通过执行"Object Encoding"可以查询当前编码方式。
String 字符串对象,大可支持512MB,memcache大只支持1MB。编码可以是int,raw或者embstr,int对应整型数据,可方便计数,embstr用来保存长度小于等于39字节的字符串值,采用连续的空间进行存储,更好利用缓存优势;字符串对象常用来进行计数,或者缓存序列化的对象;
List 列表对象,编码可以是ziplist或linkedlist,ziplist是为了节约内存而开发的,是一个经过特殊编码的连续内存块组成的顺序结构,当列表对象元素的个数较少以及元素的长度较短时会采用这种方式;列表对象一般可用来实现消息队列;
Hash 哈希对象,编码可以是ziplist或hashtable,在Redis的实现里,采用的链式冲突来解决冲突问题,并且为了维护hash表的负载因子在一个合理的范围,会执行渐进式rehash;哈希对象一般用于存储某个对象的属性数据,便于选择性查询,这个效率要比粗暴的序列化和反序列化要高很多,比如用户的个人资料;另一个用法,则是利用ziplist编码方式实现压缩存储,节省内存;
Set 集合对象,编码可以是intset或hashtable,当集合的元素不多且都是整数时,Redis就会使用整数集intset,底层是一个以有序、无重复的方式进行排列的数组,能有效的节约内存;这个对象一般用于去重,比如派奖;
Sorted Set 有序集合对象,编码可以是ziplist或者skiplist,跳跃表skiplist是一种查找效率可媲美平衡树的数据结构,平均O(logN),坏O(N),而且实现更加简单;其实,Redis用了skiplist和hashtable两种数据结构来实现zset,一方面hashtable能实现O(1)的查找,另一方面skiplist实现了有序,可支持范围查找;有集合序对象用的就比较广泛了,比如排行榜(只要是排序相关的列表都可以)、延迟任务队列等。
Redis的高可用,主要通过主从复制机制以及Sentinel集群来实现。
主从复制 分为两个阶段,首先,当从服务器发起SYNC命令后,主服务器会生成新的RDB文件发送给从服务器,并使用一个缓冲区来记录从此刻开始主服务器执行的所有写命令;待RDB文件传输完之后,再将该缓冲区的数据再发送给从服务器,这样就完成了复制。旧的Redis版本有个缺陷是,如果在第二个阶段发生失败,需要从个阶段重新开始同步,而这个阶段的操作会消耗大量的CPU、内存和磁盘I/O以及网络带宽资源,太过耗费资源。所以从2.8版本开始,实现了部分重同步,通过主从服务器各维护一个复制偏移量来实现。
Sentinel 由一个或多个Sentinel实例组成的哨兵系统,可以监视任意多个主从服务器,并完成Failover的操作。Sentinal其实是一个运行在特殊模式下的Redis服务器,运行期间,会与各服务器建立网络连接,以检测服务器的状态;同时会与其它Sentinel服务器创建连接,完成信息交换,比如发现某个主服务器心跳异常时,会互相询问心跳结果,当超过一定数量时即可判定为客观下线;一旦主服务器被判定为客观下线状态,那么Sentinel集群会通过raft协议选举,选出一个Leader来执行Failover。
Failover 一般来说,会先选出优先级高的从服务器,然后再从中选出复制偏移量大的实例,作为新的主服务器;后将其它从和旧的主都切换为新主的从。
当从服务器有2个或者多个时,Redis的主从架构可以有两种形式。一种是,所有的从服务器直接挂在主服务器上,这种模式的优点是,所有从服务器复制的延迟相对较低,而缺点在于加大了主服务器的复制压力;另一种形式,是采用级联的方式,S1从M复制,S2从S1复制,以此类推,这种模式的优点是,将主服务器的复制压力分摊到多个服务器上,而缺点在于越处于级联下游的从实例,复制延迟就越大。
从主从复制模式可以看出,Redis的数据只能保证终一致,不能保证强一致性。
读扩展,基于主从架构,可以很好的平行扩展读的能力。写扩展,主要受限于主服务器的硬件资源的限制,一是单个实例内存容量受限,二是一个实例只使用到CPU一个核。下面讨论基于多套主从架构Redis实例的集群实现,目前主要有以下几种方案:
客户端分片 实现方案,业务进程通过对key进行hash来分片,用Sentinel做failover。优点:运维简单,每个实例独立部署;可使用lua脚本,业务进程执行的key均hash到同一个分片即可;缺点:一旦重新分片,由于数据无法自动迁移,部分数据需要回源;
Redis集群 是官方提供的分布式数据库方案,通过分片实现数据共享,并提供复制和failover。按照16384个槽位进行分片,且实例之间共享分片视图。优点:当发生重新分片时,数据可以自动迁移;缺点:客户端需要升级到支持集群协议的版本;客户端需要感知分片实例,坏的情况,每个key需要一次重定向;不支持lua脚本;不支持pipeline;
Codis 是由豌豆荚团队开源的一款分布式组件,它将分布式的逻辑从Redis集群剥离出来,交由几个组件来完成,与数据的读写解耦。Codis proxy负责分片和聚合,dashboard作为管理后台,zookeeper做配置管理,Sentinel做failover。优点:底层透明,客户端兼容性好;重新分片时,数据可自动迁移;支持pipeline;支持lua脚本,业务进程保证执行的key均hash到同一个分片即可;缺点:运维较为复杂;引入了中间层;
Redis的key是string类型,大可以是512MB,那么实际中是不是也可以这样用呢?答案是否定的,redis将key保存在一个全局的hashtable,如果key过大,一是占用过多的内存,二是计算hash和字符串比较都会更耗时;一般建议key的大小不超过2kB。
或者说是big value,这会导致删除key的操作比较耗时,会阻塞主线程。比如有些同学喜欢用集合类的对象,动辄上百万的元素。对于这类超大集合,一般有两种优化方案,一是采取分片的方式,将每个集合分片控制在较小的范围内,比如小于1000个元素;二是起一个异步任务,对集合中的元素分批进行老化。
比如在业务代码使用了keys*,hgetall,zrange(0, -1)等返回集合中所有元素,这些都属于阻塞操作,一般考虑用scan,hscan等迭代操作代替。
内存过大有什么问题呢?上文中在讲到持久化的时候其实有说到,无论是生成RDB文件,还是AOF重写,都是要对整个实例的内存数据进行扫描,非常消耗CPU和磁盘资源;当使用Backgroud方式创建子进程时也会涉及到内存空间的拷贝,即便使用了COW机制,也会占用相当的内存开销。另外,在主从复制的阶段,save、传输和加载RDB文件的开销,也会随着RDB文件的变大而变大。当单个实例达到瓶颈时,更好的解决方案应该是采用集群方案。
redis删除过期键采用了惰性删除和定期删除相结合的策略,惰性删除则是在每次GET/SET操作时去删,定期删除,则是在时间事件中,从整个key空间随机取样,直到过期键比率小于25%,如果同时有大量key过期的话,极可能导致主线程阻塞。一般可以通过做散列来优化处理。
很多开发者都认为Redis不可能比Memcached快,Memcached完全基于内存,而Redis具有持久化保存特性,即使是异步的,Redis也不可能比Memcached快。但是测试结果基本是Redis占优势,主要原因有两个。
Libevent。和Memcached不同,Redis并没有选择libevent。Libevent为了迎合通用性造成代码庞大(目前Redis代码还不到libevent的1/3)及牺牲了在特定平台的不少性能。Redis用libevent中两个文件修改实现了自己的epoll event loop。业界不少开发者也建议Redis使用另外一个libevent高性能替代libev,但是作者还是坚持Redis应该小巧并去依赖的思路。
CAS问题。CAS是Memcached中比较方便的一种防止竞争修改资源的方法。CAS实现需要为每个cache key设置一个隐藏的cas token,cas相当value版本号,每次set会token需要递增,因此带来CPU和内存的双重开销,虽然这些开销很小,但是到单机10G+ cache以及QPS上万之后这些开销就会给双方带来一些细微性能差别。
Redis的数据全部放在内存带来了高速的性能,但是也带来一些不合理之处。比如一个中型网站有100万注册用户,如果这些资料要用Redis来存储,内存的容量必须能够容纳这100万用户。但是业务实际情况是100万用户只有5万活跃用户,1周来访问过1次的也只有15万用户,因此全部100万用户的数据都放在内存有不合理之处,RAM需要为冷数据买单。
这跟操作系统非常相似,操作系统所有应用访问的数据都在内存,但是如果物理内存容纳不下新的数据,操作系统会智能将部分长期没有访问的数据交换到磁盘,为新的应用留出空间。现代操作系统给应用提供的并不是物理内存,而是虚拟内存(Virtual Memory)的概念。
基于相同的考虑,Redis 2.0也增加了VM特性。让Redis数据容量突破了物理内存的限制。并实现了数据冷热分离。
Redis的VM依照之前的epoll实现思路依旧是自己实现。但是OS也可以自动帮程序实现冷热数据分离,Redis只需要OS申请一块大内存,OS会自动将热数据放入物理内存,冷数据交换到硬盘。作者antirez在解释为什么要自己实现VM中提到两个原因。
OS的VM换入换出是基于Page概念,比如OS VM 1个Page是4K, 4K中只要还有一个元素即使只有1个字节被访问,这个页也不会被SWAP, 换入也同样道理,读到一个字节可能会换入4K无用的内存。而Redis自己实现则可以达到控制换入的粒度。
访问操作系统SWAP内存区域时block进程,也是导致Redis要自己实现VM原因之一。
要想成功使用一种产品,我们需要深入了解它的特性。Redis性能突出,如果能够熟练的驾驭,对其他技术产品的分析使用也会更有体会。
来源
https://mp.weixin.qq.com/s?src=11×tamp=1640829587&ver=3527&signature=Sb9W7RfRYPAMjDamJJEL-drRzfuF*v9kCkeS0Hkxy2COguWgD-JTH3lGUYq7ptHYxlBbbf7-VHIPoicJrgPct2FtA4mbi8P2pLtY3WMRQl3Lojm4RGyg6bi7OvPNqWDp&new=1
相关文章