redis技术干货程序猿阵线联盟-汇总各类技术干货

Redis杂谈

2017-05-07  本文已影响293人  迷失于重逢

Redis杂谈

Redis是近年来发展迅速的内存数据库,网上也已经有多Redis的文章。但不管是英文还是中文,多数文章的各个知识点都比较分散,本系列是关于Redis主题的综合性讨论,也算是对我使用Redis的一个总结,主要面向已经使用Redis,但对于整体还不甚了解的Java程序员,当然也可以作为入门参考,对于重要的内容,本文力求把基本思想讲到,限于篇幅不能深入的内容,会给出相关细节的参考来源,本文写作时,Redis稳定版为 3.2.8。本系列由三部分构成:

好了,Let's go!

Redis介绍

Redis是什么

先看看维基百科是怎么说的:

Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。从2015年6月开始,Redis的开发由Redis Labs赞助,而2013年5月至2015年6月期间,其开发由Pivotal赞助。在2013年5月之前,其开发由VMware赞助。根据月度排行网站DB-Engines.com的数据显示,Redis是最流行的键值对存储数据库 。

这个描述中,有几个关键词:开源 支持网络 基于内存 可选持久性 键值对数据库,基本上概括了Redis的核心特征。Redis通过TCP套接字和一个简单的协议构建了一个服务器-客户端模式,因此,不同的进程能够以一种共享的方式查询和修改数据。

具体来说:

Redis的特点

理解Redis的特点,最好的入口,就是理解Redis常被形容为数据结构服务器,这到底是个啥?这的确是一个不常见的术语,所以Redis在首页就挂了这么一段话来解释自己对自己的定位:

Redis支持诸如strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs 和 geospatial indexes with radius等形式的数据结构,它内建了复制,Lua脚本,LRU缓存机制,事务,不同级别的数据持久化,并通过Redis Sentinel提供高可用性,和通过Redis Cluster提供自动分区。

其他类似产品

下图是DB-Engines.com根据一些特征,对使用Key-Value存储模式的数据库引擎进行的排名。常常拿来和Redis比较的,可能就是排在第二位的Memcached了。

image

虽然Memcached和Redis有众多不同,比如线程模式、存储模式等,但如果一言蔽之,诚如这篇StackOverflow中提到的:

Memcached 是一个非持久化的内存key/value数据库,Redis也能做得这一点, 但Redis还是一个可持久化的数据结构服务器

Redis的各种资料获取

Redis的资料非常丰富,建议优先阅读和查询官方的文档:

Redis基本命令

Redis使用的是server-client模式,使用Redis存取数据,就是通过Redis客户端向服务器端发送各种操作命令存取数据,大致的过程就是这样:

redis> ping
PONG
redis> info
# Server
redis_version:3.2.8
....

这里有一份不错的命令手册中文翻译

需要强调和不容易理解的一点是: Redis的整体结构是Key-Value的,但是和其他一些Key-Value产品不一样的是,这个Value本身可以是有数据结构的,比如Value本身可以是这么多类型:

  • Strings
  • Hashes
  • Lists
  • Sets
  • Sorted sets with range queries
  • Hyperloglogs
  • Geospatial indexes with radius queries.

怎么理解这一点呢,我们通过一个例子来说明:

redis>  HSET myhash field1 "foo"
(integer) 1
redis>  HGET myhash field1
"foo"
redis>  HGET myhash field2
(nil)
redis>  HSET myhash field2 "bar"
(integer) 1
redis> HMGET myhash field1 field2
1) "foo"
2) "bar"

这个例子存储的类型是Hash,也就是说Value本身也是一个Key-Value结构的数据。整体上就是Key-(Key-Value),我们可以理解为myhash是一个Hash表的名字,filed1,filed2是myhash这张Hash表中的键值,而myhash同时也是Redis这张大Hash表中的一个Key。

同理,不管Value是什么类型,它都有一个Key,这个Key就是Redis本身Key-Vaule的Key。

好的,现在就可以去对照命令手册使用Redis,什么,你还没有Redis的环境!没关系,Redis官方提供了一个 ==网页版Redis体验== 的供大家练手:

网页版Redis体验

Redis的安装与配置

安装

