Redis 常见面试知识点小结
马刺2021-12-07

淘系工程师分享关于Redis 的方方面面。

Redis是什么

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.


如果你认为Redis是一个key value store, 那可能会用它来代替MySQL;如果认为它是一个可以持久化的cache, 可能只是用它保存一些频繁访问的临时数据。Redis是REmote DIctionary Server的缩写,在Redis在官方网站的的副标题是A persistent key-value database with built-in net interface written in ANSI-C for Posix systems,这个定义偏向key value store。还有一些看法则认为Redis是一个memory database,因为它的高性能都是基于内存操作的基础。另外一些人则认为Redis是一个data structure server,因为Redis支持复杂的数据特性,比如List, Set等。对Redis的作用的不同解读决定了你对Redis的使用方式。

互联网数据目前基本使用两种方式来存储,关系数据库或者key value。但是这些互联网业务本身并不属于这两种数据类型,比如用户在社会化平台中的关系,它是一个list,如果要用关系数据库存储就需要转换成一种多行记录的形式,这种形式存在很多冗余数据,每一行需要存储一些重复信息。如果用key value存储则修改和删除比较麻烦,需要将全部数据读出再写入。Redis在内存中设计了各种数据类型,让业务能够高速原子的访问这些数据结构,并且不需要关心持久存储的问题,从架构上解决了前面两种存储需要走一些弯路的问题。

简单概括起来Redis有这样几个核心特性:

  1. 纯内存数据存储,并且支持多种持久化
  2. 支持丰富的数据结构,比如string,hash,list,set,sorted set,bitmap,hyperloglog, geospatial index等
  1. 支持复制、Lua脚本、LRU淘汰、事务
  2. 基于Sentinel实现高可用
  1. Cluster模式支持自动分区

Redis使用场景

Redis使用场景主要有缓存、排行榜、计数器、分布式会话、分布式锁、社交网络、最新列表、消息系统等。

缓存

对于有状态的服务而言,数据库往往会成为系统的瓶颈所在。在用户活跃的高峰期,或者由于PUSH、活动等引发的请求突增,都会给后端的数据库造成巨大的压力。
由存储系统的特性我们知道,从内存读一个数据,比从一般的磁盘读要快10000倍左右,基于这样的原因,数据库本身也会有一定的内存cache。但是当热数据集比较大的时候,本地cache会频繁淘汰,此时会触发大量磁盘IO,性能急剧下降,往往也会伴随有大量的慢日志。另外,有些数据是需要通过复杂的查询或计算后得到且又不会频繁变化的。
虽说数据库可以通过读写分离来扩展读的能力,但存在增加slave实例的成本、主从延迟导致数据不一致等问题。于是我们考虑在系统中再增加一个cache层,此时Redis就能够帮我们解决这样的缓存需求。

排行榜

很多网站都有排行榜应用,如淘宝的月度销量榜单、商品按时间的上新排行等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。

计数器

什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得+1,并发量高时如果每次都请求数据库操作无疑会对数据库提出挑战。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。

分布式会话

集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。

分布式锁

在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。

社交网络

点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。

最新列表

Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。

消息系统

消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

Redis 性能

Redis虽然是单进程单线程模型,但是读写性能非常优异,单机可支持10wQPS,原因主要有以下几点:

  1. 纯内存操作,避免了与磁盘的交互
  2. 用hash table作为键空间,查找任意的key只需O(1)
  1. 单线程,天生的队列模式,避免了因多线程竞争而导致的上下文切换和抢锁的开销
  2. 事件机制,Redis服务器将所有处理的任务分为两类事件,一类是采用I/O多路复用处理客户端请求的网络事件;一类是处理定时任务的时间事件,包括更新统计信息、清理过期键、持久化、主从同步等;

