小土刀

【Redis 实战】读书笔记

Redis 是一个速度非常快的非关系型数据库,可以将存储在内存的键值对数据,可以使用复制特性来扩展性能,还可以使用客户端分片来扩展写性能。这本书从消灭瓶颈、简化代码、收集数据、分发数据、构建实用程序出发,并最终帮助读者更轻松地完成构建软件的任务。


Redis 是一个速度非常快的非关系型数据库,可以将存储在内存的键值对数据,可以使用复制特性来扩展性能,还可以使用客户端分片来扩展写性能。

与 memcached 的不同:memcached 只能存储普通的字符串键,而 Redis 因为有不同的数据结构,能解决更广泛的问题,并且既可以用作主数据库使用,又可以作为其他存储系统的辅助数据库使用

Redis 每种数据类型都有自己的专属命令,另外还有批量操作(bulk operation)和不完全(partial)的事务支持。发布与订阅、主从复制、持久化、脚本

使用 Redis 而不是关系数据库或者其他硬盘存储数据库,可以避免写入不必要的临时数据,也免去了对临时数据进行扫描或者删除的麻烦,并最终改善程序的性能。

消灭瓶颈、简化代码、收集数据、分发数据、构建实用程序,并最终帮助读者更轻松地完成构建软件的任务

数据结构

类型 - 结构存储的值 - 结构的读写能力

STRING - 可以是字符串、整数或者浮点数 - 对整个字符串或者字符串的其中一部分执行操作;对整数和浮点数执行自增或者自减操作

LIST - 一个链表,链表上的每个借点都包含了一个字符串 - 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值查找或者移除元素

SET - 包含字符串的无序收集器(unordered collection),并且被包含的每个字符串都是独一无二、各不相同的 - 添加、获取、移除单个元素;检查一个元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素

HASH - 包含键值对的无序散列表 - 添加、获取、移除单个键值对;获取所有键值对

ZSET - 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 - 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素

文章投票

根据文章的发布时间、投票数量计算出一个评分,然后按照这个评分来决定如何排序和展示文章。

一些规则

  • 一篇文章如果获得了至少 200 张支持票,就认为是有趣的文章
  • 有趣的文章需要放到前 100 位至少一天
  • 评分是随时间流逝不断减少的
  • 使用 Unix 时间
  • 计算评分需要乘以一个常量,这个常量是 432,因为一天的秒数 86400 除以所需要的支持票 200 就是 432
  • 每篇文章会使用一个散列来存储文章的标题、指向文章的网址、发布文章的用户、文章的发布时间、文章得到的投票数量等信息

命名空间的构建一般可以用类别和编号来构成,可以用 :, ., /, | 之类的来做分隔符,主要要保证分隔符的一致性。

注意使用冒号创建嵌套命名空间的方法

使用两个有序集合来有序地存储文章:第一个有序集合的成员为文章 ID,分值为文章的发布时间;第二个有序集合的成员同样为文章 ID,而分值则为文章的评分。通过这两个有序集合,网站既可以根据文章发布的先后顺序来展示文章,又可以根据文章评分的高低来展示文章。

为了防止用户对同一篇文章进行多次投票,网站需要为每篇文章记录一个已投票用户名单。为此,程序将为每篇文章创建一个集合,并使用这个集合来存储所有已投票用户的 ID

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

群组功能由两个部分组成,一个部分负责记录文章属于哪个群组,另一个部分负责取出群组里面的文章。为了记录各个群组都保存了哪些文章,网站需要为每个群组创建一个集合,并将所有同属一个群组的文章 ID 都记录到那个集合里面

然后可以使用这个群组集合与分值的有序集合做 ZINTERSTORE 操作,就可以得到该群组里面文章的对应分值。这里最好把每次计算的结果保存 60 秒,避免短时间内大量的重复计算

Web 应用中的 Redis

  • 基于令牌 cookie 和 Redis 一起实现的登录功能
  • 利用 Redis 保存购物车信息,每个购物车都是一个散列,存储了商品 ID 与商品订购数量
  • 缓存网页(不需要每次都进行渲染)
  • 缓存数据行(尽量减少直接访问数据库的机会)
  • 自动补全程序的数据源
  • 构建分布式锁来提高性能(锁本身也可以带有超时)
  • 开发计数信号量来控制并发
  • 任务队列
  • 消息拉取系统来实现延迟消息传递
  • 文件分发

事务