单机版的Redis的安装非常简单,Redis兼容的操作系统为:Linux, OSX, OpenBSD, NetBSD, FreeBSD。支持Big endian和Little endian 处理器架构, 支持64位和32位系统。

在Linux下的安装过程,只需要make命令就可:

   % make

如果是32位系统:

   % make 32bit

编译后可以通过make test测试:

   % make test

如有问题,可参考官方编译说明

Redis官方没有Windows版本,但是微软实现了一个Windows版的Redis Server

配置

Redis的配置文件是自注释的,写的密密麻麻,含义非常详细,清楚。运行时,把配置路径作为参数启动,使配置生效:

redis>redis-server /opt/redis/redis.conf

总共的配置项目超过50项,不过,在非集群模式下,通常关注的配置项目只有这些:

- maxmemory [3000m] 最大使用内存
- daemonize [yes|no] 是否后台启动
- dir [path] 持久化数据存放目录
- requirepass [password] 登录密码
- save [seconds] [changes] 在多少秒内有多少次写操作,就刷入一次数据到磁盘
- appendonly [yes|no] 是否开启APPEND ONLY模式,这也是一种持久化策略,下一节会介绍

这几个常用配置都非常好理解,但是第一个maxmemory要需要注意,这个配置涉及到配置Eviction policies,更多内容可以参考 LRU算法进行缓存回收

也可以参考这一份不错的中文配置说明,但是由于Redis发展非常迅速,所以生产环境中使用的配置项一定要对照官方说明。

比如Redis配置中的VM配置(虚拟内存机制),很多文章还在提,但是这个配置其实已经在不断的发展中被废弃了,这个配置的用意是VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中,棒棒哒,对吧?但是对于Redis这么一个小软件,希望把存储做成如同Oracle一样的方式,具备自动淘汰冷热数据功能,并且比Linux操作系统本身更加优秀,太难了。

对于Linux系统,在配置文件之外,还有一些配置需要考虑:

修改内存分配策略,使系统请求分配内存时,永远假装还有足够的内存
echo 'vm.overcommit_memory = 1' >>/etc/sysctl.conf
然后执行: sysctl vm.overcommit_memory=1

定义了系统中每一个端口最大的监听队列的长度,这是个全局的参数,默认128。
echo 1024 > /proc/sys/net/core/somaxconn

禁用透明缓存页
echo never > /sys/kernel/mm/transparent_hugepage/enabled

关于这部分的内容,可以参考 Redis AdministrationRedis latency problems troubleshooting

Redis 的数据持久化

理解Redis的数据持久化对于使用Redis特别重要,因为? 当然是因为不能随便把数据搞丢,还有什么比这更重要么!况且可配置持久化,也是很多用户选择Redis的重要原因。

Redis官方有一篇专门阐述其持久化的文档,以及Redis开发者Salvatore针对这个问题撰写的一篇长文《Redis 持久化解密》

不管他们怎么说,其实归纳起来我们就想知道3个问题:

一般来说Redis的所有工作数据都在内存中,这也是内存数据库的特点,持久化数据只是启动时加载,或者作为灾备手段。前面已经提到了Redis的持久化有两种方式:RDB和AOF。

RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储,它是一个非常紧凑的文件,它保存了某个时间点得数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集。

RDB的持久化方式被称为快照,在默认情况下,Redis将数据库快照保存在名字为dump.rdb的二进制文件中。你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。你也可以通过调用 SAVE或者 BGSAVE , 手动让 Redis 进行数据集保存操作。

比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:

save 60 1000

AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。一个AOF文件就像这个样子:

$ cat appendonly.aof 
*2
$6
SELECT
$1
0
*3
$3
set
$4
key1
$5
Hello
*3
$6
append
$4
key1
$7
 World!
*2
$3
del
$4
key1

你可以配置 Redis 多久才将数据 fsync 到磁盘一次。有三种方式:

官方推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

有两点需要注意:

优缺点

RDB的优点

RDB的缺点

AOF 优点

AOF 缺点

如何选择

关于如何选择,官方这么说:

一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug 。
注意: 因为以上提到的种种原因, 未来我们可能会将 AOF 和 RDB 整合成单个持久化模型。

Two More Things ^_^