当然这种单线程事件机制也是有缺陷的,由于所有的事件都是串行执行,一旦某个事件比较重就会阻塞其它事件,从而导致整个系统的吞吐率下降。比如某个客户端执行了一个比较重的lua函数、或者使用了诸如keys*、zrange(0,-1)、hgetall等全集合扫描的操作,又或者删除的过期键是个big key,又或者使用了较多内存的redis实例进行bgsave时,都会导致服务器一定程度的阻塞,一般伴随会有相应的慢日志。所以我们在实际使用redis的过程中,必须要给每一次的操作分配合理的时间片。

Redis持久化

对于内存型数据库,比如redis和memcache,如果数据状态不落盘,一旦服务器进程退出,那么这些数据状态也就会全部消失不见。数据状态的重建需要从后端数据库回源,这会给后端数据库造成非常大的压力,最坏的情况可能会把数据库压垮,导致服务不可用。

为了解决这个问题,Redis提供了RDB和AOF两种持久化方式。前者会生成一份内存快照--RDB文件,该文件是经过压缩的二进制格式,记录的是键值对数据;后者则是以Redis的命令请求协议格式来保存,记录的是命令操作;

  1. RDB的特点,文件体积小,加载速度快;但因为是对整个实例的内存生成快照,所以操作比较重,一般持久化的间隔不宜太快,所以保存的数据相对比较旧一些;
  2. AOF的特点,文件体积较大(可以用AOF重写进行覆盖);所有的写操作会追加到AOF缓冲区,持久化的行为可配置,分为三种,always(每次刷盘)、everysec(异步线程每隔1秒刷一次)和no(只写到page cache,交给操作系统来刷盘);相对来说,AOF文件数据保存的比较新一些,所以如果开启了AOF,那么Redis服务器恢复的时候会优先加载AOF文件。

由于RDB SAVE和AOF重写会阻塞主线程,所以都支持BG模式执行,至于持久化的具体实现这里就不展开讨论了。

Redis丰富的数据类型

比较巧妙的是,Redis并没有使用固定的数据结构来存储各种类型的数据,而是创建了一套对象系统,对于同一个对象,可以对应一个或多个不同的底层数据结构(或者叫做编码方式),某些特定的编码方式在时空间的效率上有所优化,通过执行"Object Encoding"可以查询当前编码方式。

  1. String 字符串对象,最大可支持512MB,memcache最大只支持1MB。编码可以是int,raw或者embstr,int对应整型数据,可方便计数,embstr用来保存长度小于等于39字节的字符串值,采用连续的空间进行存储,更好利用缓存优势;字符串对象常用来进行计数,或者缓存序列化的对象;
  2. List 列表对象,编码可以是ziplist或linkedlist,ziplist是为了节约内存而开发的,是一个经过特殊编码的连续内存块组成的顺序结构,当列表对象元素的个数较少以及元素的长度较短时会采用这种方式;列表对象一般可用来实现消息队列;
  1. Hash 哈希对象,编码可以是ziplist或hashtable,在Redis的实现里,采用的链式冲突来解决冲突问题,并且为了维护hash表的负载因子在一个合理的范围,会执行渐进式rehash;哈希对象一般用于存储某个对象的属性数据,便于选择性查询,这个效率要比粗暴的序列化和反序列化要高很多,比如用户的个人资料;另一个用法,则是利用ziplist编码方式实现压缩存储,节省内存;
  2. Set 集合对象,编码可以是intset或hashtable,当集合的元素不多且都是整数时,Redis就会使用整数集intset,底层是一个以有序、无重复的方式进行排列的数组,能有效的节约内存;这个对象一般用于去重,比如派奖;
  1. Sorted Set 有序集合对象,编码可以是ziplist或者skiplist,跳跃表skiplist是一种查找效率可媲美平衡树的数据结构,平均O(logN),最坏O(N),而且实现更加简单;其实,Redis用了skiplist和hashtable两种数据结构来实现zset,一方面hashtable能实现O(1)的查找,另一方面skiplist实现了有序,可支持范围查找;有集合序对象用的就比较广泛了,比如排行榜(只要是排序相关的列表都可以)、延迟任务队列等。

