Redis实战学习

1. 为什么要用Redis?

  1. 开源
  2. 高性能
  3. 基于键值对的缓存与存储系统
  4. 提供多种键值数据类型来适应不同场景下的缓存与存储需求

1.1 Redis简介

1.1.1 存储结构

1.REmote DIctionary Server(远程字典服务器)的缩写,以字典结构存储数据,允许通过TCP协议读写字典中的内容。

2.Redis支持的键值数据类型如下:

  • 字符串类型(STRING)
  • 列表类型(LIST)
  • 散列类型(HASH)
  • 集合类型(SET)
  • 有序集合类型(ZSET)

3.传统MySQL关系数据库是二维表形式的存储结构,对复杂的场景还原十分复杂,不是很直观。Redis中数据的存储形式和其在程序中的存储方式非常相近。

1.1.2 内存存储于持久化

Redis数据库中的所有数据都存储在内存中。不过Redis提供了对持久化的支持,可以将内存中的数据异步写入到硬盘中,同时不影响继续提供服务。

Redis拥有两种不同形式的持久化方法:①时间点转储(point-in-timedump)②将所有修改了数据库的命令都写入一个只追加(append-only)文件里面

1.1.3 功能丰富

Redis可以用作缓存、队列系统等。它可以设置键的生存时间和限定数据占用的最大内存空间,在键到期后或数据达到空间限制后,可以淘汰不用的键。

1.2 准备

1.2.1 Redis命令

  • redis-cli -h 127.0.0.1 -p 6379 连接redis服务器
  • redis-cli PING 测试客户端与Redis的连接是否正常
  • redis-cli 进入交互模式,可以自由输入命令

1.2.2 多数据库

1.理解:一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。可以将每个字典理解成一个独立的数据库。

2.每个数据库对外都是以0开始的递增数字命名,默认支持16个数据库。客户端与Redis建立连接自动选择0号数据库,可以通过SELECT 1命令切换数据库

3.Redis不支持更改数据库的名字,开发者需要自己记录哪些数据库存储了哪些数据

4.Redis不支持为每个数据库设置不同的访问密码,一个客户端要么可以访问全部,要么都不能访问

5.多个数据库之间不是完全隔离的,使用FLUSHALL命令可以清空Redis实例中所有数据库的数据

6.不同应用应该使用不同的Redis实例存储数据,Redis非常轻量级,一个空Redis实例占用内存只有1MB左右

1.3 Redis存储结构

1.3.1 Redis中的字符串

Redis的字符串和其他编程语言或者其他键值存储提供的字符串非常相似。

字符串命令:

  • GET 获取存储在给定键中的值
  • SET 设置存储在给定键中的值
  • DEL 删除存储在给定键中的值(这个命令可以用于所有类型)

Redis中的键会区分大小写,命令不会区分大小写。

1.3.2 Redis中的列表

Redis对链表(linked-list)结构支持,一个列表结构可以有序地存储多个字符串。

Redis列表可执行的操作和很多编程语言里面的列表操作非常相似:

  • LPUSHRPUSH分别用于将元素推入列表的左端(left end)和右端(right end);
  • LPOPRPOP命令分别用于从列表的左端和右端弹出元素;
  • LINDEX命令用于获取列表在给定位置上的一个元素;
  • LRANGE命令用于获取列表在给定范围上的所有元素。

1.3.3 Redis的集合

Redis的集合和列表都可以存储多个字符串,它们之间的不同在于,列表可以存储多个相同的字符串,而集合则通过使用散列来保证自己存储的每个字符串都是各不相同的(这些散列表只有键,但没有与键相关的值)。

Redis的集合使用无序方式存储元素,所以用户不能像使用列表那样,将元素推入集合的某一端,或者从集合的某一端弹出元素。

  • SADD 将元素添加到集合
  • SREM从集合里面移除元素
  • SISMEMBER快速地检查一个元素是否已经存在于集合中
  • SMEMBERS获取集合包含的所有元素(如果集合包含的元素非常多,那么SMEMBERS命令的执行速度可能会很慢,所以谨慎使用这个命令)

1.3.4 Redis的散列

Redis的散列可以存储多个键值对之间的映射。散列在很多方面就像是一个微缩版的Redis。

  • HSET 在散列里面关联起给定的键值对
  • HGET 获取指定散列键的值
  • HGETALL 获取散列包含的所有键值对
  • HDEL 如果给定键存在于散列里面,那么移除这个键

1.3.5 Redis的有序集合

有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的;而有序集合的值则被称为分值(score),分值必须为浮点数

有序集合是Redis里面唯一一个既可以根据成员访问元素,又可以根据分值以及分值的排列顺序来访问元素的结构。

  • ZADD 将一个带有给定分值的成员添加到有序集合里面
  • ZRANGE 根据元素在有序集合排列中所处的位置,从有序集合里面获取多个元素
  • ZRANGEBYSCORE 获取有序集合在给定分值范围内的所有元素
  • ZREM 如果给定成员存在于有序集合,那么移除这个成员

1.4 Redis使用实例

1.4.1 对文章进行投票

背景:

现在要构建一个文章投票网站,如果一篇文章获得了至少200张支持票,那么网站认为这篇文章是一篇有趣的文章;假如这个网站每天发布1000篇文章,而其中的50篇符合网站对有趣文章的要求,那么网站要做的就是把这50篇文章放到文章列表前100位至少一天;该网站暂不支持投反对票的功能。

为了产生一个能随时间流逝而不断减少的评分,程序需要根据文章的发布时间和当前时间来计算文章的评分,具体计算方法为:将文章得到的支持票数量乘以一个常量,然后加上文章的发布时间,得出的结果就是文章的评分。

