Redis学习笔记
Redis(REmote DIctionary Server)是一个开源的、基于内存的数据结构存储。可以作为数据库、缓存、消息代理使用。它支持字符串、散列、列表、集合、有序集合等多种数据结构,并提供范围查询。它支持位图、hyperloglogs(一种用于解决count-distinct问题的算法,可以估算Bag中的distinct元素的近似个数)、地理空间索引,并支持径向查询。Redis内置了复制(replication)机制,支持Lua脚本、LRU清除算法。Redis支持事务,以及不同级别的磁盘持久化。基于Redis Sentinel和Redis Cluster的自动分区(automatic partitioning)机制,它能够提供HA保障。
Redis可以认为是NoSQL数据库的一种,它是目前最流行的键值对存储引擎。这类存储允许你基于键来保存数据,在之后,你必须知道键才能取回数据。
几乎所有的主流语言都有Redis的客户端。
由于Redis更加新,特性更加丰富,相比Memcached它通常总是正确的选择。
Redis的最根本优势在于数据结构的支持。它支持长达250MB的键、值大小,支持字符串、哈希、列表、有序集合、集合等数据类型;Memcached仅仅支持250字节的键,值仅仅支持字符串。
Redis作为缓存使用时,数据清除算法更加丰富,相比之下Memcached仅支持LRU、随机清除。Redis提供主动清除(生存期)、被动(延迟)清除,Memcached仅支持被动清除。
Memcached更加适合缓存相对小的、静态数据,例如HTML代码片段。这是因为Memcached的内部内存管理不像Redis那样精巧,在元数据方面消耗较少的内存。但是,如果数据尺寸是动态的,Memcached的上述优势很快消失,因为它存在内存碎片问题。
Scaling是选择Memcached的另外一个场景,因为它是多线程的。你可以很容易的Scaling up来使用更多的计算资源。Redis则基本是单线程的,你需要通过集群来水平Scale,比Memcached复杂。
参考以下步骤进行安装:
1 2 3 4 5 6 |
# 安装Redis服务器 sudo apt-get install redis-server # 查看版本 redis-server --version # 禁用服务自动启动 sudo update-rc.d redis-server disable |
使用下面的命令可以运行基于Docker的Redis:
1 2 3 4 5 6 7 8 9 10 |
# 拉取镜像 docker pull redis:3 # 在后台运行Redis容器 docker run --name redis -d redis # 启用持久化存储,存储目录默认/data docker run -v ~/Docker/volumes/redis/data:/data -v ~/Docker/volumes/redis/conf:/conf -p 6379:6379 --name redis -d redis redis-server /conf/redis.conf |
要定制Redis配置文件,可以扩展镜像:
1 2 3 4 |
FROM redis:3 COPY redis.conf /usr/local/etc/redis/redis.conf CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ] |
Redis是一个TCP服务器,使用请求/应答通信模型。这意味着一个请求通常按如下步骤完成:
- 客户端发送一个请求给服务器,通常以阻塞的方式等待读套接字,以获取服务器响应
- 服务器处理命令,并把结果发送给客户端
客户端/服务器基于网络连接,这个网络可能很快——例如环回网卡,也可能很慢。从客户端发送请求,到接收到服务器响应的这段时间,称为RTT(Round Trip Time)。
Redis支持在客户端尚未读取旧的响应之前就处理新的请求,客户端可以连续发送多个命令给服务器,而在最后一起处理所有应答。Redis API提供了管道相关的接口
Redis支持这种交互模型,并提供了相关的命令。使用这种模型,消息发送者不需要显式的发消息发送给特定的接受者,而仅仅需要把消息发布到频道(Channel)上,从而实现解耦。
订阅了频道的客户端,不应该发送不相关的命令,仅可以发送:SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING, QUIT这几个命令。
每个推送给订阅客户端的消息,是三元素的数组。第一个元素是消息的类型,值可以是:
- subscribe,表示成功订阅了通道。目标通道名称作为第二个元素,第三个元素是当前订阅的通道数
- unsubscribe,表示成功取消了订阅。目标通道名称作为第二个元素,第三个元素是当前订阅的通道数
- message,表示接收到其它客户端PUBLISH的消息。第二个元素是消息来自的通道,第三个元素是消息载荷
发布/订阅与Redis数据库、键空间没有任何关系,你在数据库0上发布,客户端可以在数据库10上订阅。
Redis支持基于通配符的订阅,示例:
1 2 |
PSUBSCRIBE news.* PSUBSCRIBE f* |
从2.2开始,Redis优化了很多数据类型,以占用更少的内存空间。仅仅由整数构成的哈希、集合、列表,以及有序集合,在编码后占用内存大小可能小十倍。从用户和API的角度来说,这一编码是完全透明的。
特殊编码是一种CPU消耗 - 内存占用的权衡。Redis提供一些参数,来调整编码行为:
1 2 3 4 5 6 7 8 9 |
# 进行压缩的容器,其元素个数的限制 hash-max-zipmap-entries 512 # 进行压缩的集合,其元素长度的限制 hash-max-zipmap-value 64 list-max-ziplist-entries 512 list-max-ziplist-value 64 zset-max-ziplist-entries 128 zset-max-ziplist-value 64 set-max-intset-entries 512 |
当目标容器元素个数或者元素长度超过限制,则自动使用正常编码。
由于指针长度短,32位的Redis使用更少的内存,但是缺点是,它最多使用4GB的内存。对于32/64位Redis,RDB、AOF文件的格式是兼容的。
从Redis 2.2引入的GETRANGE, SETRANGE, GETBIT,SETBIT命令,允许你将字符串作为随机访问数组看待。前面两个命令可以按字节操作,后面两个命令更是按位操作。合理使用这些命令可以减少内存消耗。
小的哈希被Redis很好的编码,很节约空间,因此你可以尽可能考虑以哈希的方式存储数据,而不是单独存储每个字段。
为了存储用户的键值,Redis可以分配不超过maxmemory设置的内存。
关于Redis管理内存的方式,你需要知道:
- 当键被移除后,Redis不总会把内存归还给OS。你需要根据内存需要量的峰值来规划配置
- 尽管Redis可能不归还内存,但是新插入的键会智能的使用这些未归还、但是已经空闲的内存
- 碎片率( fragmentation ratio)在当前内存用量远小于峰值用量时,可能不准确。碎片率 = 当前内存用量 / RSS。RSS即驻留工作集尺寸——占据物理内存的尺寸
如果你不设置maxmemory参数,Redis会尽可能多的占用内存,从而影响OS性能。
当把Redis作为缓存使用时,让其自动的清除陈旧数据,通常符合缓存需求。
LRU是最常见的一种数据清除算法,Redis也支持其它算法。
使用配置指令可以限制Redis使用内存的峰值:
1 |
maxmemory 100mb |
如果设置为0,表示无限制。这是64位系统的默认值,32位系统隐含限制3GB。
当Redis到达最大内存限制后,可以依据设置的策略(policies)来决定如何处理 —— 例如返回错误,或者清除旧数据。支持的策略如下表:
策略 | 说明 |
noeviction | 在内存不足的时候,返回错误 |
allkeys-lru | 针对所有键执行LRU算法,最久未被使用的键被移除 |
volatile-lru | 针对设置了超时的键执行LRU算法 |
allkeys-random | 针对所有键进行随机清除 |
volatile-random | 针对设置了超时的键进行随机清除 |
volatile-ttl | 尝试移除TTL最短(过期时间最近)的键 |
从4.0版本开始,Redis支持新的LFU( Least Frequently Used)算法,在某些情况下该算法工作的更好。该算法会尝试跟踪每个键被使用的频率,使用频率最少的键会被清除。LRU算法可能会保留最近访问了一次,但是实际上基本不会被访问的键,LFU不存在此问题。
Redis支持把多个命令分为一组,然后作为单个事务来执行。
MULTI, EXEC, DISCARD,WATCH这几个命令是Redis事务控制的基础。通过组合使用这些命令,你可以单步执行多个命令,并确保:
- 事务中所有命令被顺序的执行。从效果上说,不会发生其他客户端发起的命令,插入上述事务中间执行的情况。所有命令就像是单个操作似的
- 要么所有命令执行,要么都不执行。EXEC命令触发事务中所有命令的执行。当使用AOF文件时,Redis确保使用单个write(2)系统调用把事务操作写入磁盘,除非Redis崩溃或者被强制杀死 —— 可能仅有部分命令的结果被写入磁盘。如果发生这种意外,在Redis下一次启动时,它会提示错误并退出。你需要使用 redis-check-aof来修复AOF文件,把不完整执行的事务移除,然后再启动Redis
从2.2开始,Redis为事务提供额外保证 —— 使用乐观锁。
Redis事务从MULTI命令开始,该命令的应答总是OK。发起该命令后,用户可以继续发起多个命令。后续的命令不会立即执行,而是排队,直到你调用EXEC命令。
如果调用DISCARD而不是EXEC,会清除命令队列并且关闭事务。
在事务中,你可能遇到两类命令错误:
- 命令可能无法进入队列,原因例如命令格式错误,或者出现极端情况,例如内存不够
- 命令可以在EXEC调用之后失败
对于第一类错误,用户可以检查入队命令的返回值,如果显示QUEUED意味着入队成功。否则意味着入队失败,这种情况下通常需要DISCARD事务。从2.6.5开始,只要存在入队失败,EXEC一定会返回一个错误并自动DISCARD事务。
对于第二类错误,没有特殊的处理。如果某个命令执行失败,其它任务仍然会继续执行。
前面我们提到过,事务过程中某条命令执行失败,并不会中断后续命令的执行,或者回滚之前已经执行的命令,这和关系型数据库很不一样。Redis的这种行为的原因是:
- Redis命令仅仅会在语法错误、键持有不匹配的数据类型的情况下,才会失败。这意味着失败通常由于编程错误,应该在开发阶段就发现
- 由于不去支持回滚,这让Redis更简单、更快
WATCH命令提供了一种检查并设置(CAS,check-and-set),为Redis事务提供乐观锁。
被监控(WATCHed)的键的修改会被发现,如果存在一个或者更多的键,在EXEC之前修改了,则整个事务被中止,EXEC命令返回一个Null Reply。
WATCH命令可以调用多次,其监控的时间范围是,从调用的那一刻起,到EXEC被调用时为止。
调用UNWATCH,可以撤销之前所有WATCH命令。
Redis提供了两种持久化模式:RDB、AOF。这两种模式可以同时启用,你也可以完全禁用持久化。当同时启用时,在Redis启动时会读取AOF文件,因为它更能保证数据的完整性。
Redis官方建议同时使用两种持久化模式,并且在远期未来将其合并为一种。
RDB相关的配置项:save;AOF相关的配置项:appendonly。
该模式在磁盘上保存某个瞬间的数据集快照,可以定期的保存。Redis异步的把数据写到磁盘上,形成.rdb文件。该模式对于很多应用程序足够好用。
RDB的优势:
- 作为Redis的瞬时快照,RDB文件格式紧凑,很适合作备份用途,例如远程灾备
- RDB性能较好,因为Redis主线程仅仅需要Fork出子线程负责磁盘I/O,主线程基本不需要执行磁盘I/O
- 如果数据集很大,RDB的启动速度高于AOF
RDB的劣势:
- 如果Redis进程崩溃或者电力中断,你可能丢失数分钟的数据,具体时间范围取决于你如何配置保存点(Save point)
另一种持久化模式是仅附加文件(Append Only File),它实际上是服务器接收到的每个写命令的日志,且命令日志的格式与Redis协议相同。当服务器重新启动时,该日志可以被回放,以还原先前的数据集。
该模式提供了好得多的durability:
- 使用默认的数据fsync策略你最多在断电这种极端情况下丢失1秒的数据
- Redis进程本身出现问题(但OS正常)时,最多丢失最后一次写操作
AOF的额外优势:
- AOF日志仅仅需要进行追加操作,因而不需要Seek。AOF不存在文件损坏的问题,即使日志结尾是写入一半的命令,也可以用 redis-check-aof进行修复
- AOF文件变大后,Redis可以在后台进行重写。这个重写操作是完全安全的,因为重写时Redis还会在旧文件上追加,如果重写失败,旧文件仍然可用
- AOF日志格式容易解析
AOF的劣势:
- 相同数据集下AOF比RDB大
- 基于使用的fsync策略,AOF可能比RDB慢。禁止fsync在高并发情况下,性能与RDB一样快,如果每秒fsync一次,也还算很快
Redis中进行主从复制的配置很简单,关于主从复制,你需要知道:
- Redis使用异步的主从复制,从2.8开始,Slave定期的确认(acknowledge)从复制流中处理的数据量
- 一个Master可以拥有多个Slave
- Slave允许来自其它Slave的连接。并形成Master - Slave - Slave的树形结构
- 在Master节点,复制是非阻塞的。当一个或者多个Slave执行初始同步时,Master仍然接受查询请求
- 在Slave节点,复制也是非阻塞的。当Slave执行初始同步时,它仍然能够(如果你进行适当的配置)使用原来的数据集对外提供查询服务
- 复制可以提供扩容性,可以把只读的缓慢查询分配给Slave执行
- 可以完全避免Master的磁盘I/O开销,你可以配置一个Slave,并启用AOF。这种技巧需要注意处理Master宕机,因为重启后它的数据集为空,不应该作为Master
上面我们提到过,可以处于性能的考虑禁用Master的持久化。Redis不建议这样,如果禁用Master的持久化,一定要同时禁止Redis开机启动。
当你配置好一个Slave后,在连接建立后它发送一个PSYNC命令。如果这次连接属于“重新连接”并且Master的backlog足够大,则Master把增量数据集发送给Slave。否则,触发一次完全同步(full resynchronization )。
完全同步触发后,Master在后台启动一个保存线程,产生RDB文件。与此同时,Master对新的写命令进行缓存。RDB文件准备好之后,发送给Slave加载到内存,然后写命令缓存也被发送给Slave进行回放。
当与Master的连接断掉后,Slave会自动进行重连。
如果多个Slave需要同步,Master只会产生一个RDB保存进程。
某些场景下你需要在短时间内完成大量数据的插入,例如添加百万个新的Redis键。本节内容介绍如何尽快的完成数据的插入。
使用普通的Redis客户端执行海量数据插入通常不是好主意,因为:
- 最简单的方式:一个一个的发送命令,大量时间浪费在了RRT上
- 使用管道(Pipelining)可以缓解上一条,但是,它限制了在最后一起处理响应。在大量数据插入的场景下,最好能一边插入新条目,一边处理旧条目的响应
仅仅少量的客户端支持非阻塞I/O,而且,并非所有客户度能够高效的解析响应以最大化吞吐量。因此,在Redis中完成海量数据的最好方式是,生成包含原始数据、Redis协议的文本文件,然后通过Redis客户端的Pipe模式发送给服务器处理。
所谓分区(Partitioning)是指把你的数据分散到多个Redis实例的处理过程,每个实例仅持有键空间的子集。分区的意义在于:
- 通过利用多台计算机,突破单机内存限制,支持更大的数据集
- 把计算能力Scale到多台计算机的多个CPU;把网络带宽Scale到多台计算机的多个网络接口
最简单的是范围分区(range partitioning),它要求键的格式为 key-name:id。它还需要一张表来记录id范围和实例的映射关系。在实际中很少使用这种方式
另外一种是哈希分区(hash partitioning),它通过计算键的散列值来决定其应当由哪个实例持有,对键格式没有要求,也不需要额外的表。
分区的职责可以划分给软件栈中的不同组件:
- 客户端分区:客户端直接决定该把键发送给哪个节点处理。很多Redis客户端实现了此功能
- 代理辅助分区:客户端把请求发送给一个理解Redis协议的代理,此代理负责转发请求给适当的实例。Redis实现了这种方式,Memcached的Twemproxy类似
- 查询路由:客户端随机的把请求发送给一个实例。由该实例将其转发给正确的实例。Redis集群利用客户端的辅助,实现了混合形式的查询路由 —— 请求不在节点间转发,而是客户端被重定向到正确的节点,并由客户端直接发送请求给正确的节点
Redis分区的某些特性做的不是很好:
- 牵涉到多个键的操作常常不被支持。例如,你不能(直接)对两个分布在不同实例上的两个集合进行交叉操作
- 牵涉到多个键的事务无法支持
- 分区的粒度是键,无法对一个键下的巨大数据集进行分区
- 使用分区时,数据的处理更加复杂。例如需要处理多个RDB/AOF文件。在备份时,你需要从多个机器上收集持久化文件
- 增减容量可能比较复杂。Redis集群支持几乎透明的在运行时添加/移除节点,实现数据的Rebalance。其它分区实现,例如客户端分区、代理辅助分区则不支持这种Rebalance,需要使用Pre-sharding技术
尽管从概念上说,Redis分区用在数据存储还是缓存场景下没有什么区别。但是,用作数据存储时,有一个重要的限制——一个键必须总是映射到相同的Redis实例。
一致性哈希(Consistent hashing)实现通常能够在某个键的最优节点不可用时,自动切换到其它节点。类似的,当添加新节点后,一部分新的键可以自动分配到新节点上。
当作为缓存使用时,Redis可以基于一致性哈希很容易的Scale up/down。
当用作数据存储时,需要基于固定的键-节点映射表,节点的数量必须是固定的,不能改变 —— 否则必须在增减节点时实现Rebalance,当前只有Redis Cluster支持这种Rebalance。
前面我们提到过,作为数据存储的分区,要添加/删除节点并不容易。但是,随着时间的推移,数据存储肯定是要不断变化的,今天需要10个节点就满足需要,明天可能需要增加到15个。
要解决这个问题,第一个方案是,从开始就准备足够多(32或者64个满足绝大部分场景)的实例。可以这样做的原因是Redis实例本身占用很少的资源,它仅仅需要1MB的内存。计算在数据很少的情况下,你也可以在单台机器上使用这种分区方式。当数据增多,单台机器计算资源不够时,可以增加一台服务器,并把一半的实例迁移到新的机器上。利用Redis的复制机制(Replication),可以在免宕机/最小化宕机时间的前提下,实现这种迁移:
- 在新服务器上启动新实例
- 把这些实例作为需要迁移的旧实例的Slave
- 停止客户端
- 更新迁移实例的IP地址配置
- 在新服务器上,对实例发送 SLAVEOF NO ONE命令
- 基于新的配置启动客户端
- 关闭旧服务器上不再需要的那些实例
从2015年4月开始,Redis Cluster已经可以胜任生产环境了,Redis Cluster的实现方式类似于查询路由、客户端分区的混合。
Redis Cluster是最优的分区方案,因为它可以自动分区并提供高可用性。一旦基于你熟悉编程语言的Redis Cluster客户端可用,Redis集群将作为分区实现的标准。
当多个进程需要互斥的操作同一共享资源时,分布式锁是一种有用的原语。
很多第三方库实现了可以配合Redis的分布式锁管理器(Distributed Lock Manager,DLM),它们的实现方式各有不同。Redis推荐基于红锁(Redlock)算法的实现,主流语言有实现。
Redlock提供以下最小化保证:
- 安全性保证:互斥性,在任意时刻,仅一个客户端能够持有锁
- 活动性A:不会发生死锁,请求者最终一定会获得锁,甚至是在持有锁的客户端崩溃、分区发生的情况下
- 活动性B:容错,只要大部分Redis节点可用,客户端总是能获得、释放锁
这是2.8.0开始引入的功能,它允许客户端订阅特定的频道,并在影响了Redis数据集的事件发生时,获得通知。这些事件例如:
- 针对某个特定键的命令被执行
- 针对所有键的LPUSH命令被执行
- 数据库0中的任意键过期
事件通过普通的Redis Pub/Sub机制完成推送。但是,由于Pub/Sub没有提供持久化机制(因为它是fire and forget的),如果你的应用程序需要可靠的事件通知(reliable notification),键空间通知目前是不满足需求的。
由于值可以是结构化的,Redis并不是严格意义上的键-值存储。由于值可以是结构化的,因此支持不同类别的辅助索引(secondary indexes),包括组合(多列)索引就很有意义了。
Redis支持创建以下类型的索引:
- 对于有序集合,可以根据ID或者其它数字字段创建索引
- 基于词法范围(lexicographical ranges)的有序集合,可以创建更加复杂的辅助索引,组合索引,或者图遍历(graph traversal )索引
- 对于集合,可以创建随机索引
- 对于列表,可以创建简单的迭代索引以及最后N条目的索引
索引的实现和维护,是Redis服务器的高级主题。对于大部分使用复杂查询的用户来说,应该考虑是否利用关系型数据库更加合适。
在Redis中使用辅助索引的最简单方式是,使用有序集合 —— 基于浮点数Score来排序元素的数据结构。使用有序集合索引的两个基础命令是ZADD、ZRANGEBYSCORE,前者添加元素,后者根据Score进行范围扫描。
举例来说,你可以根据年龄来索引一系列的用户名:
1 2 3 4 5 6 7 |
# 添加元素 ZADD myindex 18 Anna ZADD myindex 35 Jon ZADD myindex 67 Helen # 索引范围查找 ZRANGEBYSCORE myindex 20 40 |
通常情况下,用户(User) 实体包含多个字段,而不仅仅是名字,这种情况下,可以在有序集合中存储用户的ID:
1 2 3 4 5 6 7 |
# 有序集合中存储实体标识符 ZADD user.age.index 38 1 ZADD user.age.index 42 2 # 实体以哈希方式另外存放 HMSET user:1 id 1 username antirez ctime 1444809424 age 38 HMSET user:2 id 2 username maria ctime 1444808132 age 42 |
这样,可以针对多个字段,分别建立索引。
ZADD还可以用来更新索引值,例如一年后,用户的年龄需要增加1(当然使用出身日期更简单),这时候,可以:
1 2 3 4 5 6 |
# 设置实体哈希的年龄字段 HSET user:1 age 39 # 更新索引 ZADD user.age.index 39 1 # 注意,上面的两个操作,可以使用MULTI/EXEC事务确保原子性 |
有序集合具有一个重要特性,当两个元素的Score相同时,它们将根据元素值进行字典排序(底层调用memcmp函数)。Redis的这种索引的内部实现方式和性能类似于关系型数据库的B树索引。使用字典序索引时,常常把Scoure一律设置为0。
使用 ZRANGEBYLEX、 ZLEXCOUNT 命令,可以使用字典序索引。
自动完成的例子
字典序索引的一个常见应用是,表单的快速自动完成提示。当用户键入bit时,可以在后端执行以下Redis命令:
1 2 |
# \xff表示最后一个字节的最大值可以是255 ZRANGEBYLEX myindex "[bit" "[bit\xff" |
如果需要对自动提示根据使用频率进行排序,我们可以扩展元素的值:
1 2 3 |
ZREM myindex 0 banana:1 # banana第二次被搜索的时候,设置频率为2 ZADD myindex 0 banana:2 |
这里用冒号来分隔自动完成关键字和搜索频率,由于字典序范围搜索在Redis中是二进制安全的,所以你可以使用任何分隔符,例如\0\0。
添加辅助信息
使用上述的分隔机制,我们可以在字典元素值中附加任意的内容,以满足应用需要。总之记住,字典序比较是以前缀为基准的。
前面我们介绍了索引单个字段的方式,那么,能不能像关系型数据库那样,索引多个字段呢。
考虑一个场景,我们需要对一个巨大仓库中的产品进行查询,依据是房间号、价格。这依赖于这两个字段的复合索引,实现起来其实很简单,关键之处还是字典序的前缀机制。我们需要对数字类型的索引字段进行前缀补齐,确保它们的长度总是一致,这样才能正确的进行字典序排序:
1 2 3 4 5 6 7 8 9 |
# room:price:product_id # 索引字段必须补齐 # 辅助信息产品ID不需要补齐 ZADD myindex 0 0056:0028.44:90 ZADD myindex 0 0034:0011.00:832 # 查询56号房间中,价格在10-30之间的产品 ZRANGEBYLEX myindex [0056:0010.00 [0056:0030.00 |
Redis集群(Cluster)提供了自动的把数据分区(Shard)到多个Redis节点(实例)的机制。
此外,集群也提供了某种程度的高可用性,当部分节点不可达的情况下,集群仍然可以运行。但是某些极端情况下,例如大多数Master不可达,则不行。
Redis集群中的节点需要两个端口,一个用于服务客户端,默认6379,另外一个端口比此端口大10000,用于集群总线。集群总线使用二进制协议,进行节点-节点之间的通信,用于故障检测(failure detection)、配置更新、故障转移授权等操作。
注意Redis集群与Docker的端口映射不兼容(也不兼容一般性的NAT环境、IP或者端口被重映射的环境),你可能需要使用host networking mode模式。
Redis集群不使用一致性哈希(consistent hashing),而是基于所谓Hash Slot进行分片,从概念上说,每个键都是Hash Slot的一部分。
集群中一共有16384个Hash Slot,计算键所属的Slot时,仅需要获得键的CRC16,然后针对16384进行取模操作。
集群中的每个节点负责所有Hash Slot的一个子集,当增减节点时,可以很方便的重新分配Hash Slot。把Hash Slot从一个节点转移到另外一个,不会引入downtime。
如果所有键都属于同一个Hash Slot,Redis集群支持针对这些键的multiple key操作。你可以使用Hash Tag强制一批键归属于同一个Slot。
为了避免因为Master子集故障,或者无法连接到大部分的Master节点导致的集群不可用,Redis使用主从节点模型,每个Hash Slot具有N份复制,其中一份位于Master节点,N-1份位于N-1个Slave节点上。当Master节点失败后,集群会自动推举它的一个Slave成为新的Master。
Redis集群不保证强一致性,这意味着某些情况下,服务器已经向客户端确认的写操作,其数据可能丢失。
导致写丢失的一个原因是Redis使用异步复制,考虑以下场景:
- 客户端向Master B发送写操作
- Master B向客户端应答OK
- Master B向它的Slave B1 B2 B3传播此写操作
可以看到,在同步到Slave之前,Master已经应答了客户端OK,Redis这样做是出于性能考虑。如果在第2、3步之间Master B宕机,Bx晋升为Master,则写操作就永远的丢失了。
上述场景和那些配置为每秒执行Flush操作的数据库类似,也就是说你以前可能也面临这种数据丢失的风险。此外,你可以在应答客户端之前强制Redis Flush数据,这可能会影响性能,但是可以改善一致性。
Redis集群可以提供同步写操作,如果你的确需要。使用WAIT命令,丢失写的可能性大大减小。注意,即使使用WAIT也不能保证强一致性,特殊的情况下,一个不能接收来自Master的写入操作的Slave可能晋升为Master。
首先,你需要若干个运行在集群模式的Redis实例。节点的最小化配置文件示例如下:
1 2 3 4 5 6 7 8 |
port 7000 cluster-enabled yes # 节点的集群配置文件,集群内部使用,不应该手工编辑 cluster-require-full-coverage no cluster-node-timeout 15000 cluster-config-file nodes.conf cluster-migration-barrier 1 appendonly yes |
节点的数量至少需要3个Master。本节我们使用6个节点,其余三个为Slave。依次分配端口7000 - 7005。 你可以依据端口分别创建目录,在目录中存放上面的配置文件和Redis服务器二进制文件。然后,启动Redis服务器,你就拥有了6个Redis实例。
在节点第一次启动时,nodes.conf尚不存在,redis会自动初始化此文件,并得到自己的唯一标识(Node ID):
1 |
[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1 |
要建立集群,可以使用Redis提供的命令行工具redis-trib,该工具包含在Redis 的源码中,依赖于:
1 |
gem install redis |
执行下面的命令即可建立集群:
1 2 3 4 5 |
# create表示建立集群 # --replicas 指定每个Master的Slave数量 # 后面的参数为每个节点的地址和端口 ./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \ 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 |
上述命令执行后,如果一起正常,屏幕上最终会打印:
1 |
[OK] All 16384 slots covered |
你可以随时执行下面的命令,判断集群状态是否正常:
1 2 3 4 5 6 |
redis-trib.rb check redis-node-ip:6379 # 理想输出 # [OK] All nodes agree about slots configuration. # >>> Check for open slots... # >>> Check slots coverage... # [OK] All 16384 slots covered. |
如果你不想如上面那样,手工的配置、执行每个Redis实例, 可以使用Redis的utils目录下的create-cluster,这是一个Bash脚本,在底层它还是调用redis-trib完成集群创建的。示例:
1 2 3 |
create-cluster create create-cluster start create-cluster stop |
重分区就是把一些Hash Slots从一批节点转移到另外一批节点的过程,我们同样需要使用redis-trib完成此工作:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 只需要提供一个集群节点,其它节点会自动找到 ./redis-trib.rb reshard 127.0.0.1:7000 # 回答问题,需要转移多少个Hash Slots How many slots do you want to move (from 1 to 16384)? # 根据提示,提供接收这些Slots的节点的ID # 最后,可以检查集群的健康状态 ./redis-trib.rb check 127.0.0.1:7000 # 非交互式的: ./redis-trib.rb reshard --from <node-id> --to <node-id> --slots <number of slots> --yes <host>:<port> |
某些时候,在Master没有宕机的时候,你就需要进行“故障转移”,例如Master机器需要进行硬件维护。手工故障转移时,数据安全性比被迫故障转移要高,数据不会在处理过程中丢失。
要实现手工的故障转移,需要使用命令 CLUSTER FAILOVER,注意必须在被转移的Master的某个Slave上执行该命令。
向集群中添加节点,基本上就是建立一个Redis实例,然后:
- 分配一些Slot给它,这种情况下新节点作为Master
- 作为一个已知节点的Replica,这种情况下新节点作为Slave
添加节点命令:
1 2 |
# 添加一个新节点 7006到集群 ./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000 |
新添加的节点,与其它Master的不同之处:
- 由于没有分配Slots,因此它不持有数据
- 由于没有分配Slots,它不参与Slave晋升的选举处理
通过重分区,可以为新节点分配Slots。
如果要作为Slave添加,可以参考如下脚本:
1 |
./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000 |
另外,你也可以使用CLUSTER REPLICATE命令完成上面的操作。
从集群中移除节点,可以执行命令: ./redis-trib del-node 127.0.0.1:7000 `<node-id>`
如果要移除的是Master节点,它必须是空的,如果不是,你需要Reshard它的数据到其它节点。
Redis集群支持重新配置Slave节点,让它成为其它Master的Slave。你只需要在Slave上执行:
1 |
CLUSTER REPLICATE <master-node-id> |
另外,Redis集群支持自动化的重新配置Slave节点以增强集群的可用性,不需要管理员手工介入。这种自动化的重新配置称为replicas migration。
Redis Sentinel为Redis提供高可用性(HA),利用Sentinel,你可以创建无人值守的高可用系统,自动处理某些故障。此外,Sentinel还负责一些附属任务,包括监控、通知,并作为客户端的配置提供者(configuration provider):
- 监控:不断的检查你的Master、Slave是否正常工作
- 通知:可以通过API通知系统管理员或者其他应用程序,某个Redis实例出现问题
- 自动故障转移:如果一个Master不能正常工作,Sentinel启动一个故障转移,把Master的某个Slave提升为新Master,并让其兄弟Slave重新配置为新Master的Slave。此外,使用Redis的客户端可以得到Master变更的通知
- 配置提供者:可以作为客户端服务发现的源,客户端连接到Sentinel,后者提供某个服务的Master地址,故障转移发生后,Sentinel会报告新的地址
Sentinel是一个分布式的系统,其本身设计为多个Sentinel进程相互协作的运作模式。多个Sentinel进程协作模式的优势:
- 当多个Sentinel进程一致认为某个Master不可用,则启动故障检测。这可以降低误报( false positives)的几率
- 如果部分Sentinel进程不可用,Sentinel作为一个整体仍然能够正常工作,这增强了系统的健壮性
当前版本为Sentinel 2,比起上个版本使用了更强大、简单的预测算法(predict algorithms),作为Redis 2.8+的一部分发布。
启动启用了Sentinel的Redis服务器,有两种方式:
1 2 3 |
redis-sentinel /path/to/sentinel.conf # 或者 redis-server /path/to/sentinel.conf --sentinel |
提供Sentinel配置文件是必须的,因为配置文件会被用来保存系统的状态,如果不提供配置文件则无法启动。Sentinel默认监听26379端口。
在部署Sentinel之前,你需要知道一些重要的事情:
- 你需要至少3个Sentinel实例,以确保健壮性
- 这三个Sentinel相互独立,不会同时发生故障。例如将它们部署在不同的物理服务器上,或者独立的几个虚拟机上
- Sentinel + Redis的分布式系统,不能保证已经确认的写操作在故障发生后不丢失,这是由于Redis异步复制机制导致的。但是,可以使用更安全的方式部署Sentinel,是数据丢失的窗口更小
- 客户端必须支持Sentinel,流行的客户端大多支持
- 注意测试,甚至是在生产环境下
- Sentinel、Docker或者其它形式的NAT/端口映射机制需要小心的共存。Docker的端口重映射破坏了Sentinel自动发现其它Sentinel、发现Master的Slave列表的能力
最小化的配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 指定每个需要监控的Master,为每个Master提供一个唯一性的名称 # 不需要指定Slave,因为可以自动发现。自动发现后,该配置文件会被Sentinel自动更小以反映Slave信息 # 每当故障转移发生的时候,该配置文件也会被自动更新 # 第一组Redis实例 # 定义Master组的选项: # sentinel monitor <master-group-name> <ip> <port> <quorum> # quorum的含义是,如果认定当前Master不可达,需要几个Sentinel进程同意 # quorum仅仅用于检测故障,要执行故障转移,某个Sentinel进程需要被推举为故障转移的Leader然后被授权执行故障转移 # 推举是由Sentinel进程内部进行的,如果大部分Sentinel同意推举则OK,这显然要求大部分Sentinel进程的相互可达 sentinel monitor mymaster 127.0.0.1 6379 2 # 其它选项的格式都是: # sentinel <option_name> <master_name> <option_value> # 至少多少mm不可达,Sentinel才开始考虑目标Master是否宕机 sentinel down-after-milliseconds mymaster 60000 sentinel failover-timeout mymaster 180000 # 在新的Master产生后,同时与之重新同步的兄弟Slave的最大数量。数字越低,故障转移消耗的时间越多 # 但是,如果Slave被配置为,继续使用旧数据对外服务,你可能不希望所有兄弟Slave同时重新同步 sentinel parallel-syncs mymaster 1 # 第二组Redis实例 sentinel monitor resque 192.168.1.3 6380 4 sentinel down-after-milliseconds resque 10000 sentinel failover-timeout resque 180000 sentinel parallel-syncs resque 5 |
所有配置项在运行时,都可以基于 SENTINEL SET命令来配置。
如果处于测试和开发的目的,可以零配置的启动Redis服务器。但是在生产环境下,你通常需要自定义配置。
提供自定义配置,可以编写配置文件redis.conf。该文件由一系列的指令组成,每个指令的格式如下:
1 2 3 4 5 |
keyword argument1 argument2 ... argumentN # 示例: slaveof 127.0.0.1 6380 # 如果某个参数包含空格,可以使用引号: requirepass "hello world" |
除了提供配置文件以外,还可以直接从命令行传递配置项:
1 2 |
# 格式与配置文件一样,只是每个配置项都具有 -- 前导标记 ./redis-server --port 6380 --slaveof 127.0.0.1 6379 |
要指定Redis使用的配置文件,可以这样启动服务:
1 |
./redis-server /path/to/redis.conf |
Redis支持运行时按需修改配置,而不需要重启服务器。Redis提供在运行时读取、写入配置的命令: CONFIG GET、 CONFIG SET,大部分指令支持基于这两个命令进行读写。
需要注意,CONFIG SET命令对服务器配置的修改不会持久化,因此服务重启后配置信息即丢失。从2.8开始,你可以调用 CONFIG REWRITE自动更新redis.conf,使其反映当前运行时的配置信息。
将Redis作为缓存服务使用时,你可以配置:
1 2 |
maxmemory 2mb maxmemory-policy allkeys-lru |
这样,你不需要为键设置过期信息,所有键均会基于LRU算法清除。
配置项 | 说明 |
基本配置 | |
include path-to-file | 包含其它的配置文件 |
protected-mode | 保护模式,如果on则仅仅本地连接可以访问 |
daemonize bool |
默认情况下Redis不是以守护程序的形式运行的 配置为daemonize yes则作为守护程序运行,Redis会在/var/run/redis.pid写PID文件 |
pidfile path-to-file | 定制PID文件的位置 |
port number | 监听端口,默认6379。如果设置为0则不监听TCP端口 |
tcp-backlog number | TCP连接建立请求最大排队数 |
bind ipaddr [ipaddr...] | 监听的IP地址,默认0.0.0.0 |
timeout number | 客户端空闲多久后,断开连接,默认0表示不断开 |
tcp-keepalive number | 如果不为0,启用TCP保活。默认0,如果启用,建议的值可以是60,表示每60秒使用SO_KEEPALIVE发送TCP ACKs |
loglevel level | 设置服务器日志级别,默认notice。依据日志冗长程度,可选debug、verbose、notice、warning |
logfile path-to-file | 日志文件的名称 |
syslog-enabled bool | 是否记录到操作系统日志 |
syslog-ident identity | 系统日志标识符,默认redis |
syslog-facility | 可选值USER,或者LOCAL0-LOCAL7。默认local0 |
databases number | 指定可以使用的数据库数量, 默认16。每个连接都可以选择不同的数据库 |
快照相关配置 | |
save delay keynum |
启用RDB快照。在delay秒内,如果有keynum或者更多的键被修改,执行持久化 该指令可以出现多次,可以使用 save ""取消前面所有save指令 |
stop-writes-on-bgsave-error bool |
默认取值yes,意味着,如果启用了RDB快照(至少一个savepoint),并且上一次发起的后台保存操作失败,则不再接受写入命令。加入后台保存线程恢复工作,则Redis会自动允许保存 设置为no,则在保存失败的情况下继续支持写操作 |
rdbcompression bool |
是否在dmup出.rdb文件时,启用压缩。默认yes |
rdbchecksum bool | 从版本5的RDB格式开始,文件尾部放置校验和。这可以检测文件破坏,但是会在保存、加载RDB时,损失10%左右的性能。默认yes |
dbfilename filename | Dump出RDB时,使用的文件名,默认dump.rdb |
dir path-to-dir |
Dump出RDB时的工作目录,默认./ AOF也存放在此目录 |
主从复制相关配置 | |
slaveof masterip masterport | 设置当前实例为某个实例的Slave |
masterauth master-password | 如果Master启用了密码保护(基于requirepass配置项),则这里需要提供此密码 |
slave-serve-stale-data bool |
当Slave失去到Master的网络连接,或者复制操作正在进行中,该选项可以设置为:
|
slave-read-only bool |
设置Slave是否是只读的,从2.6开始默认yes 允许Slave可写,可以用它存放一些生存期短暂的数据。因为每次和Slave重新同步时,这些数据都被清除 |
repl-diskless-sync bool |
是否启用无盘复制,默认no 对于新的或者重新连接到Master的Slave,不能进行增量同步,必须首先进行完整同步——把RDB文件传输给Slave。传输有两种方式:
|
repl-diskless-sync-delay sec |
上面提到的无盘同步方式,产生传输RDB的子进程的延迟,默认5秒 |
repl-ping-slave-period sec | Slave会定期发送PING给Master,默认10秒 |
repl-timeout sec |
主从复制的超时时间,超时用于:
该配置的值必须比repl-ping-slave-period大,否则总是会超时 |
repl-disable-tcp-nodelay bool |
同步完成后,是否在Slave套接字上禁用TCP_NODELAY,默认否 在禁用TCP_NODELAY的情况下,Redis会使用更小的TCP数据包、更小的带宽峰值来发送数据到Slave,但是会导致数据延迟。在Linux内核默认配置下,Slave可能在40ms后才看到Master上的数据 |
repl-backlog-size number |
主从复制排队(Backlog)大小,默认值1mb。Backlog是一个数据缓冲区,当Slave断开并重连后,如果使用此缓冲区就足以进行增量同步,则不进行完整同步 仅当至少一个Slave连接到本Master的情况下,才分配此缓冲区 在很多环境下这个默认值显得太小,可能需要调整到100MB |
repl-backlog-ttl sec | 默认600,表示当最后一个Slave断开到Master的连接之后,再过多少秒,清空Backlog |
slave-priority number | Slave优先级,默认100。数值越低,有有资格晋升为Master。数值0表示永远不能晋升 |
min-slaves-to-write number | 如果连接的Slave的数量少于指定的数字,Master拒绝写入操作,默认0 |
min-slaves-max-lag sec | 如果最慢的Slave,其延迟超过指定的秒数,Master拒绝写入操作,默认10 |
安全性配置 | |
requirepass passwd | 要求客户端在发起其它命令之前,先发起AUTH进行身份认证 |
rename-command cmd newname |
可以执行命令的重命名,从而避免客户端调用某些重要命令 newname为""则禁用 |
资源限制配置 | |
maxclients num | 同时连接的最大客户端数,默认10000 |
maxmemory num |
允许Redis使用的最大内存,如果超过此限制,Redis会根据清除策略来移除键 如果清除策略为noeviction,则那些可能导致内存占用增加的命令,都会收到错误 |
maxmemory-policy policy |
当最大内存限制到达时,Redis选择什么键执行移除: 默认noeviction |
maxmemory-samples | 为了节省资源,Redis使用的LRU和最小化TTL算法是近似算法。该配置项用于微调,数字越大,CPU消耗越大,但是算法越精确。默认值5,较为适合生产环境,如果取值10则足够精确,但是较慢,如果取值3则足够快,但是不怎么精确 |
APPEND ONLY 配置 | |
appendonly bool | 是否启用AOF模式的持久化,默认no |
appendfilename filename | AOF文件的名字,默认appendonly.aof |
appendfsync when |
设置Redis调用fsync的频率。fsync调用导致操作系统立即同步写操作到磁盘,而不是在OS缓存中等待更多的输出。可选值:
|
no-appendfsync-on-rewrite bool |
当AOF策略设置为everysec或者always时,如果一个后台保存线程(一个后台保存或者AOF日志后台重写线程)正在执行很多磁盘IO操作,此时Redis主线程调用fsync可能(在某些Linux配置下)阻塞过长时间,此现象目前无法避免 为了缓和此问题,可以设置此选项的值为yes(默认值no),当BGSAVE、BGREWRITEAOF正在进行时,防止主线程调用fsync
|
auto-aof-rewrite-percentage 100 |
配置自动触发的AOF重写,通过重写,AOF文件的尺寸可以得到优化。AOF重写会隐含的触发BGREWRITEAOF命令 当AOF日志文件增长了一定比例(以上一次重写后AOF文件,如果启动后从来没有进行过重写,则以启动时的AOF文件为基准)后,可以触发重写。第一个配置项设置此比例,设置为0则禁用自动的AOF重写 当AOF文件小于某个尺寸时,可以总不触发重写,第二个配置项设置此尺寸 |
auto-aof-rewrite-min-size 64mb | |
aof-load-truncated yes |
当操作系统崩溃后,特别是挂在Ext4文件系统时没有指定data=ordered选项的情况下,你可能发现在Redis启动时,AOF文件被截断了 AOF截断的情况发生后,Redis要么:
该配置项就是控制这个行为的
|
集群配置 | |
cluster-enabled yes | 是否启用集群,如果设置为yes则以集群节点的方式启动当前实例 |
cluster-config-file name |
集群的每个节点具有一个集群配置文件。该文件通常不应该手工编辑,它由节点自己创建和更新 该配置用于手工指定集群配置文件的名称,注意,集群中每个节点必须拥有唯一性名称 |
cluster-node-timeout 15000 | 在认为一个节点进入失败状态(Failure state)之前,它必须处于不可达(unreachable)状态的毫秒数 |
cluster-slave-validity-factor 10 |
如果一个Slave的数据貌似过于陈旧,它不会在Master宕机后作为故障转移的目标。陈旧的判断依据:
上述第二条的“久远”的阈值,计算公式为: (node-timeout * slave-validity-factor) + repl-ping-slave-period 公式中的slave-validity-factor即为当前所述的配置项
|
cluster-migration-barrier 1 |
Slave可以迁移到孤立Master —— 即那些没有工作中的Slave的Master。该特性增强了Redis集群的健壮性,避免孤立Master无法故障转移 为了避免迁移后,Slave原先的Master又变成孤立Master,可以设置一个阈值,即迁移后,原先Master至少拥有的工作中的Slave数量 |
cluster-require-full-coverage yes |
默认的,如果有任何一个Hash Slot没有被覆盖(分配给某个Master),则集群拒绝接受查询请求。这样,如果集群的一部分节点关闭,则整个集群不再可用 这个默认行为可以改变,设置为no即可 |
缓慢日志 | |
slowlog-log-slower-than 10000 | 判定查询为缓慢的阈值,单位微秒。设置为0则记录所有命令 |
slowlog-max-len 128 | 缓慢日志的最大长度 |
延迟监控 | |
latency-monitor-threshold 00 |
Redis的延迟监控子系统在运行时分析不同操作的样本,以分析Redis实例高延迟的可能原因 该系统仅仅记录消耗时间大于latency-monitor-threshold的操作。如果latency-monitor-threshold设置为0,则延迟监控系统被关闭 |
事件通知 | |
notify-keyspace-events "" | Redis可以通知Pub/Sub客户端键空间中发生的事件,该配置项指定启用哪些通知 |
该工具是Redis提供的命令行接口,它具有两种工作模式:交互式(REPL - 读取、估算、打印循环)、脚本式(命令作为redis-cli的参数提供)。
在交互式环境下,键入clear可以清屏,键入help可以获取帮助,键入exit可以退出交互模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# 执行一个命令,把键mycounter的值增加1 redis-cli incr mycounter # 输出如下,括号内是值的类型: # (integer) 7 # 强制输出原始数据 redis-cli --raw incr mycounter # 强制输出易读数据 redis-cli --no-raw mycounter # 默认情况下,redis-cli链接到本机的6379端口,你可以定制主机、端口 redis-cli -h hostname -p 6390 ping # PONG # 默认情况下,redis-cli操控数据库序号0,你可以指定操控的数据库序号: redis-cli flushall redis-cli -n 1 incr a # 操控数据库1 # 从文本中读取foo的值,并写入到Redis redis-cli -x set foo < fooval.txt # 从文本中读取一系列redis-cli命令,并交付执行 # cat /tmp/commands.txt | redis-cli # commands.txt的内容如下: # set foo 100 # incr foo # append foo xxx # get foo # ... # 连续执行、延迟执行:每隔100ms执行命令,连续5次 redis-cli -r 5 -i 100 incr foo # 在交互式环境下,可以使用如下命令连接到其它Redis实例 connect host 6379 # 在连接断开后,CLI会自动尝试重新连接,如果连接失败,会给出提示信息 # 执行下面的命令手工重新连接 ping # 连续打印Redis的统计信息,例如内存用量 redis-cli --stat # 扫描整个键空间,来获得最大的键 redis-cli --bigkeys # 扫描键空间 redis-cli --scan # 扫描键空间,打印匹配指定正则式的键 redis-cli --scan --pattern '*-11*' # 持续监控在Redis中执行的命令 redis-cli monitor # 监控Redis实例的延迟,不停的发送PING命令 redis-cli --latency # RDB远程备份:在Redis复制模式第一次同步的时候,Master/Slave以RDB文件的形式交换整个数据集 # 你可以通过CLI利用这一特性,实现远程备份 redis-cli --rdb /tmp/dump.rdb #该命令获取Master的Dump,保存到本地 |
某些命令是管理性的,甚至能立即清空数据库,这类命令可以重命名,甚至禁用,以防客户端误用:
1 |
rename-command FLUSHALL "" |
这是一个非常轻量易用的Java客户端,完全兼容Redis 2.8.x和3.x.x。
Maven依赖:
1 2 3 4 5 6 7 |
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> <type>jar</type> <scope>compile</scope> </dependency> |
简单的客户端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Jedis jedis = new Jedis( "localhost", 6379 ); final String HELLO_KEY = "Hello"; final String NUMS_KEY = "Numbers"; jedis.set( HELLO_KEY, "World" ); jedis.get( HELLO_KEY ); jedis.lpush( NUMS_KEY, "1", "2", "3" ); jedis.lpop( NUMS_KEY ); // 使用Redis事务 Transaction multi = jedis.multi(); jedis.set( HELLO_KEY, "Alex" ); jedis.expire( HELLO_KEY, 10 ); multi.exec(); jedis.close(); |
使用连接池:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
JedisPoolConfig cfg = new JedisPoolConfig(); cfg.setMaxTotal( 100 ); JedisPool pool = new JedisPool( cfg, "localhost", 6379 ); Jedis jedis = pool.getResource(); jedis.auth( "foobared" ); jedis.set( "foo", "0" ); jedis.close(); jedis = pool.getResource(); jedis.auth( "foobared" ); jedis.incr( "foo" ); jedis.close(); pool.destroy(); |
定义RestTemplate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Bean JedisConnectionFactory jedisConnectionFactory() { JedisConnectionFactory jcf = new JedisConnectionFactory(); jcf.setHostName( "10.4.37.22" ); jcf.setPort( 6079 ); jcf.setDatabase( 0 ); jcf.setUsePool( true ); JedisPoolConfig cfg = new JedisPoolConfig(); cfg.setMaxTotal( 256 ); cfg.setMaxIdle( 16 ); jcf.setPoolConfig( cfg ); return jcf; } @Bean public RedisTemplate<String, Object> redisTemplateNoCompress() { final RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory( jedisConnectionFactory() ); template.setValueSerializer( new GenericToStringSerializer<Object>( Object.class ) ); return template; } |
使用RestTemplate:
1 2 3 4 |
// SET/GET操作 ValueOperations<String, Object> ops = template.opsForValue(); ops.set( key, value ); ops.get( key ); |
定制串行化器,示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
package com.dangdang.digital.spring.data.redis; import net.jpountz.lz4.LZ4Compressor; import net.jpountz.lz4.LZ4Factory; import net.jpountz.lz4.LZ4FastDecompressor; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.nio.ByteBuffer; import java.util.Arrays; /** * 基于LZ4算法的RedisSerializer装饰器 * * @param <T> 串行化处理的目标类型 */ public class LZ4RedisSerializeDecorator<T> implements RedisSerializer<T> { private final int compressThreshold; private final RedisSerializer<T> serializer; private LZ4Factory factory; private LZ4Compressor compressor; private LZ4FastDecompressor decompressor; private static final int COMPRESS_HEAD_SIZE = Integer.SIZE / Byte.SIZE; private static final int NOCOMPRESS_HEAD_SIZE = 1; /** * 构造函数 * * @param serializer 被装饰的串行化器 * @param fastMode 是否使用快速模式,快速模式压缩比低但是压缩的速度快 * 快速模式不会让解压缩的速度更快 * @param compressThreshold 启用压缩的最小字节数 */ public LZ4RedisSerializeDecorator( RedisSerializer<T> serializer, boolean fastMode, int compressThreshold ) { this.serializer = serializer; this.compressThreshold = compressThreshold; factory = LZ4Factory.fastestInstance(); if ( fastMode ) { compressor = factory.fastCompressor(); } else { compressor = factory.highCompressor(); } decompressor = factory.fastDecompressor(); } public byte[] serialize( T src ) throws SerializationException { byte[] srcBytes = serializer.serialize( src ); int srcLen = srcBytes.length; ByteBuffer destBuf; if ( srcLen < compressThreshold ) { destBuf = ByteBuffer.allocate( NOCOMPRESS_HEAD_SIZE + srcLen ); destBuf.put( (byte) -1 ); destBuf.put( srcBytes ); onSerialize( false, srcLen, srcLen ); return destBuf.array(); } else { int maxDestLen = compressor.maxCompressedLength( srcLen ); destBuf = ByteBuffer.allocate( COMPRESS_HEAD_SIZE + maxDestLen ); destBuf.putInt( srcLen ); byte[] destArray = destBuf.array(); int compressedLength = compressor.compress( srcBytes, 0, srcLen, destArray, destBuf.position(), maxDestLen ); onSerialize( true, srcLen, compressedLength ); return Arrays.copyOfRange( destArray, 0, COMPRESS_HEAD_SIZE + compressedLength ); } } public T deserialize( byte[] src ) throws SerializationException { if ( src == null ) return serializer.deserialize( src ); ByteBuffer srcBuf = ByteBuffer.wrap( src ); boolean noCompressed = srcBuf.array()[0] < 0; if ( noCompressed ) { byte[] destBytes = Arrays.copyOfRange( src, 1, src.length ); return serializer.deserialize( destBytes ); } else { int destLen = srcBuf.getInt(); byte[] destBytes = new byte[destLen]; decompressor.decompress( src, srcBuf.position(), destBytes, 0, destLen ); return serializer.deserialize( destBytes ); } } protected void onSerialize( boolean compressed, int srcLen, int compressedLen ) { } } |
Lettuce是一个可扩容的、线程安全的Redis客户端,提供同步、异步、响应式(reactive)的API。
在不使用阻塞性、事务性操作 —— 例如BLPOP、MULTI/EXEC ——的情况下,多个Java线程可以共享一个连接。到Redis服务器的连接由netty框架管理。Letttuce支持Sentinel、Cluster等高级特性。
Maven依赖:
1 2 3 4 5 |
<dependency> <groupId>biz.paluch.redis</groupId> <artifactId>lettuce</artifactId> <version>4.3.1.Final</version> </dependency> |
开启连接:
1 2 3 4 5 6 |
// 使用URI连接到本机的Redis实例,使用数据库0 // redis://[password@]host[:port][/databaseNumber] // 如果是使用SSL的连接,协议部分改为rediss RedisClient redisClient = RedisClient.create( "redis://passwd@localhost:6379/0" ); // 显式创建连接 StatefulRedisConnection<String, String> connection = redisClient.connect(); |
资源清理:
1 2 3 |
// 资源清理 connection.close(); redisClient.shutdown(); |
通过Sentinel连接:
1 2 |
// redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber]#sentinelMasterId RedisClient redisClient = RedisClient.create("redis-sentinel://localhost:26379,localhost:26380/0#mymaster"); |
连接到集群:
1 2 3 |
// Syntax: redis://[password@]host[:port] RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7379"); StatefulRedisClusterConnection<String, String> connection = redisClient.connect(); |
基于Spring:
1 2 3 |
<bean id="RedisClient" class="com.lambdaworks.redis.support.RedisClientFactoryBean"> <property name="uri" value="redis://localhost:6379"/> </bean> |
同步API:
1 2 |
RedisCommands<String, String> syncCommands = connection.sync(); syncCommands.set( "Hello", "World" ); |
异步API:
1 2 3 |
RedisAsyncCommands<String, String> asyncCommands = connection.async(); RedisFuture<String> future = asyncCommands.get( "Hello" ); String value = future.get( 1, TimeUnit.MINUTES ); |
响应式API:
1 2 3 4 5 6 |
RedisStringReactiveCommands<String, String> reactiveCommands = connection.reactive(); reactiveCommands.get( "Hello" ).subscribe( new Action1<String>() { public void call( String value ) { System.out.println( value ); } } ); |
首先需要安装包: sudo pip install redis
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import redis if __name__ == '__main__': client = redis.StrictRedis(host='localhost', port=6379, db=0, password='passwd', encoding='utf-8') client.set('Hello', '世界') print(client.get('Hello').decode()) # 管道,在一个请求中发送多个命令给服务区 pipeline = client.pipeline() pipeline.set('Name', 'Alex') pipeline.get('Name') # 批量执行命令,返回结果的列表 # transaction默认为true,能确保命令被原子的执行 resp = pipeline.execute(transaction=True) print(resp) # [True, b'Alex'] |
下面是搭配Calico CNI的Redis集群样例。
配置文件内容:
1 2 3 4 5 6 7 8 |
port 6379 cluster-enabled yes cluster-require-full-coverage no cluster-node-timeout 5000 cluster-config-file nodes.conf cluster-migration-barrier 1 appendonly yes dir /data |
创建ConfigMap:
1 |
kubectl -n dev create configmap redis-cluster-conf --from-file=redis.conf |
模板如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: redis-cluster-8-pvc namespace: dev spec: storageClassName: rook-block accessModes: - ReadWriteOnce resources: requests: storage: 256Mi --- apiVersion: v1 kind: Pod metadata: namespace: dev name: redis-cluster-8 labels: app: redis-cluster annotations: "cni.projectcalico.org/ipAddrs": "[\"172.27.0.18\"]" spec: terminationGracePeriodSeconds: 10 containers: - name: redis image: docker.gmem.cc/redis-cluster ports: - containerPort: 6379 name: client - containerPort: 16379 name: gossip command: - /usr/local/bin/redis-server args: - /usr/local/etc/redis/redis.conf readinessProbe: exec: command: - sh - -c - "/usr/local/bin/redis-cli -h $(hostname) ping" initialDelaySeconds: 15 timeoutSeconds: 5 livenessProbe: exec: command: - sh - -c - "redis-cli -h $(hostname) ping" initialDelaySeconds: 20 periodSeconds: 3 resources: limits: cpu: 100m memory: 256Mi volumeMounts: - name: redis-cluster-conf-volume mountPath: /usr/local/etc/redis - name: redis-cluster-8-pv mountPath: /data volumes: - name: redis-cluster-conf-volume configMap: name: redis-cluster-conf - name: redis-cluster-8-pv persistentVolumeClaim: claimName: redis-cluster-8-pvc |
执行下面的命令创建集群:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
./redis-trib.rb create --replicas 1 172.27.0.10:6379 172.27.0.11:6379 172.27.0.12:6379 \ 172.27.0.13:6379 172.27.0.14:6379 172.27.0.15:6379 172.27.0.16:6379 172.27.0.17:6379 \ 172.27.0.18:6379 172.27.0.19:6379 # >>> Creating cluster # >>> Performing hash slots allocation on 10 nodes... # Using 5 masters: # 172.27.0.10:6379 # 172.27.0.11:6379 # 172.27.0.12:6379 # 172.27.0.13:6379 # 172.27.0.14:6379 # Adding replica 172.27.0.15:6379 to 172.27.0.10:6379 # Adding replica 172.27.0.16:6379 to 172.27.0.11:6379 # Adding replica 172.27.0.17:6379 to 172.27.0.12:6379 # Adding replica 172.27.0.18:6379 to 172.27.0.13:6379 # Adding replica 172.27.0.19:6379 to 172.27.0.14:6379 |
根据提示操作,默认前面5个节点作为Master,后面5个作为Slave。
执行如下命令连接到集群节点(Pod):
1 |
kubectl -n dev exec redis-cluster-0 -it bash |
运行Redis命令行工具redis-cli,查看集群信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# CLUSTER INFO cluster_state:ok # 全部Slot已经分配 cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:10 # 集群的大小取决于Master cluster_size:5 cluster_current_epoch:10 cluster_my_epoch:1 cluster_stats_messages_sent:2301 cluster_stats_messages_received:2301 |
- 实现了Redis Cluster
- 多种性能提升
- PSYNC 2.0,增强复制性能,避免不必要的全量复制
- UNLINK命令,异步删除,提升性能
- SWAPDB,交换两个数据库
- 混合持久化,一种新的RDB-AOF混合持久化模式。开启后,AOF重写产生的文件将同时包含RDB格式的内容以及AOF格式的内容。其中RDB格式的部分记录已有数据,AOF则记录最新的增量数据。利用选项aof-use-rdb-preamble开启
- 集群模式兼容NAT和Docker
- 支持自动碎片整理activedefrag,相关配置:
12345678910111213# 开启自动内存碎片整理(总开关)activedefrag yes# 当碎片达到 100mb 时,开启内存碎片整理active-defrag-ignore-bytes 100mb# 当碎片超过 10% 时,开启内存碎片整理active-defrag-threshold-lower 10# 内存碎片超过 100%,则尽最大努力整理active-defrag-threshold-upper 100# 内存自动整理占用资源最小百分比active-defrag-cycle-min 25# 内存自动整理占用资源最大百分比active-defrag-cycle-max 75
- 增加Stream类型,相比起List、Zset等获取元数据更加高效。作为订阅发布模型,比Pub/Sub起来支持:消息持久化、消息分组、ACK等
- 不再使用redis-trib脚本,直接用redis-cli --cluster来进行集群管理
- 自动碎片整理V2,配合Jemalloc,更快更智能
- HyperLogLog算法改进,计数时内存效率更高
- 引入多线程IO,需要设置 io-threads-do-reads yes以启用。注意: Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行,这意味着不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。参数 io-threads控制IO线程数量,4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。线程数不是越多越好,一般认为超过8没有意义
- 支持SSL协议连接
- 支持对连接进行ACL控制
- 集群代理。因为 Redis Cluster 内部使用的是P2P中的Gossip协议,每个节点既可以从其他节点得到服务,也可以向其他节点提供服务,没有中心的概念,通过一个节点可以获取到整个集群的所有信息。所以如果应用连接Redis Cluster可以配置一个节点地址,也可以配置多个节点地址。但需要注意如果集群进行了增减节点的的操作,其应用也需要进行修改,这样会导致需要重启应用,非常的不友好。通过使用 redis-cluster-proxy可以与组成Redis集群的一组实例进行通讯,就像是单个实例一样
- 新增Function自定义函数库,函数库支持持久化与可复制
- Lua脚本(脚本本身代码)不再支持持久化和复制,仅对命令执行结果进行持久化和复制。
- ACL支持对Pub/Sub channel的权限控制
- 支持Multi-Part AOF
- 支持Client-Eviction
- 支持Sharded-Pub/Sub
- 支持命令执行耗时直方图
- 支持子命令级别的性能统计
- Ziplist编码替换为Listpack编码
- 支持Global Replication Buffer
和其它键值存储方案相比,Redis具有以下特点:
复杂数据类型支持 Redis支持多种数据类型,并在其上定义了一些原子操作。这些数据类型和常用的基础数据结构很类似并且直接暴露给程序员,没有添加额外的抽象层 |
驻留内存而又持久化到磁盘 为了支持可能大于内存总量的数据集,同时保证高速的读写,Redis在内存、磁盘存储之间进行了权衡。作为磁盘存储格式的RDB、AOF不需要考虑随机访问的支持,因而它们格式很紧凑,并且总是以append-only的方式生成 |
在64bit机器上,Redis内存用量大概如下:
- 实例本身占用 1MB左右内存
- 100万简短的键 - 字符串值,大概占用100MB内存
- 100万键 - 哈希值,每个哈希有5个字段,大概占用200MB内存
你可以使用redis-benchmark来自己测试内存占用。
64bit机器会占用比32位机器更多的内存,特别是键值都比较小的时候,这是因为前者指针长度为8字节。
命令行客户端默认不处理重定向指令,加上 -c 参数即可。
Leave a Reply