Redis的高可用

Redis的高可用,主要通过主从复制机制以及Sentinel集群来实现

  1. 主从复制 分为两个阶段,首先,当从服务器发起SYNC命令后,主服务器会生成最新的RDB文件发送给从服务器,并使用一个缓冲区来记录从此刻开始主服务器执行的所有写命令;待RDB文件传输完之后,再将该缓冲区的数据再发送给从服务器,这样就完成了复制。旧的Redis版本有个缺陷是,如果在第二个阶段发生失败,需要从第一个阶段重新开始同步,而这个阶段的操作会消耗大量的CPU、内存和磁盘I/O以及网络带宽资源,太过耗费资源。所以从2.8版本开始,实现了部分重同步,通过主从服务器各维护一个复制偏移量来实现。
  2. Sentinel 由一个或多个Sentinel实例组成的哨兵系统,可以监视任意多个主从服务器,并完成Failover的操作。Sentinal其实是一个运行在特殊模式下的Redis服务器,运行期间,会与各服务器建立网络连接,以检测服务器的状态;同时会与其它Sentinel服务器创建连接,完成信息交换,比如发现某个主服务器心跳异常时,会互相询问心跳结果,当超过一定数量时即可判定为客观下线;一旦主服务器被判定为客观下线状态,那么Sentinel集群会通过raft协议选举,选出一个Leader来执行Failover。
  1. Failover 一般来说,会先选出优先级最高的从服务器,然后再从中选出复制偏移量最大的实例,作为新的主服务器;最后将其它从和旧的主都切换为新主的从。

当从服务器有2个或者多个时,Redis的主从架构可以有两种形式。一种是,所有的从服务器直接挂在主服务器上,这种模式的优点是,所有从服务器复制的延迟相对较低,而缺点在于加大了主服务器的复制压力;另一种形式,是采用级联的方式,S1从M复制,S2从S1复制,以此类推,这种模式的优点是,将主服务器的复制压力分摊到多个服务器上,而缺点在于越处于级联下游的从实例,复制延迟就越大。

从主从复制模式可以看出,Redis的数据只能保证最终一致,不能保证强一致性。

Redis的扩展性

读扩展,基于主从架构,可以很好的平行扩展读的能力。写扩展,主要受限于主服务器的硬件资源的限制,一是单个实例内存容量受限,二是一个实例只使用到CPU一个核。下面讨论基于多套主从架构Redis实例的集群实现,目前主要有以下几种方案:

  1. 客户端分片 实现方案,业务进程通过对key进行hash来分片,用Sentinel做failover。优点:运维简单,每个实例独立部署;可使用lua脚本,业务进程执行的key均hash到同一个分片即可;缺点:一旦重新分片,由于数据无法自动迁移,部分数据需要回源;
  2. Redis集群 是官方提供的分布式数据库方案,通过分片实现数据共享,并提供复制和failover。按照16384个槽位进行分片,且实例之间共享分片视图。优点:当发生重新分片时,数据可以自动迁移;缺点:客户端需要升级到支持集群协议的版本;客户端需要感知分片实例,最坏的情况,每个key需要一次重定向;不支持lua脚本;不支持pipeline;
  1. Codis 是由豌豆荚团队开源的一款分布式组件,它将分布式的逻辑从Redis集群剥离出来,交由几个组件来完成,与数据的读写解耦。Codis proxy负责分片和聚合,dashboard作为管理后台,zookeeper做配置管理,Sentinel做failover。优点:底层透明,客户端兼容性好;重新分片时,数据可自动迁移;支持pipeline;支持lua脚本,业务进程保证执行的key均hash到同一个分片即可;缺点:运维较为复杂;引入了中间层;

Redis使用误区

键过大

Redis的key是string类型,最大可以是512MB,那么实际中是不是也可以这样用呢?答案是否定的,redis将key保存在一个全局的hashtable,如果key过大,一是占用过多的内存,二是计算hash和字符串比较都会更耗时;一般建议key的大小不超过2kB。