常量是432,这个常量是通过将一天的秒数(86400)除以文章展示一天所需的支持票数量(200)得出的:文章每获得一张支持票,程序就需要将文章的评分增加432分。

设计基本的存储结构:

1.文章:我们需要存储文章的标题、指向文章的网址、发布文章的用户、文章的发布时间、文章得到的投票数量等信息。最好的存储方式就是使用散列,一篇文章有这些基本属性,每个属性有对应的值;那么我们理应使用散列,一篇文章的标识作为散列的键,具体的属性及值作为散列里面的键值对。

2.为了提供文章排序功能,我们使用发布时间和文章评分作为排序标准;文章的唯一标识和文章的发布时间组成键值对,文章的唯一标识和文章评分组成键值对;既然如此,我们理应使用有序集合来存储,因为它自动帮我们进行了排序。

3.一篇文章被投票,应该是一对多关系;但是一个用户和一篇文章之间投票应该是一对一的关系;我们理应使用集合来存储文章投票的用户。

4.为了尽量节约内存,我们规定当一篇文章发不期满一周之后,用户将不能再对它进行投票,文章的评分将被固定下来,而记录文章已投票用户名单的集合也会被删除。

编码思路:

当用户尝试对一篇文章进行投票时,程序要使用ZSCORE命令检查记录文章发布时间的有序集合,判断文章的发布时间是否超过一周。如果文章仍然处于可以投票的时间范围之内,那么程序将使用SADD命令,尝试将用户添加到记录文章已投票用户名单的集合里面。如果添加操作执行成功的话,那么说明用户是第一次对这篇文章进行投票,程序将使用ZINCRBY命令为文章的评分增加432分(ZINCRBY命令用于对有序集合成员的分值执行自增操作),并使用HINCRBY命令对散列记录的文章投票数量进行更新(HINCRBY命令用于对散列存储的值执行自增操作)。

1.4.2 发布文章

发布一篇新文章首先需要创建一个新的文章ID,这项工作可以通过对一个计数器执行INCR命令完成。接着程序使用SADD将文章发布者的ID添加到记录文章已投票用户名单的集合里面,并使用EXPIRE命令为这个集合设置一个过期时间,让Redis在文章发布期满一周之后自动删除这个集合。之后,程序会使用HMSET命令来存储文章的相关信息,并执行两个ZADD命令,将文章的初始评分和发布时间分别添加到两个相应的有序集合里面。

1.4.3 获取文章

如何取出评分最高的文章以及如何取出最新发布的文章?程序先使用ZREVRANGE命令取出多个文章ID,然后对每个文章ID执行一次HGETALL命令来取出文章的详细信息,这个方法既可以用于取出评分最高的文章,又可以用于取出发布最新的文章。注意一点:因为有序集合会根据成员的分值从小到大地排列元素,所以使用ZREVRANGE命令,以"分值从大到小"的排列顺序取出文章ID才是正确的做法。

1.4.4 对文章进行分组

文章现在可以展示最新发布的和评分最高的,但是不具备分类功能。

我们可以使用一个集合来存储一类文章,利用Redis可以在集合和有序集合间执行操作的特性,就可以得到评分高的这类文章了。

ZINTERSTORE命令可以接受多个集合和多个有序集合作为输入,找出所有同事存在于集合和有序集合的成员,并以几种不同的方式合并这些成员的分值。

2. 使用Redis构建Web应用

从高层次的角度来看,Web应用就是通过HTTP协议对网页浏览器发送的请求进行响应的服务器或者服务。一个Web服务器对请求进行响应的典型步骤如下:

  1. 服务器对客户端发来的请求(request)进行解析
  2. 请求被转发给一个预定义的处理器(handler)
  3. 处理器可能会从数据库中取出数据
  4. 处理器根据取出的数据对模板(template)进行渲染(render)
  5. 处理器向客户端返回渲染后的内容作为对请求的响应(response)

2.1 登录和cookie缓存

每当登录互联网服务的时候,这些服务都会使用cookie来记录我们的身份。对于用来登录的cookie,有两种常见的方法可以将登录信息存储在cookie里面:一种事签名(signed)cookie,另一种是令牌(token)cookie。

签名cookie:通常会存储用户名,可能还有用户ID、用户最后一次成功登录的时间,以及网站觉得有用的其他任何信息。除了用户的相关信息之外,签名cookie还包含一个签名,服务器可以使用这个签名来验证浏览器发送的信息是否未经改动(比如将cookie中登录的用户名改为另一个用户)。

令牌cookie:会在cookie里面存储一串随机字节作为令牌,服务器可以根据令牌在数据库中查找令牌的拥有者。随着时间推移,旧令牌会被新令牌取代。

2.1.1 存储登录cookie令牌

使用散列存储登录cookie令牌与已登录用户之间的映射。要检查一个用户是否已经登录,需要根据给定的令牌来查找与之对应的用户。

  1. 检查用户是否已经登录,需要根据给定的令牌来查找与之对应的用户,并在用户已经登录的情况下,返回该用户的ID
  2. 用户每次浏览页面的时候,程序都会对用户存储在登录散列里面的信息进行更新,并将用户的令牌和当前时间戳添加到记录最近登录用户的有序集合里面;如果用户正在浏览的是一个商品页面,那么程序还会将这个商品添加到记录这个用户最近浏览过的商品的有序集合里面,并在被记录商品的数量超过25个时,对这个有序集合进行修剪。
  3. 因为存储会话数据所需的内存会随着时间的推移而不断增加,所以我们需要定期清理旧的会话数据。为了限制会话数据的数量,我们决定只保留最新的1000万个会话。清理旧会话的程序由一个循环构成,这个循环每次执行的时候,都会检查存储最近登录令牌的有序集合的大小,如果有序集合的大小超过了限制,那么程序就会从有序集合里面移除最多100个最旧的令牌,并从记录用户登录信息的散列里面,移除被删除令牌对应的用户的信息,并对存储了这些用户最近浏览商品记录的有序集合进行清理。与此相反,如果令牌的数量未超过限制,那么程序会先休眠1秒,之后再重新进行检查。

