最近重新认识了一下Redis,借着这个机会,也整理一篇算是比较详尽和全面的文章吧。
缓存
缓存就是数据交换的缓冲区(称作Cache)——摘自百度百科。无论是在计算机硬件体系结构还是软件体系结构中,缓存都是提高系统性能的重要手段,应用十分广泛,如:CPU多级缓存、磁盘缓存、操作系统缓存、数据库缓存、浏览器缓存等。互联网的高速发展不断挑战WEB系统的性能极限,随着分布式集群应用的日益广泛,对缓存技术的要求也越来越高,除高性能外,还要满足动态扩展性、高可用性等。
Redis简介
Redis —— REmote DIctionary Server,可以直接理解为远程字典服务,也就是基于Key-Value模式的Memcached+Database Persistence。Redis是一个开源,先进的key-value存储,并用于构建高性能,可扩展的Web应用程序的完美解决方案。
Redis可以支持字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)和 有序集合(sorted sets)等数据类型。 对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。
为了获得优异的性能,Redis采用了内存中(in-memory)数据集(dataset)的方式。根据使用场景的不同,你可以每隔一段时间将数据集转存到磁盘上来持久化数据,或者在日志尾部追加每一条操作命令。
Redis同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步(non-blocking first synchronization)、网络断开自动重连等功能。同时Redis还具有其它一些特性,其中包括简单的check-and-set机制、pub/sub和配置设置等,以便使得Redis能够表现得更像缓存(cache)。
Redis还提供了丰富的客户端,以便支持现阶段流行的大多数编程语言。详细的支持列表可以参看Redis官方文档:http://redis.io/clients。Redis自身使用ANSI C来编写,并且能够在不产生外部依赖(external dependencies)的情况下运行在大多数POSIX系统上,例如:Linux、*BSD、OS X和Solaris等。
Redis的订阅实现了邮件系统,发送者(在Redis的术语中被称为发布者)发送的邮件,而接收器(用户)接收它们。由该消息传送的链路被称为通道。 在Redis客户端可以订阅任何数目的通道。
Redis事务让一组命令在单个步骤执行。事务中有两个属性,说明如下:
- 在一个事务中的所有命令按顺序执行作为单个隔离操作。通过另一个客户端发出的请求在Redis的事务的过程中执行,这是不可能的。
- Redis的事务具有原子性。原子意味着要么所有的命令都执行或都不执行。
Redis的事务由指令多重(MULTI)发起,然后需要传递在事务,而且整个事务是通过执行命令EXEC执行命令列表。
Redis 优势
- 异常快速:Redis的速度非常快,每秒能执行约11万集合,每秒约81000+条记录。
- 支持丰富的数据类型:Redis支持最大多数开发人员已经知道像列表,集合,有序集合,散列数据类型。这使得它非常容易解决各种各样的问题,因为我们知道哪些问题是可以处理通过它的数据类型更好。
- 操作都是原子性:所有Redis操作是原子的,这保证了如果两个客户端同时访问的Redis服务器将获得更新后的值。
- 多功能实用工具:Redis是一个多实用的工具,可以有多种用途,例如缓存,消息,队列使用(Redis原生支持发布/订阅),任何短暂的数据,应用程序,如Web应用程序会话,网页命中计数等。
安装和配置
tar zxvf redis-3.0.0.tar.gz
Redis可以解压至任何目录,一个make安装即可获得执行、配置文件。
安装(这里将redis解压到/opt/目录下):
cd /opt/redis-3.0.0
make之后,我们会得到以下可执行文件:
- redis-server:Redis服务器的daemon启动程序;
- redis-cli:Redis命令行操作工具。或者通过telnet进行纯文本协议操作;
- redis-benchmark:Redis性能测试工具,测试Redis在你的系统及你的配置下的读写性能;
文件位于src目录下。
此外,还会得到一个默认的配置文件——redis.conf。
最好,把它拷贝到固定的目录下,例如:/etc/redis/目录下
mkdir /etc/redis
cp redis.conf /etc/redis
运行:redis-server /etc/redis/redis.conf
通过客户端命令redis-cli访问Redis
redis-cli
redis> set name lmt
OK
redis> get name
"lmt"
关闭:redis-cli shutdown
在Ubuntu上安装Redis的桌面管理器
在Ubuntu上安装Redis的桌面管理器,只需从 http://redisdesktop.com/download 打开下载软件包并安装它。
Redis桌面管理器会给你用户界面来管理Redis的Key和数据。
Redis数据类型
Redis的键值是二进制安全的,这意味着可以用任何二进制序列作为键,从形如”foo”的简单字符串到一个JPEG文件的内容都可以。空字符串也是有效键。
关于键值的几条规则:
- 太长的键值不是个好主意,例如1024字节的键值就不是个好主意,不仅因为消耗内存,而且在数据中查找这类键值的计算成本很高;
- 太短的键值通常也不是好主意,如果你要用”u:1000:pwd”来代替”user:1000:password”,这没有什么问题,但后者更易阅读,并且由此增加的空间消耗相对于key object和value object本身来说很小。当然,没人阻止您一定要用更短的键值节省一丁点儿空间;
- 最好坚持一种模式。例如:”object-type:id:field”就是个不错的注意,像这样”user:1000:password”。我喜欢对多单词的字段名中加上一个点,就像这样:”comment:1234:reply.to”。
- Strings
Strings是Redis中最基本的数据类型,但是别被这个名字迷惑了,因为Redis并不关心值是什么,所以你可以存放字符串、数字、十进制串等任何东西,甚至是对象的序列化。就像之前我们使用set命令把值存入键中(set mykey 1),再通过get命令取出键对应的值一样(get mykey),Redis就是这么简单。当然,与Strings相关的命令还有很多,可以到http://redis.io/查看全部的命令,这里我们就不一一介绍了。
值得强调的是Strings类型还支持位操作,setbit命令可以设置某个key指定位置为1或0,getbit可以查询key指定位置的值(0或1),bitcount命令可以查询某个key一共有多少为1的十进制位。还有一点,虽然Redis对存放的值并不关心,但有些命令却依赖于值的类型。比如incr、decr等命令,当键的值不是数值的时候就会报错。
Redis字符串是二进制安全的,这意味着他们有一个已知的长度没有任何特殊字符终止,所以你可以存储任何东西,512兆为上限。
- Hash
Hash的存在使的Redis变得更加强大,功能上它类似于Java语言中的Map。对于Hash来说,一个值可以有若干个字段(field),或者叫属性,比起Strings类型使用序列化的方式来存储对象来说,使用Hash来存储的话更加合理。比起Strings,使用Hash来存放对象的一个好处是需要更新某个属性时,不用更新整个对象的序列化字串。
127.0.0.1:6379> hset people:id001 id id001
(integer) 1
127.0.0.1:6379> hset people:id001 name ZhangSan
(integer) 1
127.0.0.1:6379> hset people:id001 age 21
(integer) 1
127.0.0.1:6379> hset people:id001 sex male
(integer) 1
127.0.0.1:6379> hget people:id001 name
"ZhangSan"
- List
List相当于编程语言中的数组,但比数组的操作要灵活很多。Redis lists基于Linked Lists实现。List类型在Redis中是有序的,使用lpush和rpush命令可以把元素分别加到数据的左边和右边,lindex命令可以获取指定位置的元素,lrange命令可以获得指定位置区间的多个元素,lpop(rpop)命令删除并返回最左边(最右边)的元素等。通过List,我们可以非常容易的实现各个链表、栈等数据结构,得益于Redis的优点,使用这些实现在性能上都非常的快。
- Sets
Sets是一个集合,比起List,它不允许重复的值,而且也不是有序的,所以不能通过下标索引来获取元素。可以使用sadd命令向集合中加入新的元素,smembers命令返回集合中的所有元素,sismember命令判断一个元素是否属于某个集合(值得一提的是sismember命令的复杂度是O(1),不管集合中有多少个元素,它总是花费固定的时间完成执行)。对于具体超大数据量的系统来说,使用Sets做来唯一性判断未尝不是一个好的方案。
- Sorted Sets
Sorted Sets是一个非常强大的数据结构,它在Sets的基础上,为集合中每个元素绑定了一个数值,这样一来就可以对集合做一些排序相关的操作了。zadd命令向集合中新增一个元素,同时指定该元素对应的数值;zank返回元素在集合中根据数值排序后的下标索引,zrevrank与zank类似,只是排序方式由大到小;zrange与zrevrange命令可以根据下标获取排序后的元素,非常有用的命令。 比如我们使用Sorted Sets来存放一个成绩表,可以非常容易处理诸如:多少人在90分以上,多少人不及格,80分以上的人是哪些,排名第一的是谁,多少分等等查询。
127.0.0.1:6379> zadd math:score 58 person1 63 person2 78 person3 85 person4 90 person5 100 person6
(integer) 6
127.0.0.1:6379> zrevrangebyscore math:score 100 60 //及格的人是哪些
1) "person6"
2) "person5"
3) "person4"
4) "person3"
5) "person2"
127.0.0.1:6379> zrevrange math:score 0 0 withscores //排名第一的是谁,多少分
1) "person6"
2) "100"
Redis支持按键来设置过期时间,过期后值将被删除(在客户端看来是被删除了的)。用TTL命令可以获取某个键值的过期时间(-1表示永不过期)。使用EXISTS命令查看键值是否存在,然后使用EXPIRE设置过期时间段(以秒为单位),或者使用EXPIRET设置过期时间值。
Redis本身支持一些简单的组合型的命令,比如以NX结尾命令都是判断在这个值没有时才进行某个命令。
当然,Redis还支持自定义的命令组合,通过MULTI和EXEC,将几个命令组合起来执行,你还可以用DISCARD命令来中断执行中的命令序列。
Redis持久化
Redis的所有数据都存储在内存中,但是他也提供对这些数据的持久化。
数据快照
数据快照的原理是将整个Redis中存的所有数据遍历一遍存到一个扩展名为rdb的数据文件中。通过SAVE命令可以调用这个过程。
redis 127.0.0.1:6379> SET name "John Doe"
OK
redis 127.0.0.1:6379> SAVE
OK
redis 127.0.0.1:6379> SET name "Sheldon Cooper"
OK
redis 127.0.0.1:6379> BGSAVE
Background saving started
如果你是使用的brew在Mac OSX上安全的Redis,那么rdb文件会存在如下路径
/usr/local/var/db/redis/dump.rdb
Append-Only File(追加式的操作日志记录)
Redis还支持一种追加式的操作日志记录,叫append only file,其日志文件以aof结局,我们一般称为aof文件。要开启aof日志的记录,你需要在配置文件中进行如下设置:
appendonly yes
这时候你所有的操作都会记录在aof日志文件中。
redis 127.0.0.1:6379> GET name
(nil)
redis 127.0.0.1:6379> SET name "Ganesh Gunasegaran"
OK
redis 127.0.0.1:6379> EXIT
→ cat /usr/local/var/db/redis/appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$18
Ganesh Gunasegaran
Redis支持多个DB,默认是16个,你可以设置将数据存在哪一个DB中,不同DB间的数据具有隔离性。也可以在多个DB间移动数据。
Redis主从复制
Redis 支持 master-slave(主从)模式,redis server 可以设置为另一个 redis server 的主机(从机),从机定期从主机拿数据。特殊的,一个从机同样可以设置为一个 redis server 的主机,这样一来 master-slave 的分布看起来就是一个有向无环图 DAG,如此形成 redis server 集群,无论是主机还是从机都是 redis server,都可以提供服务)。
在配置后,主机可负责读写服务,从机只负责读。redis 提高这种配置方式,为的是让其支持数据的弱一致性,即最终一致性。在业务中,选择强一致性还是弱一致性,应该取决于具体的业务需求,像微博,完全可以使用弱一致性模型;像淘宝,可以选用强一致性模型。
积压空间
同样,在主从连接中,也有更新缓存的概念。只是两者的用途不一样,前者被写入本地,后者被写入从机,这里我们把它成为积压空间。
更新缓存存储在 server.repl_backlog,redis 将其作为一个环形空间来处理,这样做节省了空间,避免内存再分配的情况。
积压空间中的数据变更记录是什么时候被写入的?在执行一个 redis 命令的时候,如果存在数据的修改(写),那么就会把变更记录传播。redis 源码中是这么实现的:call()->propagate()->replicationFeedSlaves()。
注释:命令真正执行的地方在 call() 中,call() 如果发现数据被修改(dirty),则传播 propagrate(),replicationFeedSlaves() 将修改记录写入积压空间和所有已连接的从机。
这里可能会有疑问:为什么把数据添加入积压空间,又把数据分发给所有的从机?为什么不仅仅将数据分发给所有从机呢?
因为有一些从机会因特殊情况(???)与主机断开连接,注意从机断开前有暂存主机的状态信息,因此这些断开的从机就没有及时收到更新的数据。redis 为了让断开的从机在下次连接后能够获取更新数据,将更新数据加入了积压空间。从 replicationFeedSlaves() 实现来看,在线的 slave 能马上收到数据更新记录;因某些原因暂时断开连接的 slave,需要从积压空间中找回断开期间的数据更新记录。如果断开的时间足够长,master 会拒绝 slave 的部分同步请求,从而 slave 只能进行全同步。
redis 主从同步有两种方式(或者所两个阶段):全同步和部分同步。
主从刚刚连接的时候,进行全同步;全同步结束后,进行部分同步。当然,如果有需要,slave 在任何时候都可以发起全同步。redis 策略是,无论如何,首先会尝试进行部分同步,如不成功,要求从机进行全同步,并启动 BGSAVE……BGSAVE 结束后,传输 RDB 文件;如果成功,允许从机进行部分同步,并传输积压空间中的数据。
下面这幅图,总结了主从同步的机制:
全同步
接着自动发起 PSYNC 请求 master 进行全同步。无论如何,redis 首先会尝试部分同步,如果失败才尝试全同步。而刚刚建立连接的 master-slave 需要全同步。
从机连接主机后,会主动发起 PSYNC 命令,从机会提供 master_runid 和 offset,主机验证 master_runid 和 offset 是否有效?master_runid 相当于主机身份验证码,用来验证从机上一次连接的主机,offset 是全局积压空间数据的偏移量。
验证未通过则,则进行全同步:主机返回 +FULLRESYNC master_runid offset(从机接收并记录 master_runid 和 offset,并准备接收 RDB 文件)接着启动 BGSAVE 生成 RDB 文件,BGSAVE 结束后,向从机传输,从而完成全同步。
全同步请求的数据是 RDB 数据文件和积压空间中的数据。关于 RDB 数据文件,请参看《深入剖析 redis RDB 持久化策略》。如果没有后台持久化 BGSAVE 进程,那么 BGSVAE 会被触发,否则所有请求全同步的 slave 都会被标记为等待 BGSAVE 结束。BGSAVE 结束后,master 会马上向所有的从机发送 RDB 文件。
部分同步
如上所说,无论如何,redis 首先会尝试部分同步。部分同步即把积压空间缓存的数据,即更新记录发送给从机。
从机连接主机后,会主动发起 PSYNC 命令,从机会提供 master_runid 和 offset,主机验证 master_runid 和 offset 是否有效?
验证通过则,进行部分同步:主机返回 +CONTINUE(从机接收后会注册积压数据接收事件),接着发送积压空间数据。
暂缓主机
从机因为某些原因,譬如网络延迟(PING 超时,ACK 超时等),可能会断开与主机的连接。这时候,从机会尝试保存与主机连接的信息,譬如全局积压空间数据偏移量等,以便下一次的部分同步,并且从机会再一次尝试连接主机。注意一点,如果断开的时间足够长, 部分同步肯定会失败的。
简单来说,主从同步就是 RDB 文件的上传下载;主机有小部分的数据修改,就把修改记录传播给每个从机。
Redis主从切换
Redis 2.8版开始正式提供名为Sentinel的主从切换方案,Sentinel用于管理多个Redis服务器实例,主要负责三个方面的任务:
1. 监控(Monitoring):Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
2. 提醒(Notification):当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
3. 自动故障迁移(Automatic failover):当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。
Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。
启动Sentinel
使用--sentinel参数启动,并必须指定一个对应的配置文件,系统会使用配置文件来保存 Sentinel 的当前状态, 并在 Sentinel 重启时通过载入配置文件来进行状态还原。
redis-server /path/to/sentinel.conf --sentinel
使用TCP端口26379,可以使用redis-cli或其他任何客户端与其通讯。
如果启动 Sentinel 时没有指定相应的配置文件, 或者指定的配置文件不可写(not writable), 那么 Sentinel 会拒绝启动。
配置Sentinel
以下是一段配置文件的示例:
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5
第一行配置指示 Sentinel 去监视一个名为 mymaster 的主服务器, 这个主服务器的 IP 地址为 127.0.0.1 , 端口号为 6379 , 而将这个主服务器判断为失效至少需要 2 个 Sentinel 同意 (只要同意 Sentinel 的数量不达标,自动故障迁移就不会执行)。
不过需要注意的是,无论你设置要多少个 Sentinel 同意才能判断一个服务器失效,一个 Sentinel 都需要获得系统中多数(majority) Sentinel 的支持,才能发起一次自动故障迁移,并预留一个给定的配置纪元 (Configuration Epoch ,一个配置纪元就是一个新主服务器配置的版本号)。也就是说,如果只有少数(minority)Sentinel 进程正常运作的情况下,是不能执行自动故障迁移的。
down-after-milliseconds 选项指定了 Sentinel 认为服务器已经断线所需的毫秒数(判定为主观下线SDOWN)。
parallel-syncs 选项指定了在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步, 这个数字越小, 完成故障转移所需的时间就越长,但越大就意味着越多的从服务器因为复制而不可用。可以通过将这个值设为 1 来保证每次只有一个从服务器处于不能处理命令请求的状态。
主观下线和客观下线
1. 主观下线(Subjectively Down, 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断。
2. 客观下线(Objectively Down, 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。
客观下线条件只适用于主服务器: 对于任何其他类型的 Redis 实例, Sentinel 在将它们判断为下线前不需要进行协商, 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件。
只要一个 Sentinel 发现某个主服务器进入了客观下线状态, 这个 Sentinel 就可能会被其他 Sentinel 推选出, 并对失效的主服务器执行自动故障迁移操作。
每个Sentinel实例都执行的定时任务
1. 每个 Sentinel 以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令。
2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线。 一个有效回复可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。
3. 如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。
4. 如果一个主服务器被标记为主观下线, 并且有足够数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。
5. 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。 当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
6. 当没有足够数量的 Sentinel 同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向 Sentinel 的 PING 命令返回有效回复时, 主服务器的主管下线状态就会被移除。
Sentinel API
有两种方式可以与Sentinel进行通讯:指令、发布与订阅。
Sentinel命令
PING :返回 PONG 。
SENTINEL masters :列出所有被监视的主服务器,以及这些主服务器的当前状态;
SENTINEL slaves <master name> :列出给定主服务器的所有从服务器,以及这些从服务器的当前状态;
SENTINEL get-master-addr-by-name <master name> : 返回给定名字的主服务器的 IP 地址和端口号。 如果这个主服务器正在执行故障转移操作, 或者针对这个主服务器的故障转移操作已经完成, 那么这个命令返回新的主服务器的 IP 地址和端口号;
SENTINEL reset <pattern> : 重置所有名字和给定模式 pattern 相匹配的主服务器。 pattern 参数是一个 Glob 风格的模式。 重置操作清楚主服务器目前的所有状态, 包括正在执行中的故障转移, 并移除目前已经发现和关联的, 主服务器的所有从服务器和 Sentinel ;
SENTINEL failover <master name> : 当主服务器失效时, 在不询问其他 Sentinel 意见的情况下, 强制开始一次自动故障迁移。
客户端可以通过SENTINEL get-master-addr-by-name <master name>获取当前的主服务器IP地址和端口号,以及SENTINEL slaves <master name>获取所有的Slaves信息
发布与订阅信息
客户端可以将 Sentinel 看作是一个只提供了订阅功能的 Redis 服务器: 你不可以使用 PUBLISH 命令向这个服务器发送信息, 但你可以用 SUBSCRIBE 命令或者 PSUBSCRIBE 命令, 通过订阅给定的频道来获取相应的事件提醒。
一个频道能够接收和这个频道的名字相同的事件。 比如说, 名为 +sdown 的频道就可以接收所有实例进入主观下线(SDOWN)状态的事件。
通过执行 PSUBSCRIBE * 命令可以接收所有事件信息。
+switch-master <master name> <oldip> <oldport> <newip> <newport> :配置变更,主服务器的 IP 和地址已经改变。 这是绝大多数外部用户都关心的信息。
可以看出,我们使用Sentinel命令和发布订阅两种机制就能很好的实现和客户端的集成整合:
使用get-master-addr-by-name和slaves指令可以获取当前的Master和Slaves的地址和信息;而当发生故障转移时,即Master发生切换,可以通过订阅的+switch-master事件获得最新的Master信息。
*PS:更多Sentinel的可订阅事件参见官方文档。
sentinel.conf中的notification-script
在sentinel.conf中可以配置多个sentinel notification-script <master name> <shell script-path>, 如sentinel notification-script mymaster ./check.sh
这个是在群集failover时会触发执行指定的脚本。脚本的执行结果若为1,即稍后重试(最大重试次数为10);若为2,则执行结束。并且脚本最大执行时间为60秒,超时会被终止执行。
PS:目前会存在该脚本被执行多次的问题,查找资料有人解释是:
脚本分为两个级别, SENTINEL_LEADER 和 SENTINEL_OBSERVER ,前者仅由领头 Sentinel 执行(一个 Sentinel),而后者由监视同一个 master 的所有 Sentinel 执行(多个 Sentinel)。
Redis集群、一致性Hash
Keepalived
参考官方文档:http://keepalived.org/pdf/sery-lvs-cluster.pdf
Keepalived 是一个用c写的路由选择软件,配合IPVS 负载均衡实用,通过VRRP 协议提供高可用。目前最新版本1.2.7.Keepalived 机器之间实用VRRP路由协议切换VIP,切换速度秒级,且不存在脑裂问题。可以实现
可以实现一主多备,主挂后备自动选举,漂移VIP,切换速度秒级;切换时可通过运行指定脚本更改业务服务状态。
如两台主机A、B,可以实现如下切换:
1.A 、B 依次启动,A作为主、B为从
2 .主A 挂掉,B接管业务,作为主
3.A 起来,作为从SLAVEOF B
4.B 挂掉,A 切回主
将一台全部作为主,即可实现主从,可做读写分离;也可以通过多个VIP,在一台机器上多个实例中一半主、一半从,实现互备份,两机同时负责部分业务,一台宕机后业务都集中在一台上
安装配置都比较简单:
需要依赖包:openssl-devel(ubuntu 中为 libssl-dev),popt-devel (ubuntu中为libpopt-dev)。
配置文件默认路径:/etc/keepalived/keepalived.conf 也可以手动指定路径,不过要注意的是手动指定需要使用绝对路径。主要要确保配置文件的正确性,keepalived 不会检查配置是否符合规则。
使用keepalived -D 运行,即可启动3个守护进程:一个父进程,一个check健康检查,一个Vrrp,-D将日志写入/var/log/message,可以通过日志查看切换状况。
注意问题:
1. VRRP 协议是组播协议,需要保证主、备、VIP 都在同一个VLAN下
2. 不同的VIP 需要与不同的VRID 对应,一个VLAN 中VRID 不能和其他组冲突
Keepalived 是一个用c写的路由选择软件,配合IPVS 负载均衡实用,通过VRRP 协议提供高可用。目前最新版本1.2.7.Keepalived 机器之间实用VRRP路由协议切换VIP,切换速度秒级,且不存在脑裂问题。可以实现
可以实现一主多备,主挂后备自动选举,漂移VIP,切换速度秒级;切换时可通过运行指定脚本更改业务服务状态。
如两台主机A、B,可以实现如下切换:
1.A 、B 依次启动,A作为主、B为从
2 .主A 挂掉,B接管业务,作为主
3.A 起来,作为从SLAVEOF B
4.B 挂掉,A 切回主
将一台全部作为主,即可实现主从,可做读写分离;也可以通过多个VIP,在一台机器上多个实例中一半主、一半从,实现互备份,两机同时负责部分业务,一台宕机后业务都集中在一台上
安装配置都比较简单:
需要依赖包:openssl-devel(ubuntu 中为 libssl-dev),popt-devel (ubuntu中为libpopt-dev)。
配置文件默认路径:/etc/keepalived/keepalived.conf 也可以手动指定路径,不过要注意的是手动指定需要使用绝对路径。主要要确保配置文件的正确性,keepalived 不会检查配置是否符合规则。
使用keepalived -D 运行,即可启动3个守护进程:一个父进程,一个check健康检查,一个Vrrp,-D将日志写入/var/log/message,可以通过日志查看切换状况。
注意问题:
1. VRRP 协议是组播协议,需要保证主、备、VIP 都在同一个VLAN下
2. 不同的VIP 需要与不同的VRID 对应,一个VLAN 中VRID 不能和其他组冲突
3. 在keepalived 有两个角色:Master(一个)、Backup(多个),如果设置一个为Master,但Master挂了后再起来,必然再次业务又一次切换,这对于有状态服务是不可接受的。解决方案就是两台机器都设置为Backup,而且优先级高的Backup设置为nopreemt 不抢占。
通过keepalived实现的高可用方案
切换流程:
1. 当Master挂了后,VIP漂移到Slave;Slave 上keepalived 通知redis 执行:slaveof no one ,开始提供业务
2. 当Master起来后,VIP 地址不变,Master的keepalived 通知redis 执行slaveof slave IP host ,开始作为从同步数据
3. 依次类推
主从同时Down机情况:
1. 非计划性,不做考虑,一般也不会存在这种问题
2. 、计划性重启,重启之前通过运维手段SAVE DUMP 主库数据;需要注意顺序:
1. 关闭其中一台机器上所有redis,是得master全部切到另外一台机器(多实例部署,单机上既有主又有从的情况);并关闭机器
2. 依次dump主上redis服务
3. 关闭主
4. 启动主,并等待数据load完毕
5. 启动从
删除DUMP 文件(避免重启加载慢)
使用Twemproxy 实现集群方案
一个由twitter开源的c版本proxy,同时支持memcached和redis,目前最新版本为:0.2.4,持续开发中;https://github.com/twitter/twemproxy .twitter用它主要减少前端与缓存服务间网络连接数。
特点:快、轻量级、减少后端Cache Server连接数、易配置、支持ketama、modula、random、常用hash 分片算法。
这里使用keepalived实现高可用主备方案,解决proxy单点问题;
优点:
1. 对于客户端而言,redis集群是透明的,客户端简单,遍于动态扩容
2. Proxy为单点、处理一致性hash时,集群节点可用性检测不存在脑裂问题
3. 高性能,CPU密集型,而redis节点集群多CPU资源冗余,可部署在redis节点集群上,不需要额外设备
Redis高并发、锁
Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists。这系列的命令非常有用,这里讲使用SETNX来实现分布式锁。
用SETNX实现分布式锁
利用SETNX非常简单地实现分布式锁。例如:某客户端要获得一个名字foo的锁,客户端使用下面的命令进行获取:
SETNX lock.foo <current Unix time + lock timeout + 1>
如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁;如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
解决死锁
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。
发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:
C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
C1 发送DEL lock.foo
C1 发送SETNX lock.foo 并且成功了。
C2 发送DEL lock.foo
C2 发送SETNX lock.foo 并且成功了。
这样一来,C1,C2都拿到了锁!问题大了!
幸好这种问题是可以避免D,让我们来看看C3这个客户端是怎样做的:
C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
反之,如果已超时,C3通过下面的操作来尝试获得锁:
GETSET lock.foo <current Unix time + lock timeout + 1>
通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。
Redis缓存策略
Redis 提供 6种数据淘汰策略:
(1)volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
(2)volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
(3)volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
(4)allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
(5)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
(6)no-enviction(驱逐):禁止驱逐数据
Redis性能
Redis-benchmark为Redis性能测试工具。
指令说明:
- Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]
- -h <hostname> Server hostname (default 127.0.0.1)
- -p <port> Server port (default 6379)
- -s <socket> Server socket (overrides host and port)
- -c <clients> Number of parallel connections (default 50)
- -n <requests> Total number of requests (default 10000)
- -d <size> Data size of SET/GET value in bytes (default 2)
- -k <boolean> 1=keep alive 0=reconnect (default 1)
- -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD
- Using this option the benchmark will get/set keys
- in the form mykey_rand:000000012456 instead of constant
- keys, the <keyspacelen> argument determines the max
- number of values for the random number. For instance
- if set to 10 only rand:000000000000 - rand:000000000009
- range will be allowed.
- -P <numreq> Pipeline <numreq> requests. Default 1 (no pipeline).
- -q Quiet. Just show query/sec values 只显示每秒钟能处理多少请求数结果
- --csv Output in CSV format
- -l Loop. Run the tests forever 永久测试
- -t <tests> Only run the comma separated list of tests. The test
- names are the same as the ones produced as output.
- -I Idle mode. Just open N idle connections and wait.
实例:
redis-benchmark -h 127.0.0.1 -p 6379 -q -d 100
SET/GET 100 bytes 检测host为127.0.0.1 端口为6379的redis服务器性能
redis-benchmark -n 100000 -c 60
向redis服务器发送100000个请求,每个请求附带60个并发客户端
结果(部分):
====== SET ======
99.98% <= 46 milliseconds
99.99% <= 62 milliseconds
100.00% <= 62 milliseconds
所有请求在62毫秒内完成
42105.26 requests per second
每秒处理42105.26次请求
redis-benchmark -h 127.0.0.1 -p 6379 -c 5000 -n 100000
5000个并发连接,100000个请求,检测host为127.0.0.1 端口为6379的redis服务器性能 benchmark工具测试信息:
测试命令:redis-benchmark -n 100000 -c 60
向redis服务器发送100000个请求,每个请求附带60个并发客户端
结果(部分):
====== SET ======
对集合写入测试
100000 requests completed in 2.38 seconds
100000个请求在2.38秒内完成
60 parallel clients
每次请求有60个并发客户端
3 bytes payload
每次写入3个字节的数据
keep alive: 1
保持一个连接,一台服务器来处理这些请求93.06% <= 15 milliseconds
99.96% <= 31 milliseconds99.98% <= 46 milliseconds
99.99% <= 62 milliseconds
100.00% <= 62 milliseconds
所有请求在62毫秒内完成
42105.26 requests per second
每秒处理42105.26次请求
- [root@localhost ~]# redis-benchmark -h 127.0.0.1 -p 6379 -c 5000 -n 100000 -d 100 -q
- PING_INLINE: 34506.55 requests per second
- PING_BULK: 34059.95 requests per second
- SET: 31959.09 requests per second
- GET: 31466.33 requests per second
- INCR: 33311.12 requests per second
- LPUSH: 29265.44 requests per second
- LPOP: 36968.58 requests per second
- SADD: 32030.75 requests per second
- SPOP: 33344.45 requests per second
- LPUSH (needed to benchmark LRANGE): 29735.36 requests per second
- LRANGE_100 (first 100 elements): 16116.04 requests per second
- LRANGE_300 (first 300 elements): 6659.56 requests per second
- LRANGE_500 (first 450 elements): 4108.29 requests per second
Redis的Java客户端
在实际的项目开发中,各种语言是使用Redis的客户端库来与Redis交互。针对Java语言,Redis官方推荐Jedis。
Jedis提供了多种操作方式:单机单连接方式、单机连接池方式、多机分布式+连接池方式。
使用jedis-2.5.2和commons-pool2-2.2.jar。
使用单连接
此方式仅建议用于开发环境做调试用。
// 创建连接
String host = "192.168.56.102";
int port = 6379;
Jedis client = new Jedis(host, port);
// 执行set指令
String result = client.set("key-string", "Hello, Redis!");
System.out.println( String.format("set指令执行结果:%s", result) );
// 执行get指令
String value = client.get("key-string");
System.out.println( String.format("get指令执行结果:%s", value) );
运行上述代码,控制台输出:
set指令执行结果:OK
get指令执行结果:Hello, Redis!
使用连接池
此方式适用于仅使用单个Redis实例的场景。
// 生成连接池配置信息
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(10);
config.setMaxTotal(30);
config.setMaxWaitMillis(3*1000);
// 在应用初始化的时候生成连接池
JedisPool pool = new JedisPool(config, "192.168.56.102", 6379);
// 在业务操作时,从连接池获取连接
Jedis client = pool.getResource();
try {
// 执行指令
String result = client.set("key-string", "Hello, Redis!");
System.out.println( String.format("set指令执行结果:%s", result) );
String value = client.get("key-string");
System.out.println( String.format("get指令执行结果:%s", value) );
} catch (Exception e) {
// TODO: handle exception
} finally {
// 业务操作完成,将连接返回给连接池
if (null != client) {
pool.returnResource(client);
}
} // end of try block
// 应用关闭时,释放连接池资源
pool.destroy();
运行上述代码,控制台输出:
set指令执行结果:OK
get指令执行结果:Hello, Redis!
使用连接池+分布式
在规模较大的系统中,往往会有多个Redis实例做负载均衡。并且还实现主从备份,当主实例发生故障时,切换至从实例提供服务。
类似于Memcached的客户端,Jedis也提供了客户端分布式操作的方式,采用一致性哈希算法。
// 生成多机连接信息列表
List<JedisShardInfo> shards = new ArrayList<JedisShardInfo>();
shards.add( new JedisShardInfo("127.0.0.1", 6379) );
shards.add( new JedisShardInfo("192.168.56.102", 6379) );
// 生成连接池配置信息
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(10);
config.setMaxTotal(30);
config.setMaxWaitMillis(3*1000);
// 在应用初始化的时候生成连接池
ShardedJedisPool pool = new ShardedJedisPool(config, shards);
// 在业务操作时,从连接池获取连接
ShardedJedis client = pool.getResource();
try {
// 执行指令
String result = client.set("key-string", "Hello, Redis!");
System.out.println( String.format("set指令执行结果:%s", result) );
String value = client.get("key-string");
System.out.println( String.format("get指令执行结果:%s", value) );
} catch (Exception e) {
// TODO: handle exception
} finally {
// 业务操作完成,将连接返回给连接池
if (null != client) {
pool.returnResource(client);
}
} // end of try block
// 应用关闭时,释放连接池资源
pool.destroy();
运行上述代码,控制台输出:
set指令执行结果:OK
get指令执行结果:Hello, Redis!
Redis实现
redis 主从复制的实现主要在 replication.c 中。
参考文献
Redis两种持久化方式及原理-http://www.m690.com/archives/371
深入剖析Redis主从复制-http://www.cnblogs.com/daoluanxiaozi/p/3724299.html
深入剖析Redis数据淘汰策略-http://www.aikaiyuan.com/7089.html
使用Redis的Java客户端Jedis-http://aofengblog.blog.163.com/blog/static/631702120147298317919/
Redis-benchmark使用总结-http://blog.csdn.net/jiangguilong2000/article/details/24143721