要在 Redis 里面执行事务,我们首先需要执行 MULTI 命令,然后输入那些我们想要在事务里面执行的命令,最后再执行 EXEC 命令。当 Redis 从一个客户端那里接收到 MULTI 命令时,Redis 会将这个客户端之后发送的所有命令都放入到一个队列里面,直到这个客户端发送 EXEC 命令为止,然后 Redis 就会在不被打断的情况下,一个接一个地执行存储在队列里面的命令。从语义上来说,Redis 事务在 Python 客户端上面是由流水线实现的:对连接对象调用 pipeline() 方法将创建一个事务,在一切正常的情况下,客户端会自动地使用 MULTI 和 EXEC 包裹起用户输入的多个命令。此外,为了减少 Redis 与客户端之间的通信往返次数,提升执行多个命令时的性能,Python 的 Redis 客户端会存储起事务包含的多个命令,然后在事务执行时一次性地将所有命令都发送给 Redis。

MULTI 和 EXEC 事务的一个主要作用是移除竞争条件。

过期时间

只有少数几个命令可以原子地为键设置过期时间,并且对于列表、集合、散列和有序集合这样的容器来说,键过期命令只能为整个键设置过期时间,而没办法为键里面的单个元素设置过期时间(为了解决这个问题,本书在好几个地方都使用了存储时间戳的有序集合来实现针对单个元素的过期操作)

这部分需要深挖一下

持久化

快照

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。在创建快照之后,用户可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本,还可以将快照留在原地以便重启服务器时使用。

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

在个人开发服务器上面,主要考虑的是尽可能地降低快照持久化带来的资源消耗。基于这个原因以及对自己硬件的信任,我只设置了 save 900 1 这一条规则。其中 save 选项告知 Redis,它应该根据这个选项提供的两个值来执行 BGSAVE 操作。在这个规则设置下,如果服务器距离上次成功生成快照已经超过了 900 秒,并且在此期间执行了至少一次写入操作,那么 Redis 就回自动开始一次新的 BGSAVE 操作。

如果你打算在生产服务器中使用快照持久化并存储大量数据,那么你的开发服务器最好能够运行在与生产服务器相同或者相似的硬件上面,并在这两个服务器上使用相同的 save 选项、存储相似的数据集并处理相近的负载量。把开发环境设置得尽量贴近生产环境,有助于判断快照是否生成得过于频繁或者过于稀少。

当 Redis 存储的数据量只有几个 GB 的时候,使用快照来保存数据是没有问题的。Redis 会创建子进程并将数据保存到硬盘里面,生成快照所需的时间比你读这句话所需的时间还要短。但随着 Redis 占用的内存越来越多,BGSAVE 在创建子进程时耗费的时间也会越来越多。如果 Redis 的内存占用量达到数十个 GB,并且剩余的空闲内存并不多,或者 Redis 运行在虚拟机上面,那么执行 BGSAVE 可能会导致系统长时间地停顿,也可能引发系统大量地使用虚拟内存,从而导致 Redis 的性能降低至无法使用的成都

执行 BGSAVE 而导致的停顿时间有多长取决于 Redis 所在的系统:对于真实的硬件、VMWare 虚拟机或者 KVM 虚拟机来说,Redis 进程每占用一个 GB 的内存,创建该进程的子进程所需的时间就要增加 10-20 毫秒;而对于 Xen 虚拟机来说,根据配置的不同,Redis 进程每占用一个 GB 的内存,创建该进程的子进程所需的时间就要增加 200-300 毫秒。

因此,如果我们的 Redis 进程占用了 20GB 的内存,那么在标准硬件上运行 BGSAVE 所创建的子进程将导致 Redis 停顿 200-400 毫秒;如果我们使用的是 Xen 虚拟机(亚马逊 EC2 和其他几个云计算供应商都使用这种虚拟机),那么相同的创建子进程操作将导致 Redis 停顿 4-6 秒。用户必须考虑自己的应用程序能否接受这种停顿。

为了防止 Redis 因为创建子进程二出现停顿,我们可以考虑关闭自动保存,转而通过手动发送 BGSAVE 或者 SAVE 来进行持久化。手动发送 BGSAVE 一样会引起停顿,唯一不同的是用户可以通过手动发送 BGSAVE 命令来控制停顿出现的时间。另一方面,虽然 SAVE 会一直阻塞 Redis 直到快照生成完毕,但是因为它不需要创建子进程,所以就不会像 BGSAVE 一样因为创建子进程而导致 Redis 停顿;并且因为没有子进程在争抢资源,所以 SAVE 创建快照的速度回避 BGSAVE 创建快照的速度要来得更快一些。