2.1.2 使用Redis实现购物车

使用cookie实现购物车——也就是将整个购物车都存储到cookie里面的做法非常常见,这种做法的一大优点是无须对数据库进行写入就可以实现购物车功能,而缺点则是程序需要重新解析和验证(validate)cookie,确保cookie的格式正确,并且包含的商品都是真正可购买的商品。cookie购物车还有一个缺点:因为浏览器每次发送请求都会连cookie一起发送,所以如果购物车cookie的体积比较大,那么请求发送和处理的速度可能会有所降低。

购物车的定义非常简单:每个用户的购物车都是一个散列,这个散列存储了商品ID与商品订购数量之间的映射。对商品数量进行验证的工作有Web应用程序负责,我们要做的则是在商品的订购数量出现变化时,对购物车进行更新:如果用户订购某件商品的数量大于0,那么程序会将这件商品的ID以及用户订购该商品的数量添加到散列里面,如果用户购买的商品已经存在于散列里面,那么新的订购数量会覆盖已有的订购数量;相反地,如果用户订购某件商品的数量不大于0,那么程序将从散列里面移除该条目。

2.1.3 网页缓存

网站所处理的90%页面每天最多只会改变一次,这些页面的内容实际上并不需要动态地生成,而我们的工作就是想办法不再生成这些页面。减少网站在动态生成内容上面所花的时间,可以降低网站处理相同负载所需的服务器数量,并让网站的速度变得更快。

我们使用Redis缓存函数:对于一个不能被缓存的请求,函数将直接生成并返回页面;而对于可以被缓存的请求,函数首先会尝试从缓存里面取出并返回被缓存的页面,如果缓存页面不存在,那么函数会生成页面并将其缓存在Redis里面5分钟,最后再将页面返回给函数调用者。

这个缓存函数对于减少页面载入时间和降低数据库负载的作用会更加显著。

2.1.4 数据行缓存

商品页面通常只会从数据库里面载入一两行数据,包括已登录用户的用户信息和商品本身的信息。即使是那些无法被缓存起来的页面——比如用户账号页面、记录用户以往购买商品的页面等等,程序也可以通过缓存页面载入时所需的数据库行来减少载入页面所需要的时间。

假设网站为了清空旧库存和吸引客户消费,决定开始新一轮的促销活动:这个活动每天都会推出一些特价商品供用户抢购,所有特价商品的数量都是限定的,卖完即止。

  1. 在这种情况下,网站是不能对整个促销页面进行缓存的,因为这可能导致用户看到错误的特价商品剩余数量
  2. 但是每次载入页面都从数据库里面取出特价商品的剩余数量的话,又会给数据库带来巨大的压力,并导致我们需要花费额外的成本来扩展数据库
  3. 为了应对促销活动带来的大量负载,我们需要对数据行进行缓存
  4. 编写一个持续运行的守护进程函数,让这个函数将指定的数据行缓存到Redis里面,并不定期地对这些缓存进行更新。
  5. 为了让缓存函数定期地缓存数据行,程序首先需要将行ID和给定延迟值添加到延迟有序集合里面,然后再将行ID和当前时间的时间戳添加到调度有序集合里面。
  6. 实际执行缓存操作的函数需要用到数据行的延迟值,如果某个数据行的延迟值不存在,那么程序将取消对这个数据行的调度。如果我们想要移除某个数据行已有的缓存,并且让缓存函数不再缓存那个数据行,那么只需要把那个数据行的延迟值设置为小于或等于0就可以了。
  7. 负责缓存数据行的函数会尝试读取调度有序集合的第一个元素以及该元素的分值,如果调度有序集合没有包含任何元素,或者分值存储的时间戳所指定的时间尚未来临,那么函数会先休眠50毫秒,然后再重新进行检查。
  8. 当缓存程序发现一个需要立即进行更新的数据行时,缓存函数会检查这个数据行的延迟值:如果数据行的延迟值小于或等于0,那么缓存函数会从延迟有序集合和调度有序集合里面移除这个数据行的ID,并从缓存里面删除这个数据行已有的缓存,然后再重新进行检查;对于延迟值大于0的数据行来说,缓存函数会从数据库里面取出这些行,将他们编码为JSON格式并存储到Redis里面,然后更新这些行的调度时间。

通过组合使用调度函数和持续运行缓存函数,我们实现了一种重复进行调度的自动缓存机制,并且可以随心所欲地控制数据行缓存的更新频率。如果数据记录的是特价促销商品的剩余数量,并且参与促销活动的用户非常多的话,那么我们最好每隔几秒更新一次数据行缓存;另一方面,如果数据并不经常改变,或者商品缺货是可以接受的,那么我们可以每分钟更新一次缓存。

2.1.5 网页分析

