这篇文章是一系列redis研究学习的总集篇。文章主要是以提纲、概要的形式将redis的研究学习串联涵盖在一篇文章的篇幅之内,然后在需要细节描述的地方会引用外部的详细描述。

NoSqlFan是一个非常棒的nosql数据相关前沿技术网站,其中有非常多的redis内容,这里给出它的汇总链接:Redis资料汇总专题
本文基本只关注在redis作为数据库来看几个比较critical的功能点,关于未提到的一些周边功能,可以查看Redis系统性介绍,这篇文章里有详细的介绍。

目录

  1. redis的简介、优点及安装
  2. redis的存储数据类型描述、接口定义及场景范例
  3. redis的内存分配逻辑
  4. redis的持久化策略
  5. redis的主从策略
  6. redis的集群策略
  7. 参考资料列表

1. redis的简介、优点及安装

1.1 简介

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。Redis提供了一些丰富的数据结构,包括 lists, sets, ordered sets 以及 hashes ,当然还有和Memcached一样的 strings结构.Redis当然还包括了对这些数据结构的丰富操作。

1.2 redis的优点

  • 性能极高 – Redis能支持超过 100K+ 每秒的读写频率。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

1.3 redis的安装

安装并不复杂,MAC下的安装请参考我写的blog:在MAC下安装redis以及其PHP扩展

2. redis的存储数据类型描述、接口定义及场景范例

redis非常特色的一点就是能支持非常丰富的存储数据类型,有Strings、Lists、Hashes、Sets及Ordered Sets。这里就另成一篇,请参考我的另一篇博客:Redis数据类型介绍接口定义及简单范例

3. redis的内存分配逻辑

3.1 基础概念

redis会将所有的数据从磁盘上导入到内存中,所有的数据操作全部在内存中进行,不存在部分数据在磁盘部分数据在内存里的情况,所以提前预估和节约内存非常重要。

3.2 redis的内存分配

redis和memcache非常不同的一点其实并不在其支持的数据类型上,而是内存分配上。memcache的内存非配是预分配形式的,以page为分配单位,slabs为分割单位,chunk为存储单位,保证了内存碎片的情况不会发生,当然,内存浪费是这种内存分配方案的坏处。redis则完全自主分配内存,在请求到的时候实时根据内建的算法分配内存,完全自主控制内存的管理。为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中统一使用zmalloc,zfree一系列函数,位于zmalloc.h,zmalloc.c文中。

上边说过,封装就是为了屏蔽底层平台的差异,同时方便自己实现相关的统计函数。具体来说就是:

  • 若系统中存在Google的TC_MALLOC库,则使用tc_malloc一族函数代替原本的malloc一族函数。
  • 若当前系统是Mac系统,则使用<malloc/malloc.h>中的内存分配函数。
  • 其他情况,在每一段分配好的空间前头,同时多分配一个定长的字段,用来记录分配的空间大小。

3.3 版本2.4开始的优化

从版本2.4开始,redis使用了一种新的内存分配机制,被称为“jemalloc”。官方博客有介绍:Everything about Redis 2.4,找到“Jemalloc FTW”部分。我简单翻译如下(难免错漏):

The jemalloc affair is one of our most fortunate use of external code ever. If you used to follow the Redis developments you know I'm not exactly the kind of guy excited to link some big project to Redis without some huge gain. We don't use libevent, our data structures are implemented in small .c files, and so forth.

But an allocator is a serious thing. Since we introduced the specially encoded data types Redis started suffering from fragmentation. We tried different things to fix the problem, but basically the Linux default allocator in glibc sucks really, really hard.

Including jemalloc inside of Redis (no need to have it installed in your computer, just download the Redis tarball as usually and type make) was a huge win. Every single case of fragmentation in real world systems was fixed by this change, and also the amount of memory used dropped a bit.

So now we build on Linux using Jemalloc by default. Thanks Jemalloc! If you are on osx or *BSD you can still force a jemalloc build with make USE_JEMALLOC=yes, but those other systems have a sane libc malloc so usually this is not required. Also a few of those systems use jemalloc-derived libc malloc implementations.