根据我的经验,在一台拥有 68GB 内存的 Xen 虚拟机上面,对一个占用 50GB 内存的 Redis 服务器执行 BGSAVE 命令的话,光是创建子进程就需要花费 15 秒以上,而生成快照则需要花费 15-20 分钟;蛋使用 SAVE 只需要 3-5 分钟就可以完成快照的生成工作。因为我的应用程序只需要每天生成一次快照,所以我写了一个脚本,让它在每天凌晨 3 点停止所有客户端对 Redis 的访问,调用 SAVE 命令并等待该命令执行完毕,之后备份刚刚生成的快照文件,并通知客户端继续执行操作。

如果用户能够妥善地处理快照持久化可能会带来的大量数据丢失,那么快照持久化对用户来说将是一个不错的选择,但对于很多应用程序来说,丢失 15 分钟的数据都将是不可接受的,在这种情况下,我们可以使用 AOF 持久化来存储在内存里面的数据尽快地保存到硬盘里面。

AOF 持久化

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

在向硬盘写入文件时,至少会发生 3 件事。当调用 file.write() 方法对文件进行写入时,写入的内容首先会被存储到缓冲区,然后操作系统会在将来的某个时候将缓冲区存储的内容写入硬盘,而数据只有在被写入硬盘之后,才算是真正地保存到了硬盘里面。用户可以通过调用 file.flush() 方法来请求操作系统尽快地将缓冲区存储的数据写入硬盘里,但具体何时执行写入操作仍然由操作系统决定。除此之外,用户还可以命令操作系统将文件同步(sync)到硬盘,同步操作会一直阻塞直到指定的文件被写入硬盘为止。当同步操作执行完毕之后,即使系统出现故障也不会对被同步的文件造成任何影响。

如果用户使用 appendfsync always 选项的话,那么每个 Redis 写入命令都会被写入硬盘,从而将发生系统崩溃时出现的数据丢失减到最少。不过遗憾的是,因为这种同步策略需要对硬盘进行大量写入,所以 redis 处理命令的速度会受到硬盘性能的限制:机械硬盘在这种同步频率下每秒智能处理大约 200 个写命令,而固态硬盘每秒大概也只能处理几万个写命令。

使用固态硬盘的用户请谨慎使用 appendfsync always 选项,因为这个选项让 Redis 每次只写入一个命令,这种不断写入少量数据的做法可能会引发严重的写入放大(write amplification) 问题,在某些情况下甚至会将固态硬盘的寿命从原来的几年降低为几个月。

为了兼顾数据安全和写入性能,用户可以考虑使用 appendfsync everysec 选项,让 Redis 以每秒一次的频率对 AOF 文件进行同步。Redis 每秒同步一次 AOF 文件时的性能和不使用任何持久化特性时的性能相差无几。

为了解决 AOF 文件体积不断增大的问题,用户可以向 Redis 发送 BGREWRIEAOF 命令,这个命令会通过移除 AOF 文件中的冗余命令来重写 AOF 文件,使其体积尽可能小。


使用复制和 AOF 持久化可以极大地保障数据安全;在多个客户端同时处理相同的数据时,可以使用 WATCH, MULTI, EXEC 等命令来防止数据出错。

性能

通过 Redis 自带的性能测试程序 redis-benchmark 来了解 Redis 的性能特征

redis-benchmark -c 1 -q 利用 -q 简化输出结果,给定 -c 1 让程序只使用一个客户端来进行测试

  • 单个客户端的性能达到了 redis-benchmark 的 50%~60% - 不使用流水线时的预期性能
  • 单个客户端的性能达到了 redis-benchmark 的 25%~30% - 对于每个命令或者每组命令都创建了新的连接 - 通过重用已有的 Redis 连接解决
  • 客户端返回错误 Cannot assign requested address - 对于每个命令或者每组命令都创建了新的连接 - 通过重用已有的 Redis 连接解决

大部分 Redis 客户端都提供了某种级别的内置连接池(需要看看是否支持)

基于搜索的应用程序

  • 预处理:反向索引,parsing, tokenization, token, word
  • 去 stop words
  • 用户给出多个单词,就在多个单词对应的集合中求交集即可