网站总共包含100,000件商品,而冒然地缓存所有商品页面将耗尽整个网站的全部内存,所以我们决定只对其中10,000件商品的页面进行缓存。新建一个viewed的有序集合,其中放用户浏览的商品及其被访问次数。conn.zincrby('viewd:',item,-1)表示对商品每访问一次,就将它的分值-1,以达到访问最多的排在有序集合最先的位置。除了缓存最常被浏览的商品之外,程序还需要发现那些变得越来越流行的新商品,并在合适的时候缓存它们。

为了让商品浏览次数排行榜能保持最新,我们需要定期修剪有序集合的长度并调整已有元素的分值,从而使得新流行的商品也可以在排行榜中占据一席之地。

然后根据排行榜进行判断页面是否需要被缓存。

3. Redis命令

3.1 字符串

Redis的字符串就是一个由字节组成的序列,字符串可以存储以下3中类型的值:

  • 字节串(byte string)
  • 整数
  • 浮点数

Redis字符串执行自增和自减操作的命令:

当用户将一个值存储到Redis字符串里面的时候,如果这个值可以被解释为十进制整数或者浮点数,那么Redis会察觉到这一点,并允许用户对这个字符串执行各种INCR*和DECR*操作;如果用户对一个不存在的键或者一个保存了空串的键执行自增或者自减操作,那么Redis在执行操作时会将这个键的值当做是0来处理。如果用户尝试对一个值无法被解释为整数或者浮点数的字符串键执行自增或者自减操作,那么Redis将向用户返回一个错误。

Redis还拥有对字节串的其中一部分内容进行读取或者写入的操作,下表展示了用来处理字符串子串和二进制位的命令。

3.2 列表

Redis的列表允许用户从序列的两端推入或弹出元素,获取列表元素,以及执行各种常见的列表操作。除此之外,列表还可以用来存储任务信息、最近浏览过的文章或者常用联系人信息。

有几个列表命令可以将元素从一个列表移动到另一个列表,或者阻塞(block)执行命令的客户端直到有其他客户端给列表添加元素为止。

3.3 集合

Redis的集合以无序的方式来存储多个各不相同的元素,用户可以快速地对集合执行添加元素操作、移除元素操作以及检查一个元素是否存在于集合里。

下面对最常用的集合命令进行介绍,包括插入命令、移除命令、将元素从一个集合移动到另一个集合的命令,以及对多个集合执行交集运算、并集运算和差集运算的命令。

集合真正厉害的地方在于组合和关联多个集合,其命令如图:

3.4 散列

Redis的散列可以让用户将多个键值对存储到一个Redis键里面。从功能上来说,Redis为散列值提供了一些与字符串值相同的特性,使得散列非常适用于将一些相关的数据存储在一起。我们可以把这种数据聚集看作是关系数据库中的行。

散列的常用命令:

散列的其他几个批处理操作命令,以及一些和字符串操作类似的散列命令。

尽管有HGETALL存在,但是HKEYSHVALUES也是非常有用的:如果散列中包含的值非常大,那么用户可以先使用HKEYS取出散列中包含的所有键,然后再使用HGET一个接一个地取出键的值,从而避免因为一次获取多个大体积的值而导致服务器阻塞。

3.5 有序集合

和散列存储着键与值之间的映射类似,有序集合也储存着成员与分值之间的映射,并且提供了分值处理命令,以及根据分值大小有序地获取(fetch)或扫描(scan)成员和分值的命令。

下图展示了一部分常用的有序集合命令:

一些有用的有序集合命令:

3.6 发布与订阅

一般来说,发布与订阅(又称pub/sub)的特点是订阅者(listener)负责订阅频道(channel),发送者(publisher)负责向频道发送二进制字符串消息(binary string message)。每当有消息被发送至给定频道时,频道的所有订阅者都会收到消息。

下图是Redis提供的5个发布与订阅命令。

虽然Redis的发布和订阅模式非常有用,但是它的使用频率很低;

因为

  1. 和Redis系统的稳定性有关,对于旧版Redis来说,如果一个客户端订阅了某个或某些频道,但他读取消息的速度却不够快的话,那么不断积压的消息就会使得Redis输出缓冲区的体积变得越来越大,这可能导致Rdis的速度变慢,甚至直接崩溃。也可能导致Redis被操作系统强制杀死,甚至导致操作系统本身不可用。新版的Redis不会出现这种问题,因为它会自动断开不符合client-output-buffer-limit pubsub配置选项要求的订阅客户端。
  2. 和数据传输的可靠性有关。任何网络系统在执行操作时都可能会遇上断线情况,而断线产生的连接错误通常会使得网络连接两端中的其中一端进行重新连接。但是,如果客户端在执行订阅操作的过程中断线,那么客户端将丢失在断线期间发送的所有消息,因此依靠频道来接收消息的用户可能会对Redis提供的PUBLISH命令和SUBSCRIBE命令的语义感到失望。

3.7 其他命令

下面将要介绍的命令可以用于处理多种类型的数据:首先要介绍的是可以同时处理字符串、集合、列表和散列的SORT命令;之后介绍用于实现基本事务特性的MULTI命令和EXEC命令,这两个命令可以让用户将多个命令当做一个命令来执行;最后要介绍的是几个不同的自动过期命令,它们可以自动删除无用数据。

3.7.1 排序

负责执行排序操作的SORT命令可以根据字符串、列表、集合、有序集合、散列这5种键里面存储着的数据,对列表、集合以及有序集合进行排序。

3.7.2 基本的Redis事务