jemalloc是redis项目迄今为止使用得最好的外部代码之一。如果你一直有跟redis开发工作的话,你应该知道我(原作者)一直认为,如果没有非常大的收益的话,最好不要让redis依赖某些外部大项目。我们并没有使用libevent,我们的数据类型是完全使用小型的.c文件进行实现的,并且将来都将如此。

但是内存分配工具是一件非常严肃的事情。由于redis添加了一些非常特殊的数据类型支持,导致redis现在开始受到内存碎片的影响。我们尝试了非常多的手段来解决这个问题,但是由于linux默认的glibc内存分配工具异常的烂,导致这个工作非常难以完成。

将jemalloc引入到redis中是一次巨大的成功(用户没必要在你的系统中手动安装这个软件,只需要下载redis源码进行编译安装就行了)。所有的内存碎片问题通过这次改动得到了修正,并且内存使用量也下降了一点。

所以现在redis在linux平台上默认使用jemalloc作为内建的内存分配工具。非常感谢jemalloc!如果用户使用的是osx或者*BSD操作系统,你可以使用USE_JEMALLOC=yes这个命令进行编译,来强制redis使用jemalloc。当然其他几个平台都有非常健全的libc malloc,所以一般来说无此必要。此外,那些平台中也有一部分使用的就是jemalloc衍生出来的libc malloc实现。

3.4 从源代码数据结构理解内存分配

网上有篇非常给力的技术博文,从redis的内存处处结构角度入手,分析源码给出了内存分配的分析。这里做下转帖:Redis内存存储结构分析

3.5 优化

经过上面的分析,基本明白了redis的内存处理方式,这里说优化,其实也就是从使用的角度上来进行优化。请参看:Redis内存容量的预估和优化

4. redis的持久化策略

4.1 简介

redis是一个支持持久化的内存数据库,也就是说redis需要经常将内存中的数据同步到磁盘来保证持久化。redis支持两种持久化方式,一种是 Snapshotting(快照)也是默认方式,另一种是Append-only file(缩写aof)的方式。

4.2 Snapshotting模式

快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置:

save 900 1  #900秒内如果超过1个key被修改,则发起快照保存
save 300 10 #300秒内容如超过10个key被修改,则发起快照保存
save 60 10000

下面介绍详细的快照保存过程:

  1. redis调用fork,现在有了子进程和父进程。
  2. 父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数 据是fork时刻整个数据库的一个快照。
  3. 当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。

client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。

另外由于快照方式是在一定间隔时间做一次的,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用aof持久化方式。

4.3 Append-only模式

aof 比快照方式有更好的持久化性,是由于在使用aof持久化方式时,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是 appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要 通过fsync函数强制os写入到磁盘的时机。

有三种方式如下(默认是:每秒fsync一次):

appendonly yes              //启用aof持久化方式
# appendfsync always      //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
appendfsync everysec     //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
# appendfsync no    //完全依赖os,性能最好,持久化没保证

aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下:

  1. redis调用fork ,现在有父子两个进程。
  2. 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令。
  3. 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
  4. 当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
  5. 现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

4.4 持久化的官方解析

官方博客上有一篇博文非常详细地对redis的持久化进行了分析:Redis persistence demystified。NoSqlFan上有一篇中文翻译,我这里做了转帖:解密Redis持久化

5. redis的主从策略

redis主从复制配置和使用都非常简单。通过主从复制可以允许多个slave server拥有和master server相同的数据库副本。

下面是关于redis主从复制的一些特点:

  1. master可以有多个slave。
  2. 除了多个slave连到相同的master外,slave也可以连接其他slave形成图状结构。
  3. 主从复制不会阻塞master。也就是说当一个或多个slave与master进行初次同步数据时,master可以继续处理client发来的请求。相反slave在初次同步数据时则会阻塞不能处理client的请求。
  4. 主从复制可以用来提高系统的可伸缩性,我们可以用多个slave 专门用于client的读请求,比如sort操作可以使用slave来处理。也可以用来做简单的数据冗余。
  5. 可以在master禁用数据持久化,只需要注释掉master 配置文件中的所有save配置,然后只在slave上配置数据持久化。