广告定向

广告服务器是一种小而复杂的技术。每当用户访问一个带有广告的 Web 页面时,Web 服务器和用户的 Web 浏览器都会向远程服务器发送请求以获取广告,广告服务器会接收各种各样的信息,并根据这些信息找出能够通过点击、浏览或动作获得最大经济收益的广告。

广告服务器需要接受一系列定向参数以便挑选出具体的广告,这些参数至少需要包含浏览者的基本位置信息(通常来源于 IP 地址)、浏览者使用的操作系统以及 Web 浏览器、可能还有浏览者正在浏览的页面的内容,甚至浏览者在当前网站上最近浏览过的一些页面。

广告预算:在典型的定向广告平台上面,每个广告通常都会带有一个随着时间减少的预算。一般来说,广告预算应该被分配到不同的时间上面,我发现的一种实用且有效的方法,就是基于小时数对广告的总预算进行划分,并在同一个小时的不同时间段把预算分配给不同的广告。

针对广告的索引操作和针对其他内容的索引操作并没有太大的不同。广告索引操作的特别之处在于它返回的不是一组广告或者一组搜索结果,而是单个广告;并且被索引的广告通常都拥有像位置、年龄或性别这类必须的定向参数。

接下来介绍的是基于位置和内容对广告进行索引的方法。我们先了解如何以一致的方式评估广告的价格。

计算广告的价格

Web 页面上展示的广告主要有 3 种方式:按展示次数计费(cost per view)、按点击次数计费(cost per click)和按动作执行次数计费(cost per action)。按动作执行次数计费又称按购买次数计费(cost per acquisition)。按展示次数计费的广告又称 CPM 广告或按千次计费(cost per mille)广告,这种广告每展示 1000 次就需要收取固定的费用。按点击计费的广告又称 CPC 广告,这种广告根据被点击的次数收取固定的费用。按动作执行次数计费的广告又称 CPA 广告,这种广告根据用户在广告目的地网站上执行的动作收取不同的费用。

让广告的价格保持一致

为了尽可能地简化广告的计算方式,程序将对所有类型的广告进行转换,使得它们的价格可以基于每千次展示进行计算,产生出一个估算 CPM(estimated CPM),简称 eCPM。对于 CPM 广告来说,因为这种广告已经给出了 CPM 价格,所以程序只要直接把它的 CPM 用作 eCPM 就可以了。至于 CPC 广告和 CPA 广告,程序则需要根据相应的规则为它们计算出 eCPM。

计算 CPC 广告的 eCPM

对于 CPC 广告,程序只要将广告的每次点击架构乘以广告的点击通过率(click-through rate, CTR),然后再乘以 1000,得出的结果就是广告的 eCPM(其中点击通过率可以用广告被点击的次数除以广告展示的次数计算得出)。举个例子,如果广告的每次点击价格为 0.25 美元,通过率为 0.2%(即0.002),那么广告的 eCPM 为 0.25x0.002x1000=0.5 美元

计算 CPA 广告的 eCPM

CPA 广告计算 eCPM 的方法和 CPC 广告计算 eCPM 的方法在某种程度上是相似的。程序只需要将广告的点击通过率、用户在广告投放者的目标页面上执行动作的概率、被执行动作的价格这三者相乘起来,然后再乘以 1000,得出的结果就是广告的 eCPM。举个例子,如果广告的点击通过率为 0.2%,用户执行动作的概率为 10%(即 0.1),而广告的 CPA 为 3 美元,那么广告的 eCPM 为 0.002x0.1x3x1000=0.60 美元

将广告插入索引

对广告进行定向需要用到一组定向参数,其中既有可选的参数,也有必须的参数。为了正确地进行广告定向,广告的索引必须考虑定向的需求。本节要实现的广告系统接受两个定向选项:位置和内容。其中位置选项(包括城市、州和国家)是必须的,而广告与页面内内容之间的任何匹配单词则是可选的,并且只作为广告的附加值存在。

执行广告定向操作

当系统收到广告定向请求的时候,它要做的就是在匹配用户所在位置的一系列广告里面,找出 eCPM 最高的那一个广告。除了基于位置对广告进行匹配之外,程序还会记录页面内容与广告内容的匹配程度,以及不同匹配程度的那些内容就回作为附加值被计入由 CPC 和 CPA 计算出的 eCPM 里面,使得哪些包含了匹配内容的广告能够更多地被展示出来。