有时候为了同时处理多个结构,我们需要向Redis发送多个命令。尽管Redis有几个可以在两个键之间复制或者移动元素的命令,但却没有那种可以在两个不同类型之间移动元素的命令。(除了ZUNIONSTORE命令将元素从一个集合复制到一个有序集合)。为了对相同或者不同类型的多个键值执行操作,Redis有5个命令可以让用户在不被打断的情况下对多个键执行操作,它们是:WATCH MULTI EXEC UNWATCH DISCARD

Redis的基本事务需要用到MULTI命令和EXEC命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。和关系数据库那种可以在执行过程中进行回滚的事务不同,在Reids里面,被MULTI命令和EXEC命令包围的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。当一个事务执行完毕之后,Redis才会处理其他客户端的命令。

要在Redis里面执行事务,我们首先要执行MULTI命令,然后输入那些我们想要在事务里面执行的命令,最后再执行EXEC命令。当Redis从一个客户端那里接收到MULTI命令时,Redis会将这个客户端之后发送的所有命令都放入到一个队列里面,直到这个客户端发送EXEC命令为止,然后Redis就会在不被打断的情况下,一个接一个地执行存储在队列里面的命令。

3.7.3 键的过期时间

在使用Redis存储数据的时候,有些数据可能在某个时间点之后就不再有用了,用户使用DEL命令显示地删除这些无用数据,也可以通过Redis的过期时间特性来让一个键在给定的时限之后自动被删除。

虽然过期时间特性对于清理缓存数据非常有用,但是对于列表、集合、散列和有序集合这样的容器来说,键过期命令只能为整个键设置过期时间,而没办法为键里面的单个元素设置过期时间。

下面列出了为键设置过期时间的命令,以及查看键的过期时间的命令。

4 数据安全与性能保障

本章将展示维护数据安全以及应对系统故障的方法。首先介绍Redis的各个持久化选项,这些选项可以让用户把自己的数据存储到硬盘上面。然后通过Redis的复制特性,把不断更新的数据副本存储到附加的机器上面,从而提升系统的性能和数据的可靠性。

主要弄懂更多的Redis运作原理,从而学会如何在首先保证数据正确的前提下,加快数据操作的执行速度。

4.1 持久化选项

Redis提供了两种持久化数据的方式。一种叫做快照(snapshotting),它可以将存在于某一时刻的所有数据都写入硬盘里面。另一种叫只追加文件(append-only file,AOF),它会在执行命令时,将被执行的写命令复制到硬盘里面。这两种持久化方法既可以同时使用,又可以单独使用,在某些情况下甚至可以两种方法都不使用,具体选择哪种持久化方法需要根据用户的数据及应用来决定。

4.1.1 快照持久化

Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。根据配置,快照将被写入dbfilename选项指定的文件里面,并存储在dir选项指定的路径上面。如果在新的快照文件创建完毕之前,Redis、系统或者硬件这三者中的任意一个崩溃了,那么Redis将丢失最近一次创建快照之后写入的所有数据。

所以在只使用持久化来保存数据时,一定要记住:如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适合那些即使丢失一部分数据也不会造成问题的应用程序,而不能接受这种数据损失的应用程序则考虑使用AOF持久化。

4.1.2 AOF持久化

简单来说,AOF持久化会将被执行的命令写到AOF文件末尾,以此来记录数据发生的变化。因此,Redis只要从头到尾重新执行一次AOF文件包含的所有写命令,就可以恢复AOF文件所记录的数据集。

appendfsync配置选项对AOF文件的同步频率的影响:

虽然AOF持久化非常灵活地提供了多种不同的选项来满足不同应用程序对数据安全的不同要求,但AOF持久化也有缺陷——那就是AOF文件的体积大小。

为了解决AOF文件体积不断增大的问题,用户可以向Redis发送BGREWRITEAOF命令,这个命令会通过移除AOF文件中的冗余命令来重写AOF文件,使得AOF文件的体积变得尽可能小。BGREWRITEAOF的工作原理和BGSAVE创建快照的工作原理非常相似:Redis会创建一个子进程,然后由子进程负责对AOF文件进行重写。因为AOF文件重写也需要用到子进程,所以快照持久化因为创建子进程而导致的性能问题和内存占用问题,在AOF持久化中也同样存在。更糟糕的是,如果不加以控制的话,AOF文件的体积可能会比快照文件的体积大好几倍,在进行AOF重写并删除旧AOF文件的时候,删除一个体积达到数十GB大的旧AOF文件可能会导致操作系统挂起数秒。

AOF持久化也可以设置auto-aof-rewrite-percentage选项和auto-aof-rewrite-min-size选项来自动执行BGREWRITEAOF

无论是使用AOF持久化还是快照持久化,将数据持久化到硬盘上都是非常有必要的,除了进行持久化之外,用户还必须对持久化所得的文件进行备份,这样才能尽量避免数据丢失事故发生。

4.2 复制

对于扩展平台以适应高负载经验的工程师和管理员来说,复制(replication)是不可或缺的。复制可以让其他服务器拥有一个不断地更新的数据副本,从而使得拥有数据副本的服务器可以用于处理客户端发送的读请求。

在需要扩展读请求的时候,或者在需要写入临时数据的时候,用户可以通过设置额外的Redis从服务器来保存数据集的副本。在收到主服务器发送的数据初始副本之后,客户端每次向主服务器进行写入时,从服务器都会实时地得到更新。

4.2.1 Redis复制的启动过程

下面是当从服务器连接主服务器时,主从服务器执行的所有操作。

Redis在复制进行期间也会尽可能地处理接收到的命令请求,但是,如果主从服务器之间的网络带宽不足,或者主服务器没有足够的内存来创建子进程和创建记录写命令的缓冲区,那么Redis处理命令请求的效率就会受到影响。因此,尽管这并不是必须的,但在实际中最好还是让主服务器只使用50%-60%的内存,留下30%-45%的内存用于执行BGSAVE命令和创建记录写命令的缓冲区。