首先,官方所谓“未来我们可能会将 AOF 和 RDB 整合成单个持久化模型”在某种程度上已经在4.0版本中实现了,这个改进可以分为两个层次来看:其一是AOF的实现机制,导致AOF文件太大,4.0 可以配置AOF,使其仅仅进行增量记录;其二是集群下主备必须全量复制,这种机制被更改为称为PSYNC2.0的带标签复制,看到Salvatore给五年前的一个留言的回复,我想他那刻的内心必是喜悦的(被你们TM怼了5年了啦)。

Reply4FiveYears

其次,我们回过头来,讨论一下到底什么叫数据持久化。非常简化的来看,数据持久化可以分为这么5步:

  1. 客户端发送一个写命令到数据库(数据在客户端的内存中)。
  2. 数据库接收到这个写命令(数据在服务器的内存中)。
  3. 数据库调用系统调用把写数据存入磁盘(数据在内核缓冲区kernel's buffer)
  4. 操作系统把写缓冲区数据传输到磁盘控制器(数据在磁盘缓存中)
  5. 磁盘控制器真正把数据写到物理介质上。

传统的Unix系统(Linux)实现在内核中没有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时(比如 POSIX API 的write系统调用),内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写。通常,当内核需要重用缓冲区来存放其他磁盘数据时,他会把所有延迟写数据写入磁盘。为保证磁盘上实际文件与缓冲区中的内容一致,Unix系统提供了sync,fsync和fdatasync三个函数。

其中,sync与fsync的区别在于,sync只是将所有修改过的块缓冲区排入写队列,然后返回,它并不等待实际写磁盘操作结束,一般update系统守护进程会周期性调用sync函数(Linux是30s),fsync函数需要传入文件描述符,它会等到磁盘写操作结束才返回。可以想见,虽然每个操作都调用fsync是最保险的做法,但是这种大量随机寻址对于任何运行于Rotational disks的应用来说,都是非常慢的。

上述内容旨在让读者理解数据持久化为什么需要各种策略,以及各种策略的意义,都是点到为止。

Redis集群

这一节,我们会谈论Redis的集群,。集群简单的理解就是一堆机器齐心协力提供某种类型的服务。对于Redis集群,我们关心这么几个内容:数据是如何分布的,消息是如何传递的,出现异常的如何应对。Redis集群研究和实践(基于redis 3.0.5)这篇文章根据官方的安装指南,记录了非常详细的安装和操作步骤,本文就不在赘述了,我们主要重点理解一下前面提到的三个问题。

一致性哈希

之所以要介绍这个概念,是因为一致性哈希是Web Cache类系统,用于数据分布最典型的设计(Memcached使用一致性哈希),我们将在后面一节与Redis的哈希槽(hash slot)进行一个比较。

一致性哈希在Wiki上讲的非常清楚:

需求

在使用n台缓存服务器时,一种常用的负载均衡方式是,对资源o的请求使用hash(o) = o mod n来映射到某一台缓存服务器。当增加或减少一台缓存服务器时这种方式可能会改变所有资源对应的hash值,也就是所有的缓存都失效了,这会使得缓存服务器大量集中地向原始内容服务器更新缓存。因些需要一致哈希算法来避免这样的问题。

一致哈希尽可能使同一个资源映射到同一台缓存服务器。这种方式要求增加一台缓存服务器时,新的服务器尽量分担存储其他所有服务器的缓存资源。减少一台缓存服务器时,其他所有服务器也可以尽量分担存储它的缓存资源。 一致哈希算法的主要思想是将每个缓存服务器与一个或多个哈希值域区间关联起来,其中区间边界通过计算缓存服务器对应的哈希值来决定。(定义区间的哈希函数不一定和计算缓存服务器哈希值的函数相同,但是两个函数的返回值的范围需要匹配。)如果一个缓存服务器被移除,则它会从对应的区间会被并入到邻近的区间,其他的缓存服务器不需要任何改变。

也许上个图,更容易理解:

一致性哈希

实现

一致哈希将每个对象映射到圆环边上的一个点,系统再将可用的节点机器映射到圆环的不同位置。查找某个对象对应的机器时,需要用一致哈希算法计算得到对象对应圆环边上位置,沿着圆环边上查找直到遇到某个节点机器,这台机器即为对象应该保存的位置。

当删除一台节点机器时,这台机器上保存的所有对象都要移动到下一台机器。添加一台机器到圆环边上某个点时,这个点的下一台机器需要将这个节点前对应的对象移动到新机器上。 更改对象在节点机器上的分布可以通过调整节点机器的位置来实现。

其实,也要不了几行代码

import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;

public class ConsistentHash<T> {

 private final HashFunction hashFunction;
 private final int numberOfReplicas;
 private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();

 public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
     Collection<T> nodes) {
   this.hashFunction = hashFunction;
   this.numberOfReplicas = numberOfReplicas;

   for (T node : nodes) {
     add(node);
   }
 }

 public void add(T node) {
   for (int i = 0; i < numberOfReplicas; i++) {
     circle.put(hashFunction.hash(node.toString() + i), node);
   }
 }

 public void remove(T node) {
   for (int i = 0; i < numberOfReplicas; i++) {
     circle.remove(hashFunction.hash(node.toString() + i));
   }
 }

 public T get(Object key) {
   if (circle.isEmpty()) {
     return null;
   }
   int hash = hashFunction.hash(key);
   if (!circle.containsKey(hash)) {
     SortedMap<Integer, T> tailMap = circle.tailMap(hash);
     hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
   }
   return circle.get(hash);
 }

}