在展示广告之前,系统不会为 Web 页面的任何内容设置附加值。但是当系统开始展示广告的时候,它就会记录下广告中包含的哪个单词改善或者损害了广告的预期效果,并据此修改各个可选的定向单词的相对价格。

构建简单的社交网站

在用户与 Twitter 进行交互时,用户和状态消息这两类对象是最为重要的。用户对象存储了用户的基本身份标识信息、用户的关注者人数、用户已发布的状态消息数量等信息。用户对象对于社交网站来说非常重要,因为它是构建其他可用并且有趣的数据的起点。除了用户对象以外,状态消息也同样重要,因为它记录了不同的用户都说了些什么,以及不同用户之间进行了什么交互,这些由用户创建的状态消息是社交网站真正的内容。

用散列来存储用户信息,这些信息包括用户的用户名、用户拥有的关注者人数、用户正在关注的人的数量、用户已经发布的状态消息的数量、用户的注册日期以及其他一些元信息。

当一个新用户进行注册的时候,程序需要做的就是根据用户指定的用户名以及当时的时间戳,创建一个正在关注数量、关注者数量、已发布状态消息数量都被设置为 0 的对戏那个。

创建新用户的函数除了会对存储用户信息的散列进行初始化之外,还会对用户的用户名进行加锁,这个加锁操作是必须的,它可以防止多个请求在同一时间使用相同的用户名来创建新用户。在对用户名进行加锁之后,程序会检查这个用户名是否已经被其他用户抢先占用了,如果这个用户名尚未被占用的话,那么程序会为这个用户生成一个独一无二的 ID,并将用户名与用户 ID 进行关联,最后将这个用户信息存储到新创建的散列里面。

敏感的用户信息:因为程序会频繁地取出存储用户信息的散列用于渲染模板,或者直接用作 API 请求的回复,所以程序不能将散列吼的密码、邮件地址等敏感信息存储在这个用户信息散列里面。

程序既会将用户的个人信息存储到用户简介里面,又会将用户所说的话记录到状态消息里面,并且和存储用户个人信息时的方法一样,程序也使用散列结构来存储状态消息。

用户在已登录的情况下访问 Twitter 时,首先看到的是他们自己的主页时间线,这个时间线是一个列表,它由用户以及用户正在关注的人所发布的状态消息组成。因为主页时间线是用户访问网站时的主要入口,所以这些数据必须尽可能地易于获取。

我们希望能够尽快地获取展示一个页面所需的全部数据,因此我们决定使用有序集合来实现主页时间线,并使用有序集合的成员来记录状态消息的 ID,而有序集合的分值则用于记录状态消息发布时的时间戳。

因为主页时间线只存储了状态消息的 ID 而不是状态消息本身,所以负责获取最新发布的状态消息的函数除了要获取状态消息的 ID 之外,还需要根据所得的 ID 获取相应的状态消息数据。

用户的主页时间线和个人时间线都是由有序集合存储的,这些有序集合存储着状态消息的 ID 以及状态消息发布时的时间戳。用户的正在关注列表以及关注者列表同样由有序集合存储,其中有序集合的成员为用户 ID,而分值则记录了用户开始关注某人或者被某人关注时的时间戳。

当用户开始关注或者停止关注另一个用户的时候,程序就需要对这两个用户的正在关注有序集合以及关注者有序集合进行更新,并修改他们在用户信息散列里面记录的关注数量和被关注数量。如果用户执行的是关注操作,那么程序在对以上提到的有序集合和散列进行更新之后,还需要从被关注用户的个人时间线里面,复制一些状态消息 ID 到执行关注操作的用户的主页时间线里面,从而使得用户在关注另一个用户之后,可以立即看见被关注用户所发布的状态消息。

在关注某个人并阅读他的状态消息一段时间之后,用户可能会想要取消对那个人的关注。实现取消关注操作的方法和实现关注操作的方法正好相反:程序会从正在关注有序集合以及关注者有序集合里面移除关注者和被关注者双方的用户 ID,并从执行取消关注操作的用户的主页时间线里面移除被取消关注的人所发布的状态消息,最后对两个用户的正在关注数量以及关注者数量进行更新。