Big key

或者说是big value,这会导致删除key的操作比较耗时,会阻塞主线程。比如有些同学喜欢用集合类的对象,动辄上百万的元素。对于这类超大集合,一般有两种优化方案,一是采取分片的方式,将每个集合分片控制在较小的范围内,比如小于1000个元素;二是起一个异步任务,对集合中的元素分批进行老化。

全集合扫描

比如在业务代码使用了keys*,hgetall,zrange(0, -1)等返回集合中所有元素,这些都属于阻塞操作,一般考虑用scan,hscan等迭代操作代替。

单个实例内存过大

内存过大有什么问题呢?上文中在讲到持久化的时候其实有说到,无论是生成RDB文件,还是AOF重写,都是要对整个实例的内存数据进行扫描,非常消耗CPU和磁盘资源;当使用Backgroud方式创建子进程时也会涉及到内存空间的拷贝,即便使用了COW机制,也会占用相当的内存开销。另外,在主从复制的第一阶段,save、传输和加载RDB文件的开销,也会随着RDB文件的变大而变大。当单个实例达到瓶颈时,更好的解决方案应该是采用集群方案。

大量key同时过期

redis删除过期键采用了惰性删除和定期删除相结合的策略,惰性删除则是在每次GET/SET操作时去删,定期删除,则是在时间事件中,从整个key空间随机取样,直到过期键比率小于25%,如果同时有大量key过期的话,极可能导致主线程阻塞。一般可以通过做散列来优化处理。

Redis不可能比Memcache快?

很多开发者都认为Redis不可能比Memcached快,Memcached完全基于内存,而Redis具有持久化保存特性,即使是异步的,Redis也不可能比Memcached快。但是测试结果基本是Redis占绝对优势,主要原因有两个。

  1. Libevent。和Memcached不同,Redis并没有选择libevent。Libevent为了迎合通用性造成代码庞大(目前Redis代码还不到libevent的1/3)及牺牲了在特定平台的不少性能。Redis用libevent中两个文件修改实现了自己的epoll event loop。业界不少开发者也建议Redis使用另外一个libevent高性能替代libev,但是作者还是坚持Redis应该小巧并去依赖的思路。
  2. CAS问题。CAS是Memcached中比较方便的一种防止竞争修改资源的方法。CAS实现需要为每个cache key设置一个隐藏的cas token,cas相当value版本号,每次set会token需要递增,因此带来CPU和内存的双重开销,虽然这些开销很小,但是到单机10G+ cache以及QPS上万之后这些开销就会给双方带来一些细微性能差别。

单台Redis的存放数据是否必须比物理内存小

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中提到两个原因。

  1. OS的VM换入换出是基于Page概念,比如OS VM 1个Page是4K, 4K中只要还有一个元素即使只有1个字节被访问,这个页也不会被SWAP, 换入也同样道理,读到一个字节可能会换入4K无用的内存。而Redis自己实现则可以达到控制换入的粒度。
  2. 访问操作系统SWAP内存区域时block进程,也是导致Redis要自己实现VM原因之一。

总结

要想成功使用一种产品,我们需要深入了解它的特性。Redis性能突出,如果能够熟练的驾驭,对其他技术产品的分析使用也会更有体会。

团队介绍

我们是阿里巴巴淘系技术部的新品平台技术团队, 依托于淘系大数据正在建立一套完整的涵盖消费者洞察、宏观及细分市场分析、竞争分析、市场策略研究、产品创新机制等的新品研发和创新孵化平台, 为品牌、商家及行业提供规模化的新品孵化和运营能力, 沉淀新品孵化机制和运营策略, 最终建立起一套基于大数据驱动的从市场研究、新品研发到新品投放营销的全链路新品运营平台。发送邮件到tianhang.th#alibaba-inc.com(发送邮件时,请把#替换成@)