设置从服务器的步骤非常简单,用户既可以通过配置选项SLAVEOF host port来将一个Redis服务器设置为从服务器,又可以通过向运行中的Redis服务器发送SLAVEOF命令来将其设置为从服务器。

如果用户使用的是SLAVEOF配置选项,那么Redis在启动时首先会载入当前可用的任何快照文件或者AOF文件,然后连接主服务器并执行表4-2中的复制过程。如果用户使用的是SLAVEOF命令,那么Redis会立即尝试连接主服务器,并在连接成功之后,开始表4-2中的复制过程。

从服务器在进行同步时,会清空自己的所有数据:从服务器在与主服务器进行初始连接时,数据库中原有的所有数据都将丢失,并被替换成主服务器发来的数据。

Redis不支持主主复制

在大部分情况下,Redis都会尽可能地减少复制所需要的工作,然而,如果从服务器连接主服务器的时间并不凑巧,那么主服务器就需要多做一些额外的工作。另一方面,当多个从服务器同时连接主服务器的时候,同步多个从服务器所占用的带宽可能使得其他命令请求难以传递给主服务器,与主服务器位于同一网络中的其他硬件的网速可能也会因此而降低。

4.2.2 主从链

Redis的主服务器和从服务器并没有特别不同的地方,所以从服务器也可以拥有自己的从服务器,并由此形成主从链。(master/slave chaining)

从服务器对从服务器进行复制在操作上和从服务器对主服务器进行复制的唯一区别在于,如果从服务器x拥有从服务器y,那么当服务器x在执行表4-2中步骤4时,它将断开与从服务器y的连接,导致从服务器y需要重新连接并重新同步。

4.2.3 检验硬盘写入

判断数据是否已经被保存到硬盘里,用户可以检查INFO命令的输出结果中aof_pending_bio_fsync属性的值是否为0,如果是的话,那么久表示服务器已经将已知的所有数据都保存到硬盘里了。

4.3 处理系统故障

如果我们决定要将Redis用作应用程序唯一的数据存储手段,那么就必须确保Redis不会丢失任何数据。跟提供了ACID保证的传统关系型数据库不同,在使用Redis为后端构建应用程序的时候,用户需要多做一些工作才能保证数据的一致性。

Redis是一个软件,它运行在硬件之上,即使软件和硬件都设计得完美无瑕,也有可能会出现停电、发电机因为燃料耗尽而无法发电或者备用电池电量消尽等情况。

4.3.1 验证快照文件和AOF文件

无论是快照持久化还是AOF持久化,都提供了在遇到系统故障时进行数据恢复的工具。Redis提供了两个命令行程序redis-check-aofredis-check-dump,他们可以在系统故障发生之后,检查AOF文件和快照文件的状态,并在有需要的情况下对文件进行修复。

程序修复AOF文件的方法非常简单:它会扫描给定的AOF文件,寻找不正确或者不完整的命令,当发现第一个出错命令的时候,程序会删除出错的命令以及位于出错命令之后的所有命令,只保留那些出错命令之前的正确命令。在大多数情况下,被删除的都是AOF文件末尾的不完整的写命令。

遗憾的是,目前并没有办法可以修复出错的快照文件。尽管发现快照文件首个出现错误的地方是有可能的,但是因为快照文件本身经过了压缩,而出现在快照文件中间的错误有可能会导致快照文件的剩余部分无法被读取。因此,用户最好为重要的快照文件保留多个备份,并在进行数据恢复时,通过计算快照文件的SHA1散列值和SHA256散列值来对内容进行验证。

4.3.2 更换故障主服务器

在拥有一个主服务器和一个从服务器的情况下,更换主服务器的具体步骤。

假设A、B两台机器都运行着Redis,其中机器A的Redis为主服务器,而机器B的Redis为从服务器。不巧的是,机器A刚刚因为某个暂时无法修复的故障而断开了网络连接,因此用户决定将同样安装了Redis的机器C作为新的主服务器。更换服务器的计划非常简单:

方案1.首先向机器B发送一个SAVE命令,让他创建一个新的快照文件,接着将这个快照文件发送给机器C,并在机器C上面启动Redis。最后,让机器B成为机器C的从服务器。

下面是替换主节点的命令:

其中注意在机器B上运行的SAVE命令和将机器B设置为机器C的从服务器的SLAVEOF命令。

方案2.将从服务器升级为主服务器,并为升级后的主服务器创建从服务器。

4.4 Redis事务

为了保证数据的正确性,我们必须认识到这一点:在多个客户端同时处理相同的数据时,不谨慎的操作很容易会导致数据出错。

Redis的事务和传统关系数据库的事务并不相同。在关系数据库中,用户首先向数据库服务发送BEGIN,然后执行各个相互一致(consistent)的写操作和读操作,最后,用户可以选择发送COMMIT来确认之前所做的修改,或者发送ROLLBACK来放弃那些修改。

在Redis里面也有简单的方法可以处理一连串相互一致的读操作和写操作。Redis的事务以特殊命令MULTI为开始,之后跟着用户传入的多个命令,最后以EXEC为结束。但是由于这种简单的事务在EXEC命令被调用之前不会执行任何实际操作,所以用户没办法根据读取到的数据来做决定。这个问题看上去似乎无足轻重,但实际上以一致的形式读取数据将导致某一类型的问题变得难以解决,除此之外,因为在多个事务同时处理同一个对象时通常需要用到二阶提交,所以如果事务不能一致的形式读取数据,那么二阶提交将无法实现,从而导致一些原本可以成功执行的事务沦落至执行失败的地步。

