1. redis介绍
REmoteDIctionaryServer(Redis)是一个开源(BSD协议)的,使用ANSI C语言编写,基于内存in-memory数据存储结构,可以当作NoSQL数据库,缓存和消息代理来使用。支持多种数据结构,包括string, hashes, lists, set, sorted sets, bitmaps, hyperloglogs等。此外,Redis还支持内置replication, lru (Least Recently Used )算法管理缓存,Pub/Sub,部分原子性操作,简单事务支持,基于磁盘的数据持久化以及3.0实现了分布式Redis支持HA,久违的Redis Cluster。从2013年5月开始,Redis的开发由Pivotal公司主持。
2. redis之父Antirez
饮水思源,redis之父Salvatore Sanfilippo, 一名来自意大利西西里岛(Sicily)出生于1977年的程序员,网名Antirez, 常居住于Catania。Antirez的IT职业生涯开始于系统管理员,IT安全,并于Web 2.0的年代创立一家web网络公司,主要开发社交应用。之后,在一次实时统计分析产品开发中,为了节省成本以及高性能扩展性,Antirez意识到需要一种支持多种复杂数据结构的in-memory数据库,并且支持快速操作。Redis就此诞生并开源。之后,VM慧眼相中了Redis, 并雇佣Antirez去全职开发,而之后衍生出Pivotal公司,Antirez则继续主持Redis开发。 —— 此处摘抄作者介绍。
3. redis适用场景
redis被大家称为数据结构服务器,一种相较于轻量级的kv数据存储,支持丰富的数据类型。redis以其速度而闻名,这使得其称为某一特定领域适用的最优选择。使用大多数的情况还是缓存,那为什么不用memcached呢?又或者干脆选择Java自带容器arrayList, hashMap,concurrentHashMap, HashSet呢?接下来就唠唠。
3.1 互联网/社交
redis的用户多为互联网公司如Twitter, Weibo(微博), Uber, StackOverflow, Airbnb, Alibaba等。对于社交网站Twitter, Weibo,这些网站主要使用Redis进行高效的社交关系管理,缓存用户好友,粉丝,关注等,并且可以通过Redis内置数据模型快速计算共同好友等。
我们先看简单来看看各大社交网站Twitter, Weibo(微博是国内最大的Redis用户)。这些网站的每个用户都有好友,粉丝,关注等,并且可以相互查看共同好友等。redis可以用来帮助高效管理这些社交关系, 如求共同好友:
如上,Redis很方便的使用有序集合的交集功能实现。其中user:100000:follow为用户1的粉丝列表,user:200000:follow为用户2的粉丝列表,out:100000:200000为交集共同好友列表。又如另一个常用场景,电商各种计数,比如说商品维度计数(关注度,喜欢数,评论数,浏览数等)。都可以采用redis的Hash类型,并借助其原子性操作来维护计数,如图
用户维度计数(动态,关注数,粉丝数,发帖数等)
除了这些互联网用途,redis最常用的功能还是被当作缓存。提到缓存,大家当然会想到memcached。
redis v.s. memcached
- 数据类型与操作:redis拥有更多丰富的数据结构支持与操作,而memcached则需客户端自己处理并进行网络交互
- 内存使用率:简单k/v存储,memcached内存利用率更高(使用了slab与大小不同的chunk来管理内存),而如果采用redis Hash来存储则其组合压缩,内存利用率远高于memcached
- 性能:总体来说,二者性能接近;redis使用了单核(单线程IO复用,封装了aeEvent事件处理框架,实现了epoll,kqueue,select),memcached采用了多核,各有利弊;当数据大于100K的时候,memcached性能是高于redis的,memcached还存在钙化的问题,可以采用slab_authmove开启解决。
- 数据持久化:redis支持数据文件持久化,rdb与aof两种策略;memcached则不支持
- 分布式:memcached本身并不支持服务器端分布式,客户端只能借助一致性哈希分布式算法来实现memcached分布式存储;当然redis也是从3.0版本开始才支持服务器端cluster的,以前靠哨兵模式,这个想了解可以去官网历史版本查看,重要的是现在支持了。
- 其他方面:redis提供其他一些功能,如Pub/Sub, Queue, 简单transacation, peplication等。
好了,千言不抵一图:
Redis v.s. HashMap
作为单机数据缓存,Java自带容器也是备选方案之一。我们进行压力测试,采用concurrentHashMap, memcached(假设近似于redis), MySQL进行benchmark。
具体数据如下:
可以看出,hashMap(上图显示为一根线)整体操作插入/查询/移除都 25 - 90倍快于memcached/redis; 而memcached/redis又 5 -12 倍快于mySQL InnoDB存储引擎,符合预期。究其原因,Java容器单机内存直接读取,无网络开销,无须序列化等。而反之之所以要用Redis的原因与好处则包括:
hashMap单机受限于内存容量,而正是redis分布式之优势
- hashMap当数据量超过一定限制后,需要妥善管理堆内存,不然会造成内存溢出或者memory Leak;redis则具备了文件持久性,以及failover达到HA.
- hashMap只能受限于本机,而redis天生分布式,可以让多个appServer访问,负载均衡。
所以redis适合全部数据都在内存的场景包括需要临时持久化,尤其作为缓存来使用,并支持对缓存数据进行简单处理计算;如涉及redis与rdbms双向同步的话,则需要引入一些复杂度。
4. redis核心功能
4.1 pipelining
reids是一个tcp客户端-服务器模式,使用了应请求/响应协议,客户端与服务器的交互是阻塞的方式。如下一个请求/响应事例,客户端发起4个incr命令,服务端依次响应:
- client:incr x
- server:1
- client:incr X
- server:2
- client:incr X
- server:3
- Client:incr X
- server:4
客户端与服务器通过网络连接,影响性能的关键因素往返时间(rtt round trip time)则受限于网络速度,如在网速较慢情况下rtt需要250毫秒,哪怕服务器性能卓越可以每秒处理1万个请求,然而整体则客户端受网络制约只能每秒处理4个请求。即便使用了loopback接口节省rtt时间,但如果需要大量的写操作,整体性能仍不可观。
redis引入了批处理,管道(pipelining),即客户端可以一次发送多个命令给服务器,无须等待服务器的
返回,而服务器端则回将请求放入一个有序的管道中,在执行完成后,一次性将返回值发送回客户端。
上述事例在使用pipelining时如下,客户端通过缓冲区,一次性批量发送请求incr,服务端则处理后统一
返回结果:
- client:incr X
- client:incr X
- client:incr X
- client:incr X
- server:1
- server:2
- server:3
- server:4
4.2 pub/sub
reids提供了解耦消息的发布/订阅(pub/sub)通信模式,pub/sub采用事件作为基本通信机制,支持时间(非同时),空间(无须知道具体位置)与同步(可异步模式)等解耦合,而发布与订阅则通过通道(channel)来通信。
据Antirez称,最初引入pub/sub是因为应用户需求,并且redis本身的架构已经支持pub/sub, 所以只用了150行代码就把pub/sub已实现的内部功能暴露称pub/sub api。
该图为较早版本中发布消息的一段代码片段,可以看出消息保存在一个List链表数据结构中,消息的类型为messagebulk。另外,redis也支持通配符模式匹配的订阅方式。
pub/sub v.s. 消息中间件
reids的pub/sub与消息中间件相比,并不支持持久化,如系统宕机,网络问题等都会造成消息丢失;redis的消息多用于实时性较高的消息推送,并不保证可靠性。redis的消息也无法支持水平扩展,如通过增加consumer或者订阅者分组之类进行负载均衡。性能方面,redis的pub/sub与常用消息中间件如rabbitMQ比较,则伯仲之间。
上图以处理10万个消息为例,在发布消息方面,rabbitMQ的耗时为redis的75%;订阅消费消息
rabbitMQ耗时为redis的86%,可见整体性能redis已与消息中间件相近了。
4.3 lru
lru(Least Recently Used) 最近最久未使用算法,是多数缓存系统当内存受限时自动清理旧数据的常用常用算法之一。当redis使用内存达到配置maxmemory时,redis会根据配置的policy进行数据置换处理,其中策略包括如下:
- noenviction(不清除)
- allkeys-lru(从所有数据集选择最近最少用)
- volatile-lru(从设置过期时间的数据集选择最近最少用)
- allkeys-random(所有数据集随机选取淘汰),
- volatile-random(以设置过期时间数据集中随机),
- volatile-ttl(从已设置过期过期时间的数据集中选择,非lru)
redis出于性能及节约内存考量,采用的并非严格意义lru算法算法,而是近似的lru算法,即redis通过采样一小部分键,然后在样本池中进行lru。当然在redis 3.0中,算法进一步改进为维护回收候选键池,改善了性能同时更接近于lru算法行为。
下图为官网提供reids lru性能测试,分别为理论lru算法,10个样本的redis 3.0 lru算法,以及5个样本的redis 2.8与3.0算法的表现,其中浅灰色表示被置换出去的key,灰色为没有被置换的key,绿色为新增的key。
测试算法较简单,首先默认导入一定数目key,之后从第一个key遍历至最后一个key(相当于按时间顺序使用过了当前遍历的key),之后再增加50%的数目触发清理策略。可以看出,理论算法为最旧的50%被替换;同样样本为5情况下,3.0表现要优于2.8,3.0则更接近于理论值,2.8算法则略为逊色。
4.4 transactions
绝大多数noSQL选择不支持事务,而reids以命令方式(multi, exec, discard, wa watch)提供了简单的
类/伪事务支持,之所以称之为类/伪事务,是因为redis的只保证了事务必须acid的c(一致性),i(隔离性),
并不保证a(原子性)与持久性(d),redis事务甚至不支持回滚操作。
实际上,redis的事务提供了一种将多个命令打包并置入事务队列,之后批量一次性,有序的按照先进先出(fifo)的顺序执行机制。事务在执行过程中不会被中断,所有命令命令执行之后,事务才结束。
transaction v.s. pipelining
redis的transaction与pipelining都是批量执行命令,其主要差别为:
- pipelining主要是提供了网络层面的优化,客户端通过缓存多个命令并按批次发送给服务端处理以节省网络rtt(round trip tIme)时间开销,但这些命令并不保证事务性(Redis的单线程保证了单个命令本身是原子的,但多个client可以发起多个命令串行执行)。
- transaction则保证了一个client发起的事务中的命令可以连续的执行,中间不会插入其它client的命令,而此则受益于redis的单线程模式,很容易实现。
所以,transaction与pipelining不但不冲突,对于批量命令甚至可以结合起来使用以达到网络优化及事务性
保证双重收效。
4.5 persistence
redis提供了两种常用的不同级别的磁盘持久化方式,rdb与aof(append-only-file)。rdb持久化在指定的时间间隔生成数据集的时间点快照(snapshot), aof则类似rdms的binlog日志。 由于单线程运行,redis的rdb在备份时为不影响系统正常使用,借助linux的fork命令及copy on write机制,在fork出的子进程中进行备份写rdb文件。rdb的备份相对简单并且文件小,但无法保证数据的完整性,如redis在rdb的间隔时间内宕机,则面临丢失期间的数据。rdb较适合定期的归档备份及方便灾难恢复。而aof则提供了更好的持久性,Redis会将每个命令都追加到AOF文件中,并提供了三种持久化策略:
- no fsync at all:redis不调用fsync持久化, 操作系统决定同步时机(多数30秒)
- fsync everysecond:每秒(延迟会两秒)进行一次fsync将缓冲区数据写入磁盘
- fsync always:每个命令都进行同步,持久化写入磁盘,安全但较慢
aof后台执行的方式与rdb类似,也是借助fork开启子进程进行写入aof文件。为了减少aof文件的大小,redis提了了文件压缩命令,另外支持日志重写,后台重构AOF文件以减少所需命令。
4.6 replication
redis支持master/slave主从配置,实现弱一致性,支持负载均衡,提高HA。redis的master可以对应多个slave, slave也可以级多个slave。通常master负责读写服务,slave负责读服务,master与slave间靠replication定期来进行同步。
redis的主从结构如下图所示dag(有向无环图)
slave会定期给master发送sync命令进行同步,如果第一次连接则进行全量同步否则增量同步。master则启动后台快找进程saving来收集最近修改数据集所有命令并将其用db file传输给slave。
另外,Redis从2.8版本开始支持中断后(网络断开等故障)的断点续传功能,无须重新同步。
master维护一个内存缓冲区,主从服务器都维护一个复制偏移量(offset)和master run id,
当断开重新连接后,master判断两个master run id是否相同,并根据指定的偏移量继续断点续传。
5. redis系统架构
sentinel架构
官网文字介绍:reids sentinel诞生于2012年(Redis 2.4版本),建议在单机redis或者客户端模式cluster的时候(非 3.0版本Redis Cluster)采用,作为HA, Failover来使用。
sentinel主要提供了集群管理,包括监控,通知,自动故障恢复。如上图,当其中一个master无法正常工作时,sentinel将把一个slave提升为master, 从而自动恢复故障。而sentinel本身也做到了分布式,可以部署多个sentinel实例来监控redis实例(建议为基数,至少3个sentinel实例来监控一组redis master/slaves),多个sentinel进程间通过gossip协议来确定master是否宕机,通过Agreement协议来决定是否执行故障自动迁移以及重新选主,整体设计类似zk。
cluster架构
官网文字介绍:redis cluster开始设计于2011年(早于sentinel),正式诞生于2015年愚人节,redis 3.0,其中之苦辣酸甜只有antirez自己知道。
如上图,从3.0开始, redis从一个单纯的noSQL内存数据库变成了一个真正分布式noSQL数据库。概括来说,所谓分布式即支持数据分片,并且自动管理故障恢复(failover)与备份(replication)。
如上图redis cluster采用了无中心结构,每个节点都保存,共享数据和集群状态,每个节点与其它所有节点通信,使用gossip协议来传播及发现新节点,通过分区来提供一定程度可用性,当某个node的master宕机时,cluste会自动选举一个slave形成一个新的master,这里应该是借鉴,重用了sentinel的功能。另外,redis cluster并没有使用通常的一致性哈希, 而引入哈希槽的概念,cluster中固定有16384个slot, 每个key通过CRC16校验后对16384取模来决定其对应slot的位置,而每个node负责一部分的slot管理,当node变化时,动态调整slot的分布,而数据则无须挪动。对于客户端来说,client可以向任意一个实例请求,Cluster会自动定位需要访问的slot。
上图查询路由过程中,我们随机发送到任意一个redis实例,这个实例会按照上文提到的crc16校验后取模定位,并转发至正确的redis实例中。
然而,完全去中心化的架构同时也失去了一些灵活与总控能力,如可通过引入中央控制的自动发现节点的变化及时Rebalance,分区粒度的备份,故障时分区自动调整,gossip消息的通信开销,路由表维护等。但是值得一提的是,redis的企业版/商业版redis labs enterprise cluster(rlec)则似乎是解决了我们上述问题,如引入了中央控制cluster manager来管理,监控,分片迁移等工作, 引入高性能proxy隐藏幕后的路由实现等。
当然,前提是商业化也是付费版本了,我们也期待未来的开源redis逐步可以引入这些概念。
6. redis 内部数据结构
redis支持丰富的数据类型,并提供了大量简单高效的功能。为了高效使用redis,开发设计人员需要对redis的数据结构进行选型,如选择链表还是集合等。 下面我们快速了解一下redis内部数据结构及其代码实现。redis的底层数据结构总览图:
上图列出了redis内部底层的一些重要数据结构,包括list, set, hash, string等。
来看几个比较核心的的数据结构。
redis Object
redis 3.x后的redisObject如下图所示:
redisObject定义中使用了位字段(bit filed)。简单来说,redisObject定义了类型,编码方式,
lru时间,引用计数,*ptr指向实际保存值指针。
- type: redisObject的类型,字符串,列表,集合,有序集,哈希表等
- encoding: 底层实现结构,字符串,整数,跳跃表,压缩列表等
- ptr:实际指向保存值的数据结构
举个具体例子,redisObject{type: redis_list, encoding:redis_encoding_linkedlist}, 这个对象是redis列表,其值保存在一个链表中,ptr指针指向这个列表。用惯了jvm的虚拟机,我们也来回顾一下c或者redis是如何管理对象的。总体而言,redis自己实现对象管理机制,并基于引用计数的垃圾回收。
毫无疑问的直接malloc开辟内存空间,设置type,encoding,引用计数默认1,设置默认lru。可以看出refcount,猜测其内部是基于引用计数来管理释放内存空间的。事实要以代码为准。
果然,redis提供了incrrefcount与decrrefcount来管理对象跟踪对象的引用, 当减少引用时检测计数器为是否需要释放内存对象。
redisdb
redisdb内部数据结构,封装了数据库层面的信息:
从redisdb来看,几个重要属性:
- id:数据库内部编号,仅供内部操作使用,如AOF等
- dict:存放整个数据库的键值对,键为字符串,值为Redis的数据结构,如list, set, hash等。
- expires:键的过期时间
具体代码如下:
代码注释较为清晰,其中long long是c99标准新加的,64位长整型。
redisserver
redisserver代码在redis 3.0支持Cluster后变得较复杂,我只列出部分代码。
总体来说包含如下几个大部分:
- 通用部分:如pid进程id,数据库指针,命令字典表,sentinel模式标志位等
- 网络信息:如port tCP监听端口,cluster bus监听socket, 已使用slot数量,active的客户端列表, 当前客户端,slaves列表等。
- 其它信息:如aof信息,统计信息, 配置信息(如已经配置总db数量dbnum等),日志信息,replication配置,pub/sub, cluster信息,Lua脚本信息配置等等。
redis hash
redis的哈希表/字典是其核心数据结构之一,值得深入研究。redis hash数据结构, hash新创建时,在不影响效率情况下,redis默认使用zipmap作为底层实现以节省空间,只有当size超出一定限制后(hash-max-zipmap-entries ),redis才会自动把zipmap转换为下图hash table。
上图字典的底层实现为哈希表,每个字典包含2个哈希表,ht[0], ht[1], 1号哈希表是在rehash过程中才
使用的。而哈希表则由dictentry构成。
代码层面,每个字典包含了3个内部数据结构:
- dict:字典的根结构,包含了2个dictht,其中2作为rehashing之用
- dictht:包含了linkedlist dictentry
- dictentry:包含了3个数据结构(double/uint64_6/int64t)的链表,类似java hashmap中的entry结构
hash算法
目前redis中引入了一些经典哈希算法,而HashTable则主要为以下两种:
- murmurhash2 32bit算法:著名的非加密型哈希函数,能产生32位或64位哈希值,最新版本为
- murmurhash3。该算法针对一个字符串进行哈希,可表现较强离散性。
- 基于djb算法实现散列算法:该算法较为简单,同样是将字符串转换为哈希值。主要利用字符串中
- 的ascii码与一个随机seed,进行变换得到哈希值。
评估一个哈希算法的优劣,主要看其哈希值的离散均匀效果以及消除冲突程度。redis在hashtable中引入的上述两种算法不失简单高效,离散均匀。
rehash
类似java中的hashmap, 当有新键值对添加到redis字典时,有可能会触发rehash。redis中处理哈希碰撞的方法与java一样,都是采用链表法,整个哈希表的性能则依赖于它的大小size和它已经保存节点数量used的比率。比率在1:1时,哈希表的性能最好,如果节点数量比哈希表大小大很多的话,则整个哈希表就退化成多个链表,其性能优势全无。
上图的哈希表,平均每次失败查找需要访问5个节点。为了保持高效性能,在不修改键值对情况下,
需要进行rehash,目标是将ratio比率维持在1:1左右。
ratio = used / size
rehash触发条件:
- 自然rehash:ratio >= 1, 且变量dict_can_resize为真
- 强制rehash:ratio大于dict_force_resize_ratio(v 3.2.1版本为5)
rehash执行过程:
- 创建ht[1]并分配至少2倍于ht[0] table的空间
- 将ht[0] table中的所有键值对迁移到ht[1] table
- 将ht[0]数据清空,并将ht[1]替换为新的ht[0]
redis哈希为了避免整个rehash过程中服务被阻塞,采用了渐进式的rehash,即rehash程序激活后,并不是
马上执行直到完成,而是分多次,渐进式(incremental)的完成。同时,为了保证并发安全,在执行rehash
中间执行添加时,新的节点会直接添加到ht[1]而不是ht[0], 这样保证了数据的完整性与安全性。
另一方面,哈希的rehash在还提供了创新的(相对于java hashmap)收缩(shrink)字典,当可用节点远远
大于已用节点的时候,rehash会自动进行收缩,具体过程与上面类似以保证比率始终高效使用。
7. 总结一下
我从redis的一些核心功能,适用范围与本质原因,分布式架构设计,cluster,重要内部数据结构及代码片段等层面简单深入了解,在redis使用方面,开发及设计人员通常需要对redis有一定深度理解,对业务模型进行相应的数据结构选型,还需提前预估内存使用,是否需要持久化,分布式等。总体来说,Redis还是非常适合与传统关系型数据库结合使用,做高性能数据缓存,轻量级消息队列及跨机器共享内存。希望该文章能对redis开发人员有所帮助,碰到一些实现细节,建议还是要深入其源代码一探究竟。有关其它redis的命令操作及其它高级功能则可参考redis官网。