前面已经介绍了程序是如何创建新的状态消息的,而在此之后,程序要做的就是想办法把新状态消息的 ID 添加到每个关注者的主页时间线里面。具体的添加方式会根据消息发布人拥有的关注者数量的多少而有所不同。如果用户的关注者数量相对比较少,那么程序可以立即更新每个关注者的时间线。但是,如果用户的关注者数量非常庞大,那么直接执行添加操作将导致发布消息的用户需要长时间地进行等待,超出合理的等待时间。

为了让发布操作可以尽快地返回,程序需要做两件事情。首先,在发布状态消息的时候,程序会将状态消息的 ID 添加到前 1000 个关注者的主页时间线里面。剩余的会放到任务队列中进行执行延迟执行。

在开发社交网站的过程中,我们可能会想要知道更多网站上正在发生的事情——比如网站每个小时会发布多少条新的状态消息,网站上最热门的主题是什么,诸如此类。我们可以构建一些函数来广播简单的事件,然后由负责进行数据分析的事件监听器来接收并处理这些事件。

流 API 跟我们前面为了仿制 Twitter 而构建的其他部分完全不同,前面几节实现的 Twitter 典型操作都需要尽快地执行并完成,而流 API 请求则需要在一段比较长的时间内持续地返回数据。

在构建流 API 的过程中需要进行各种各样的决策,主要和以下三个问题有关:

  • 流 API 需要对外公开哪些事件
  • 是否需要进行访问限制?如果需要的话,采取何种方式实现?
  • 流 API 应该提供哪些过滤选项?

我们的社交网站提供的过滤选项(filtering option)在特性和功能方面与 Twitter 为公开流(public stream)提供的 API 非常相似:用户既可以通过关注过滤器(基于用户进行过滤)、监测过滤器(基于关键字进行过滤)以及位置过滤器来获取过滤后的消息,又可以通过类似 Twitter 的消防水管(firehose)和样本(sample)这样的流来获取一些随机的消息。

每当新诞生的状态消息与过滤器相匹配的时候,流 APi 就会将这条消息返回给客户端。尽管 WebSockets 和 SPDY 这样的新技术可以以增量的方式不断地生成数据,甚至进行服务器端的消息推送,但是这些技术的相关协议并未完全制定好,而且很多编程语言的客户端也未能完全地支持这些新技术。幸运的是,只要使用分块(chunked)传输编码,我们就可以使用 HTTP 服务器生成并发送增量式数据。

进阶内容

降低 Redis 内存占用的三种方法:

  • 短结构(short structure)
  • 分片结构(shared structure)
  • 打包存储二进制位和字节

短结构

Redis 为列表、集合、散列和有序集合提供了一组配置选项,这些选项可以让 Redis 以更节约空间的方式存储长度较短的结构。

在列表、散列和有序集合的长度较短或者体积较小的时候,Redis 可以选择使用一种名为压缩列表(ziplist)的紧凑存储方式来存储这些结构。压缩列表是列表、散列和有序集合这 3 种不同类型的对象的一种非结构化(unstructured)表示:与 Redis 在通常情况下使用双链表表示列表、使用散列表表示散列、使用散列表加上跳跃表(skiplist) 表示有序集合的做法不同,压缩列表会以序列化的方式存储数据,这些序列化数据每次被读取的时候都要进行解码,每次被写入的时候也要进行局部的重新,并且可能需要对内存里面的数据进行移动。

让键名保持简短!

分片结构

分片本质上就是基于某些简单的规则将数据划分为更小的部分,然后根据数据所属的部分来决定将数据发送到哪个位置上面。

程序不再是将值 X 存储到键 Y 里面,而是将值 X 存储到键 Y: 里面。

对列表进行分片:想要在不使用 Lua 脚本的情况下对列表进行分片是非常困难的事。

打包存储二进制位和字节

暂时略

Lua 脚本

  • 通过 Redis 客户端可以载入 Lua 脚本
  • 传递给 Lua 脚本的键和参数:尽管被载入程序包裹了起来,调用 Lua 脚本至少需要传递 3 个参数:第一个是必不可少的 Redis 连接,第二个是由任意多个键组成的列表,第三个是由任意多个需要传递给脚本的参数组成的列表
  • Lua 脚本跟单个 Redis 命令已经 MULTI/EXEC 事务一样,都是原子操作
  • 已经对结构进行了修改的 Lua 脚本将无法被中断
  • Lua 重写锁的机制
  • 移除 WATCH/MULTI/EXEC 事务
  • 对列表进行分片
您的支持是对我创作最大的鼓励!

热评文章