4.4.1 模拟设计实现商品买卖市场

设计用户和背包的数据结构如下:

商品买卖市场的需求非常简单:一个用户(卖家)可以将自己的商品按照给定的价格放到市场上进行销售,当另一个用户(买家)购买这个商品时,卖家就会收到钱。

为了将被销售商品的全部信息都存储到市场里面,我们将商品的ID和卖家的ID拼接起来,并将拼接的结果用作成员存储到市场有序集合里面,而商品的售价则用作成员的分值。

通过将所有数据都包含在一起,我们极大地简化了实现商品买卖市场所需的数据结构,并且因为市场里面的所有商品都按照价格排序,所以针对商品的分页功能和查找功能都可以很容易地实现。

4.4.2 将商品放到市场上销售

为了将商品放到市场上进行销售,程序除了使用MULTI命令和EXEC命令之外,还需要配合使用WATCH命令,有时候甚至还会用到UNWATCHDISCARD命令。在用户使用WATCH命令对键进行监视之后,直到用户执行EXEC命令的这段时间里面,如果有其他客户端抢先对任何被监视的键进行了替换、更新或删除等操作,那么当用户尝试执行EXEC命令的时候,事务将失败并返回一个错误。通过使用WATCHMULTI/EXECUNWATCH/DISCARD等命令,程序可以在执行某些重要操作的时候,通过确保自己正在使用的数据没有发生变化来避免数据出错。

思路:在将一件商品放到市场上进行销售的时候,程序需要将被销售的商品添加到记录市场正在销售商品的有序集合里面,并且在添加操作执行的过程中,监视卖家的包裹以确保被销售的商品的确存在于卖家的包裹当中。

因为没有一个Redis命令可以在移除集合元素的同时,将被移除的元素改名并添加到有序集合里面,所以这里使用了ZADDSREM两个命令来实现这一操作。

4.4.3 购买商品

购买一件商品的具体方法:程序首先使用WATCH对市场以及买家的个人信息进行监视,然后获取买家拥有的钱数以及商品的售价,并检查买家是否有足够的钱来购买该商品。如果买家没有足够的钱,那么程序会取消事务;相反地,如果买家的钱足够,那么程序首先会将买家支付的钱转移给卖家,然后将售出的商品移动至买家的包裹,并将该商品从市场中移除。当买家的个人信息或者商品买卖市场出现变化而导致WatchError异常出现时,程序将进行重试,其中最大重试时间为10秒。

在执行商品购买操作的时候,程序除了需要花费大量时间来准备相关数据之外,还需要对商品买卖市场以及买家的个人信息进行监视:监视商品买卖市场是为了确保买家想要购买的商品仍然有售(或者在商品已经被其他人买走时进行提示),而监视买家的个人信息则是为了验证买家是否有足够的钱来购买自己想要的商品。当程序确认商品仍然存在并且买家有足够钱的时候,程序会将被购买的商品移动到买家的包裹里面,并将买家支付的钱转移给卖家。

当有多个客户端同时对相同的数据进行操作时,正确地使用事务可以有效防止数据错误发生。

4.5 非事务型流水线

MULTIEXEC包裹的命令在执行时不会被其他客户端打扰。而使用事务的其中一个好处就是底层的客户端会通过使用流水线来提高事务执行时的性能。下面介绍如何在不使用事务的情况下,通过使用流水线来提升命令的执行性能。

使用非事务型流水线可以获得相似批处理命令的性能提升,并且可以让用户同时执行多个不同的命令。

在需要执行大量命令的情况下,即使命令实际上并不需要放在事务里面执行,但是为了通过一次发送所有命令来减少通信次数并降低延迟值,用户也可能会将命令包裹在MULTIEXEC里面执行。遗憾的是,MULTIEXEC并不是免费的——他们也消耗资源,并且可能会导致其他重要的命令被延迟执行。不过好消息是,我们实际上可以在不使用MULTIEXEC的情况下,获得流水线带来的所有好处。

通过将标准的Redis连接替换成流水线连接,程序可以将通信往返的次数减少至原来的1/2到1/5。

高延迟网络使用流水线时的速度要比不使用流水线时的速度快5倍,低延迟网络使用流水线也可以带来接近4倍的速度提升,而本地网络的测试结果实际上已经达到了单核环境下使用Redis协议发送和接收命令序列的性能极限。

4.6 关于性能方面的注意事项

对于已经存在的应用程序,我们应该如何判断这个程序能否被优化?又该如何对它进行优化?

要对Redis的性能进行优化,用户首先需要弄清除各种类型的Redis命令到底能跑多快,可以通过调用Redis附带的性能测试程序redis-benchmark来得知。redis-benchmark的运行结果展示了一些常用Redis命令在1秒内可以执行的次数。

下面是我的虚拟机中docker容器安装的redis镜像,运行redis-benchmark命令。

redis-benchmark的运行结果展示了一些常用Redis命令在1秒内可以执行的次数。如果用户在不给定任何参数的情况下运行redis-benchmark,那么redis-benchmark将使用50个客户端来进行测试。

在考察redis-benchmark的输出结果时,切记不要将输出结果看作是应用程序的实际性能,这是因为redis-benchmark不会处理执行命令所获得的的命令回复,所以它节约了大量用于对命令回复进行语法分析的时间。在一般情况下,对于只使用单个客户端的redis-benchmark来说,根据被调用命令的复杂度,一个不使用流水线的Python客户端的性能大概只有redis-benchmark所示性能的50%-60%.

