一文弄懂Redis

引言

本文从实际工做中方案选型出发,以Redis的特性为着眼点,逐层剥开Redis技术内幕,并对工做中容易出现的使用误区进行了总结。redis

面临的问题

对于有状态的服务而言,数据库每每会成为系统的瓶颈所在。在用户活跃的高峰期,或者因为PUSH、活动等引起的请求突增,都会给后端的数据库形成巨大的压力。数据库

由存储系统的特性咱们知道,从内存读一个数据,比从通常的磁盘读要快10000倍左右,基于这样的缘由,数据库自己也会有必定的内存cache。可是当热数据集比较大的时候,本地cache会频繁淘汰,此时会触发大量磁盘IO,性能急剧降低,每每也会伴随有大量的慢日志。另外,有些数据是须要经过复杂的查询或计算后获得且又不会频繁变化的。后端

虽然说数据库能够经过读写分离来扩展读的能力,但存在增长slave实例的成本、主从延迟致使数据不一致等问题。子曾经曰过,“计算机科学领域的任何问题能够经过增长一个中间层来解决”,因而咱们考虑在系统中再增长一个cache层,这里暂不讨论cache的设计实现。数组

让数听说话

须要cache的热点数据缓存

  • 用户我的资料
  • 关注、粉丝、好友列表
  • 关注数、粉丝数、好友数
  • 用户之间的关系
  • 用户浏览过的TopN...

基于以上数据的特色,咱们最终选择了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.网络

以上引自官网的原话,归纳起来有这样一些特性:数据结构

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

看得见的优点

0. 性能强悍

虽然是单进程单线程模型,可是读写性能很是优异,单机可支持10wQPS,缘由主要有如下几点:多线程

  • 纯内存操做,避免了与磁盘的交互
  • 单线程,天生的队列模式,避免了因多线程竞争而致使的上下文切换和抢锁的开销
  • 事件机制,Redis服务器将全部处理的任务分为两类事件,一类是采用I/O多路复用处理客户端请求的网络事件;一类是处理定时任务的时间事件,包括更新统计信息、清理过时键、持久化、主从同步等;

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

1. 支持持久化

对于内存型数据库,好比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模式执行,至于持久化的具体实现这里就不展开讨论了。

2. 丰富的数据类型

比较巧妙的是,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实现了有序,可支持范围查找;有集合序对象用的就比较普遍了,好比排行榜(只要是排序相关的列表均可以)、延迟任务队列等。

3. 高可用

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的数据只能保证最终一致,不能保证强一致性。

4. 可扩展

读扩展,基于主从架构,能够很好的平行扩展读的能力。写扩展,主要受限于主服务器的硬件资源的限制,一是单个实例内存容量受限,二是一个实例只使用到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 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过时的话,很可能致使主线程阻塞。通常能够经过作散列来优化处理。

参考资料

相关文章
相关标签/搜索