Redis哈希槽与数据分布

Redis采用的是哈希槽的机制,它通过函数hash(key) = CRC16(key)%16384将任意一个key映射到0-16383这个范围,每个节点承接16384个key中的一段。如果增加或删除一个节点,手动变更节点的承接范围,具体的操作可以参考前面提到的《Redis集群研究和实践》。

现在,我们再回到上面实现的这个一致性哈希,假设传入了这样一个hash函数,并且手动对node添加后的节点承载范围进行调整:

private static final int RING = 16384;

public Integer hash(String key){
  return  CRC16(key)%RING;
}    

 public T get(Object key) {
   return key-nodesTable.get(hash);
 }
  public void add(T node) {
   //手动
   //Step 1: ...
   //Step 2: ...
 }

这几乎就是Reids的哈希槽方案。这样的方案,简单、粗暴、直接并且有效,不过就是要麻烦你动动手去规划。我认为,一致性哈希的本质就是通过在Hash->Node之间虚拟一个中间层使之变成Hash->RING Point->Node,从而避免Node增删带来的全局映射变动。从这个意义上说,Redis的哈希槽就是一个简化版的一致性哈希方案。

这里我们不评价这种简化版一致性哈希方案的优劣,但它的确规避了系统对于节点增加或者删除后,自动处理数据迁移,以及节点规划给系统带来的复杂性。

Gossip协议

Redis节点间的消息使用Gossip协议传播,它常用于P2P的通信协议,这个协议就是模拟人类中传播谣言的行为而来。

协议的核心内容就是节点通过将信息随机发送到N个节点来完成本次信息的传播,其涉及到周期性、配对、交互模式。Gossip的交互模式分为两种:Anti-entropy和Rumor mongering。

每个节点维护一个自己的信息表<key, (value, version)>,即属性的值以及版本号;和一个记录其他节点的信息表<node, <key, (value, version)>>。每个节点和系统中的某个节点相互配对成为peer。而节点的信息交换方式主要有3种。

可以证明Gossip协议的传播次数是收敛的。

传播起来整个Redis集群内部一共有N*(N-1)条传输路径,路径真的实在太多了,以至于开发者画出来的图都少了两条(红线补齐),就大概就像这个样子:

Gossip协议

和Server与Client的不一样,Redis内部节点间采用的是二进制协议以优化带宽。Redis节点间的“谣言”,大概是这个样子的:

Gossip内容

部分故障

对于一个分布式系统,最大的挑战就是要是节点挂了怎么办,或者更具体的说如何知道一个节点是不是真的挂了,这也就是所谓的分布式系统的本质困难:“partial failure(部分故障)”。但是不得不说,Redis的实现弱化了这个困难,因为它没有提供通常意义上说的高可用性。

当Redis集群中的一个主节点挂了之后,Goosip协议会选择一个备节点替换上来,如果没有备节点,整个集群系统就不可用了。是的,整体不可用!

这样的设计避免了数据迁移和数据分布自动平衡,也避免了部分可用性需要进行的一些屏蔽和逻辑阻断。