上图中是出现性能问题以及问题的解决方法。

大部分Redis客户端库都提供了某种级别的内置连接池(connection pool)。以Python的Redis客户端为例,对于每个Redis服务器,用户只需要创建一个redis.Redis()对象,该对象就会按需创建连接、重用已有的连接并关闭超时的连接,并且Python客户端的连接池还可以安全地应用于多线程环境和多进程环境。

5 使用Redis构建支持程序

使用日志和计数器来收集系统当前的状态信息、挖掘正在使用系统的顾客的相关信息、将Redis用作记录配置信息的字典。

5.1 使用Redis来记录日志

在构建应用程序和服务的过程中,对正在运行的系统的相关信息的挖掘能力将变得越来越重要:无论是通过挖掘信息来诊断系统问题,还是发现系统中潜在的问题,甚至是挖掘与用户有关的信息——这些都需要用到日志。

在Linux和Unix中,有两种常见的记录日志的方法。

第一种:将日志记录到文件里面,然后随着时间流逝不断地将一个又一个日志行添加到文件里面,并在一段时间之后创建新的日志文件。包括Redis在内的很多软件都使用这种方法来记录日志。但这种记录日志的方式有时候可能会遇上麻烦:因为每个不同服务都会创建不同的日志,而这些服务轮换日志的机制也各不相同,并且也缺少一种能够方便地聚合所有日志并对其进行处理的常用方法。

第二种:syslog服务是第二种常用的日志记录方法,这个服务运行在几乎所有Linux服务器和Unix服务器的514号TCP端口和UDP端口上面。syslog接受其他程序发来的日志消息,并将这些消息有(route)至存储在硬盘上的各个日志文件里面,除此之外,syslog还负责旧日志的轮换和删除工作。通过配置,syslog甚至可以将日志消息转发给其他服务来做进一步的处理。因为对指定日志的轮换和删除工作都可以交给syslog来完成,所以使用syslog服务比直接将日志写入文件要方便得多。

5.1.1 最新日志

将最新出现的日志消息以列表的形式存储到Redis里面:程序使用LPUSH命令将日志消息推入一个列表里面。之后,如果要查看已有日志消息,那么可以使用LRANGE命令来取出列表中的消息。

5.2 计数器和统计数据

通过在一段时间里面持续记录网站点击量和数据库的读写次数,我们可以注意到流量的骤增和渐增情况,预测何时需要对服务器进行升级,从而防止系统因为负荷超载而下线。

那么如何使用Redis来实现时间序列计数器,以及如何使用这些计数器来记录和监测应用程序的行为。

5.2.1 将计数器存储到Redis里面

为实现网站点击量的计数器,这个计数器会以不同的时间精度(如1秒、5秒)存储最新的数据样本。

实现计数器首先要考虑的就是如何存储计数器信息。

1.为了对计数器进行更新,我们需要存储实际的计数器信息。假如现在有一个计数器,它的精度有很多种,那么它的每一种精度存储使用散列结构,散列的键是时间片的开始时间,键就是该时间片内的点击量。每当有用户点击网页,就会触发该计数器的更新操作,它会更新每一个精度下的点击量。更新计数器的函数如下:

2.为了能清理计数器包含的旧数据,那么我们需要知道有哪些计数器在进行计数。我们需要一个有序序列,这个序列不能包含任何一个重复元素,并且能够让我们一个接一个地遍历序列中包含的所有元素。那么我们就使用有序集合来实现有序序列,将所有成员的分值设为0,Redis在尝试按分值对有序集合进行排序的时候,就会改为按成员名进行排序。

3.清理旧计数器只需要遍历有序集合并删除其中的旧计数器就可以了。

5.2.2 使用Redis存储统计数据

程序将值存储在有序集合里面是为了对存储着统计信息的有序集合和其他有序集合进行并集计算,并通过MINMAX这两个聚合函数来筛选相交的元素。

5.3 查找IP所属城市以及国家

使用Redis而不是传统的关系数据库来实现IP所属地查找功能,是因为Redis实现的IP所属地查找程序在运行速度上更具优势。

5.3.1 载入位置表格

https://dev.maxmind.com/geoip/geoip2/geolite2/ 这个网址提供了可免费使用的IP所属城市数据库作为测试数据。

实现IP所属地查找程序需要两张表,第一张需要根据输入的IP地址来查找IP所属城市的ID,而第二张需要根据输入城市的ID来查找ID对应城市的实际信息。

5.4 服务的发现与配置

对于一个Redis服务器、一个数据库服务器以及一个Web服务器来说,存储它们的配置信息不难。但是如果我们有好几个从服务器的Redis主服务器,或者不同的应用程序设置了不同的Redis服务器,甚至为数据库也设置了主服务器和从服务器的话,那么存储这些服务器的配置信息就有难度了。

5.4.1 使用Redis存储配置信息

通常情况下,使用一个标志来表示Web服务器是否正在维护;那么即使只更新配置中的一个标志,也会导致更新后的配置文件被强制推送到所有Web服务器,服务器可能需要重启。

那么我们可以尝试直接把配置写入Redis里面,只要将配置信息存储在Redis里面,并编写应用程序来获取这些信息,我们就不用再编写工具来向服务器推送配置信息了,服务器和服务也不用再通过重新载入配置文件的方式来更新配置信息了。

posted @ 2020-01-02 09:47  春刀c  阅读(1009)  评论(0编辑  收藏  举报