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有这样几个核心特性:
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虽然是单进程单线程模型,但是读写性能非常优异,单机可支持10wQPS,原因主要有以下几点:
当然这种单线程事件机制也是有缺陷的,由于所有的事件都是串行执行,一旦某个事件比较重就会阻塞其它事件,从而导致整个系统的吞吐率下降。比如某个客户端执行了一个比较重的lua函数、或者使用了诸如keys*、zrange(0,-1)、hgetall等全集合扫描的操作,又或者删除的过期键是个big key,又或者使用了较多内存的redis实例进行bgsave时,都会导致服务器一定程度的阻塞,一般伴随会有相应的慢日志。所以我们在实际使用redis的过程中,必须要给每一次的操作分配合理的时间片。
对于内存型数据库,比如redis和memcache,如果数据状态不落盘,一旦服务器进程退出,那么这些数据状态也就会全部消失不见。数据状态的重建需要从后端数据库回源,这会给后端数据库造成非常大的压力,最坏的情况可能会把数据库压垮,导致服务不可用。
为了解决这个问题,Redis提供了RDB和AOF两种持久化方式。前者会生成一份内存快照--RDB文件,该文件是经过压缩的二进制格式,记录的是键值对数据;后者则是以Redis的命令请求协议格式来保存,记录的是命令操作;
由于RDB SAVE和AOF重写会阻塞主线程,所以都支持BG模式执行,至于持久化的具体实现这里就不展开讨论了。
比较巧妙的是,Redis并没有使用固定的数据结构来存储各种类型的数据,而是创建了一套对象系统,对于同一个对象,可以对应一个或多个不同的底层数据结构(或者叫做编码方式),某些特定的编码方式在时空间的效率上有所优化,通过执行"Object Encoding"可以查询当前编码方式。
Redis的高可用,主要通过主从复制机制以及Sentinel集群来实现。
当从服务器有2个或者多个时,Redis的主从架构可以有两种形式。一种是,所有的从服务器直接挂在主服务器上,这种模式的优点是,所有从服务器复制的延迟相对较低,而缺点在于加大了主服务器的复制压力;另一种形式,是采用级联的方式,S1从M复制,S2从S1复制,以此类推,这种模式的优点是,将主服务器的复制压力分摊到多个服务器上,而缺点在于越处于级联下游的从实例,复制延迟就越大。
从主从复制模式可以看出,Redis的数据只能保证最终一致,不能保证强一致性。
读扩展,基于主从架构,可以很好的平行扩展读的能力。写扩展,主要受限于主服务器的硬件资源的限制,一是单个实例内存容量受限,二是一个实例只使用到CPU一个核。下面讨论基于多套主从架构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不可能比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的数据全部放在内存带来了高速的性能,但是也带来一些不合理之处。比如一个中型网站有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中提到两个原因。
要想成功使用一种产品,我们需要深入了解它的特性。Redis性能突出,如果能够熟练的驾驭,对其他技术产品的分析使用也会更有体会。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8