具体来说,Redis的每个节点都拥有一个与其他节点相关的状态标示。有两种状态是用于失败(失效)检测的:PFAIL标示和FAIL标示。 PFAIL意味着可能失败,这一个还没有得到确认的失败类型。FAIL意味着一个节点失败已经在一个固定的时间范围内被大多数主节点确认。

PFAIL 被确认为FAIL 需要满足下面这些条件:

如果上述条件为真,那么节点A将做如下两个动作:

当然关于Redis的失败检测,还有更细节的内容和更复杂的情况,上面没有提到,感兴趣的读者可以阅读Redis集群规范。需要注意的是,FAIL标识只是备节点提升为主节点的一个启动条件。

节点选举

备节点选举和提升是备节点来处理的,并且需要主节点进行选举。一个备节点选举发生在一个主节点被它的至少一个备节点标记为FAIL状态,并且这些备节点具备成为主节点的先决条件下。

一个备节点为了把自己提升为主节点,它需要发起一轮选举并且获胜。一个主节点的所有备节点都可以在这个主节点处于FAIL状态下发起选举,然而最后只有一个备节点能够赢得选举并提升自己成为主节点。

一个备节点发起一轮选举必须满足下面这些条件:

为了被选中,对于一个备节点来说,第一步就是增加自己的 currentEpoch计数,并且从主节点实例请求选票。

备节点通过广播一个FAILOVER_AUTH_REQUEST包给每个主节点来请求选票。然后,它等待一个最大 NODE_TIMEOUT*2(至少2秒)的时间接受回复。

一旦一个主节点投票给一个备节点,它主动回复一个FAILOVER_AUTH_ACK,它不能NODE_TIMEOUT * 2时间范围内再给这个备节点的竞争对手投票。这不是必须的安全性保障,但是对于阻止多个备节点同时选上非常有用。

一个备节点会丢弃发送选举请求后,小于当前 currentEpoch周期的所有AUTH_ACK回复。这确保了避免它错误地把上一轮选举记票记到当前周期。

一旦一个备节点得到大多数主节点的ACKs,它就赢得了选举。另外,如果这个大多数主节点在NODE_TIMEOUT*2(至少2秒)时间内没有达到,当前选举会被废弃,并且在NODE_TIMEOUT * 4(至少4秒)时间后,尝试开始一轮新的选举。

Redis集群方案对比

关于不同的集群方案对比,阿里云有一篇软文做了一些介绍,我认为:随着Redis3.2.8的发布,Redis的集群已经基本可以应用于生产环境了。

关于不同集群对于高级功能的支持,软文中有一个列表:

redis 4.0 阿里云redis codis
事务 支持相同slot 支持相同的slot 不支持
sub/pub 支持相同slot 支持 不支持
flushall 支持 支持 不支持
select 不支持 不支持 不支持
mset/mget 支持相同slot 支持 支持

以及性能对比:

不同集群性能对比

这篇软文中说:

在实际生产环境中,使用原生的redis cluster,客户端需要实现cluster protocol, 解析move, ask等指令并重定向节点,随意访问key可能需要两次访问操作才能完成,性能上并不能完全如单节点一样。

实际对于java来说,Jedis是支持redis cluster的,在后面一个主题“Spring下使用Redis”,我们会发现除非节点出现变动,几乎所有的客户端命令都可以一次完成,所以可以认为redis-cluster的性能就是实际应用时的性能,真是1core顶人家8core啊!

结语

关于Redis本身的内容我们就聊到这里,希望这篇文章能给大家起一个抛砖引玉的作用。鉴于作者水平有限,如果大家觉得什么地方不对,欢迎提出来,大家一起学习,一起进步。

最后附上Books在《人月神话》中的一句话,这句话来自于书中“贵族专制、民主政治和系统设计 ( Aristocracy,
Democracy, and System Design)”一节,是Redis作者Salvatore Sanfilippo的Google Group签名,希望对你从一个侧面理解Redis设计者的设计意图:

If a system is to have conceptual integrity, someone must control the concepts.(如果要得到系统概念上的完整性, 那么必须有人控制这些概念)——— 《人月神话》

上一篇下一篇

猜你喜欢

热点阅读