下面介绍下主从复制的过程:

当设置好slave服务器后,slave会建立和master的连接,然后发送sync命令。无论是第一次同步建立的连接还是连接断开后的重新连接,master都会启动一个后台进程,将数据库快照保存到文件中,同时master主进程会开始收集新的写命令并缓存起来。后台进程完成写文件后,master就发送文件给slave,slave将文件保存到磁盘上,然后加载到内存恢复数据库快照到slave上。接着master就会把缓存的命令转发给slave。而且后续master收到的写命令都会通过开始建立的连接发送给slave。从master到slave的同步数据的命令和从 client发送的命令使用相同的协议格式。当master和slave的连接断开时slave可以自动重新建立连接。如果master同时收到多个 slave发来的同步连接命令,只会使用启动一个进程来写数据库镜像,然后发送给所有slave。

配置slave服务器很简单,只需要在配置文件中加入如下配置:

slaveof 192.168.1.1 6379  #指定master的ip和端口

6. redis的集群策略

6.1 简介

redis是没有官方的集群解决方案的。所以当数据量大到单台服务器无法支撑(一般是内存容量不够)的时候,就必须考虑找一种手段来解决集群问题了。官方推荐的手法是在客户端对键值进行hash,然后根据hash的结果将数据分发到不同的redis服务器上进行存储。

这么做有一个非常明显的缺点,就是rehash,当服务器量再次不足,或者冗余的时候,我们很难将数据再次分配。而且这将是致命的,因为redis服务器一般是作为产品数据库进行利用,而不单单只是像memcache那样作为缓存在使用。memcache的时候,当你rehash,也只不过是暂时的增加了数据库的负载,当丢失的数据重新进入memcache之后,缓存又再次工作了,redis的情况则将是一场灾难。所以目前市面上的产品一般都是将redis作为关键数据(关系、结构)的存储,而由其他稳定可靠的关系型数据库(MySQL)进行实际数据的存储。

6.2 第三方集群工具

redis-sharding是一个由perl写的redis的proxy,使用它,你可以将数据分布存储在多个redis实例上,而在操作数据时却像只操作一个实例一样。利用它相当于透明地解决了redis单线程无法有效利用多核心服务器的问题。当然,我们更期待官方的cluster方案。

项目地址:https://github.com/kni/redis-sharding

架构:

                              /- Redis (node 1)
 Client 1 ---                /-- Redis (node 2)
              Redis Sharding --- Redis (node 3)
 Client 2 ---                \-- Redis (node 4)
                              \- Redis (node 5)

启动redis-sharding,分别为使用默认host,port与指定host,port的方式:

 perl redis_sharding.pl                             --nodes=10.1.1.2:6380,10.1.1.3:6380,...
 perl redis_sharding.pl                 --port=6379 --nodes=10.1.1.2:6380,10.1.1.3:6380,...
 perl redis_sharding.pl --host=10.1.1.1 --port=6379 --nodes=10.1.1.2:6380,10.1.1.3:6380,...

redis-sharding还支持重新切分数据,但这需要暂时停掉proxy,下面是将原来的db 9的数据重新sharding到B1-B5五个实例上:

停掉redis-sharding后再执行:

perl resharding.pl --db=9 --from=A1 --nodes=B1,B2,B3,B4,B5
perl resharding.pl --db=9 --from=A2 --nodes=B1,B2,B3,B4,B5

然后再启动新的管理B1-B5的redis-sharding实例即可:

perl redis_sharding.pl --nodes=B1,B2,B3,B4,B5

6.3 官方解答

redis官方也不是完全无视了这个问题。 有一篇官方帖子专门解答了redis工作组的目标:Redis cluster Specification (work in progress)。NoSqlFan也有翻译,这里还是转帖:Redis集群功能说明

7. 参考资料列表