Redis开发与运维笔记

活跃思想

Posted by 大狗 on January 17, 2021

Redis开发与运维笔记

第一章 可靠,可拓展和可维护性

1.1 为什么?考虑什么?

由于数据库,消息队列,缓存等工具虽然差异巨大,可是目前他们的功能越来越混淆。因此它们被统称为“数据系统”。那么我们如何考察一个数据系统?:

  • 可靠性:系统在困境(硬件故障,软件故障,人为错误)中仍可正常工作(正确完成工作,并能达到期望水平)
  • 可拓展性:有合理的方法面对系统的增长(数据量,流量,复杂性)
  • 可维护性:不同的人在不同的生命周期,都能高效地在系统上工作。

1.2 可靠性

容忍故障,提供容错常见的三个方面

  • 硬件故障:比方说磁盘坏道,掉电。因此应该组织磁盘RAID,双路电源
  • 软件错误:软件错误系统比较多:1>接收特定的错误输入,导致服务器崩溃。2>失控程序占用一些CPU资源,内存,磁盘和网络等。3>系统依赖的服务变慢。因此应该仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现差异(discrepancy)时报警
  • 人为错误:常见的人为错误包括很多,任何配置错误都是人为错误。因此,以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,将人们最容易犯错的地方与可能导致失效的地方解耦(decouple),允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。

1.3 可拓展性

可扩展性(Scalability) 是用来描述系统应对负载增长能力的术语。但是请注意,这不是贴在系统上的一维标签:说“X可扩展”或“Y不可扩展”是没有任何意义的。相反,讨论可扩展性意味着考虑诸如“如果系统以特定方式增长,有什么选项可以应对增长?”和“如何增加计算资源来处理额外的负载?”等问题。下面看看几个常见问题。

  • 如何描述负载?负载可以用一些称为 负载参数(load parameters) 的数字来描述。参数的最佳选择取决于系统架构,它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。
  • 如何描述性能?对于Hadoop这样的批处理系统,通常关心的是吞吐量(throughput),即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间iii。对于在线系统,通常更重要的是服务的响应时间(response time),即客户端发送请求到接收响应之间的时间。更详细的数据包括:通常报表都会展示服务的平均响应时间。 (严格来讲“平均”一词并不指代任何特定公式,但实际上它通常被理解为算术平均值(arithmetic mean):给定 n 个值,加起来除以 n )。通常使用百分位点(percentiles)会更好。如果将响应时间列表按最快到最慢排序,那么中位数(median)就在正中间

那么如何面对负载呢?

  • 人们经常讨论纵向扩展(scaling up)垂直扩展(vertical scaling),转向更强大的机器)和横向扩展(scaling out)水平扩展(horizontal scaling),将负载分布到多台小机器上)之间的对立。跨多台机器分配负载也称为“无共享(shared-nothing)”架构。
  • 有些系统是 弹性(elastic) 的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动扩展(人工分析容量并决定向系统添加更多的机器)。如果负载极难预测(highly unpredictable),则弹性系统可能很有用,但手动扩展系统更简单,并且意外操作可能会更少

1.4 可维护性

维护性关心以下三点:

  • *可操作性(Operability)*便于运维团队保持系统平稳运行。
  • *简单性(Simplicity)*从系统中消除尽可能多的复杂度(complexity),使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)。一种良好的针对复杂性的方法是:用于消除额外复杂度的最好工具之一是抽象(abstraction)。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。
  • *可演化性(evolability)*使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为可扩展性(extensibility)可修改性(modifiability)可塑性(plasticity)

习题:

  • Redis为啥那么快?1 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMapHashMap的优势就是查找和操作的时间复杂度都是O(1);2 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;3 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;4 使用多路I/O复用模型,非阻塞IO;

  • Redis单线程,如何利用多线程?在单机开多个Redis实例

  • 单机会有瓶颈,那你们是怎么解决这个瓶颈的? 我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node

第二章 数据模型与共识 和REDIS如何执行底部数据查询的?

2.1 数据模型

什么是数据模型?多数应用使用层层叠加的数据模型构建。对于每层数据模型的关键问题是:它是如何用低一层数据模型来表示的?

常见的数据模型分为两种:关系模型,文档模型。如何在关系模型和文档模型之间选择呢?我们这里只考虑数据模型中的差异,而不考虑容错属性和处理并发性的方面

2.1.1 关系模型

现在最著名的数据模型可能是SQL。它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织成关系(SQL中称作),其中每个关系是元组(SQL中称作)的无序集合。关系模型的优缺点如何?:

  • 优点1:多对一和多对多的关系(许多人生活在一个特定的地区,许多人在一个特定的行业工作)。关系模型对于这种连接的关系处理的很好。但JSON则不尽如人意。
  • 优点2:关系模型使用简单,它将所有的数据放在光天化日之下:一个 关系(表) 只是一个 元组(行) 的集合,仅此而已。无须担心和其它表的关系等问题。

  • 缺点1:目前大多数应用程序开发都使用面向对象的编程语言来开发,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。模型之间的不连贯有时被称为阻抗不匹配。可类比简历

2.1.2 文档模型

文档模型有哪些优缺点?

  • 优点1:规范化和反规范化比较简单。
  • 优点2:文档数据模型具备架构灵活性,因局部性而拥有更好的性能
  • 缺点1:对多对一和多对多的情况不合适

2.1.3 总结

如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。文档模型有一定的局限性:例如,不能直接引用文档中的嵌套的项目,而是需要说“用户251的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,这通常不是问题。

但如果你的应用程序确实会用到多对多关系,那么文档模型就没有那么诱人了。尽管可以通过反规范化来消除对连接的需求,但这需要应用程序代码来做额外的工作以确保数据一致性。

2.2 数据查询语言

常见的数据查询语言分为两种

  • 命令式:常见的许多常用的编程语言是命令式的多为命令式,命令式语言告诉计算机以特定顺序执行某些操作。
  • 声明式:在声明式查询语言(如SQL或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合)

2.3 图数据模型

习题

  • 数据库三大范式是什么?1 每个列都不可以再拆分;2 在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分;3在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
  • MySQL存储引擎MyISAM与InnoDB区别 1 Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统;2 提供事务的支持,也不支持行级锁和外键。3 InnoDB支持行级锁定、表级锁定,锁定力度小并发能力高,而MyISAM只支持表级锁定
  • MyISAM索引与InnoDB索引的区别?1 InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引;

第三章 数据存储与检索和REDIS的底层数据结构+REDIS的有用功能

头几部分是《数据密集型系统设计》的笔记,后面是REDIS的东西。在最后加了一些REDIS的面试题目

3.1 数据存储和索引方式

针对事务型工作负载和针对分析型负载的春初引擎优化存在巨大差异。先介绍一些常见索引

3.1.1 hash索引

使用key value找到文件的偏移,Bitcask用了这种方法。为了节省空间,需要执行压缩。压缩会将重复的箭丢弃,只保留每个键最近的更新。

追加的日志存储引擎看起来很浪费空间,为什么不原地更新?因为:

  • 追加和分段是顺序写,比随机写入更快。
  • 断文件直接或者不可变的话,并发和崩溃恢复要简单得多。
  • 合并旧段可以避免文件碎片化。

这种方式的缺点是:

  • hash表必须放入内存。如果hash表放入磁盘,会带来随机访问I/O,从而性能下降
  • 区间查询效率不高,不能简单查询A区间到B区间的所有键,只能逐个查找。

3.1.2 SSTables

排序字符串表,即SSTable,相比于基于hash索引的日志段,这种方式:

  • 合并段更加有效,每次把最小的键拷贝到输出文件,并重复过程。
  • 不需要在内存保留所有的索引,只需要根据当前已知的偏移量。

LevelDB和ROCKsDB为了应对随机写入:

  • 执行写入时,将其添加到内存的平衡树数据结构里。
  • 当内存表大于某个阈值时,作为SSTable文件写入内存。
  • 处理读请求时先在内存表查询数据,然后查询最新段文件,直到写入结束。
  • 后台周期性执行段合并和压缩,丢弃被覆盖或删除的值。

实际上SSTable就是LSM-Tree,基于合并和压缩排序原理的存储引擎通常都被称为LSM存储引擎。

3.1.3 B树

B树将数据库分解为固定大小的块或页,传统上大小为4kb,页是内部读/写的最小单元。B树基于层级进行引用,通过范围缩减查找的边界。如下图所示:

img_btree_search

B树的更新需要查找到对应的叶子元素,如果页中没有足够可用空间,则将其分裂为两个半满的页面,父页也需要更新以包含分裂之后的新的键范围。

为了数据库能从崩溃中恢复,常见的B树需要预写日志(write-ahead log, WAL),但B树需要关注多线程的并发问题。

3.1.4 B树和LSM的对比

LSM的优点:

  • 相比较Btree(两次写入,一次预写日志,一次写入树本身),LSM因为顺序写入和较低的写放大(由于一次数据库的写入而导致多次的磁盘读写,对B树是两次,对LSM是因为反复压缩和SSTable的合并),通常能够承受比B树更高的写入吞吐量。
  • 由于LSM-tree没有碎片,有定期重写以消除碎片化的问题,因此LSM-tree有较低的存储开销

LSM的缺点:

  • 压缩过程会干扰正常读写,
  • 高写入吞吐时,数据库的规模越大,压缩的成本越高,就约会侵扰正常的写入操作。

Btree的有点:

  • B树里每个键对应索引的唯一位置,因此如果数据库希望提供强大的事务语义,B-tree更简单:这些锁可以直接定义到树里

3.1.5 其他索引

下面讨论一些其他索引:

  • 在索引中存储值:值可以使上述的实际行,也可以是对其他地方存储的行的引用。后一种情况,存储行的具体位置被称为堆文件。某些情况下,从索引到对文件的额外跳转意味性能损失,因此希望将索引行直接存储在索引里,这被称为聚集索引。MySQL的InnoDB存储引擎里,表的主键就是聚集索引,二级索引引用主键。
  • 多列索引:
  • 全文索引和模糊搜索:状态机,字典树,等等。

3.1.6 MYSQL里面的索引

索引建立的基本原则:

  • 最左前缀原则:mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的
  • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
  • .尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少
  • .索引列不能参与计算,保持列“干净”
  • 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引

慢查询的优化:

  1. 先运行看看是否真的很慢,注意设置SQL_NO_CACHE
  2. where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高
  3. explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
  4. order by limit 形式的sql语句让排序的表优先查
  5. 了解业务方使用场景
  6. 加索引时参照建索引的几大原则
  7. 观察结果,不符合预期继续从0分析

3.2 事务处理与分析

基于事务处理的业务和基于大数据分析的业务并不一致,他们:

  • 访问模式和处理业务的交易类似的,被称为在线事务处理(online transaction processing, OLTP)。主要是日常的数据库。
  • 基于数据分析的,被称为在线分析处理(online analytic processing, OLAP)。主要对数据仓库分析。数据仓库存储的并不是具体的商品价格之类的事情,而是事件,比方说客户点击查询等等的数据。

基于索引的算法适合OLTP,不擅长应对分析查询。那么分析查询做了些什么改进呢?

3.2.1 星型(雪花型)分析模式

3.2.2 雪花性

3.5 redis的内部数据结构

type命令实际返回的就是当前键的数据结构类型, 它们分别是:string(字符串) 、 hash(哈希) 、 list(列表) 、 set(集合) 、 zset(有序集合)。除了这五种类型之外,还有一些其它的类型,比方说:HyperLogLog

练习题

REDIS练习题

  • Redis有哪些数据结构呀StringHashListSetSortedSet。但是,如果你是Redis中高级用户,而且你要在这次面试中突出你和其他候选人的不同,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub

  • 如果有大量的key需要设置同一时间过期,一般需要注意什么?Redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。**电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中,如果失效时间一样,又刚好在失效的时间点大量用户涌入,就有可能造成缓存雪崩

  • 那你使用过Redis分布式锁么,它是什么回事?先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

  • 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?使用keys指令可以扫出指定模式的key列表。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复

  • 使用过Redis做异步队列么,你是怎么用的? 一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。如果对方追问可不可以不用sleep呢? list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

  • Pipeline有什么好处,为什么要用pipeline? 可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

SQL习题

  • 什么是索引,有哪些优缺点?索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。优点:1可以大大加快数据的检索速度,这也是创建索引的最主要的原因;2用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。缺点:1创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;2索引需要占物理空间。
  • 索引有几种类型?1 主键索引:数据列不允许重复,不允许为NULL,一个表只能有一个主键。2 唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。3 普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。4 全文索引: 是目前搜索引擎使用的一种关键技术。
  • 索引的数据结构?B树和hash
  • 创建索引的原则?1 最左前缀匹配原则,组合索引非常重要的原则 2 较频繁作为查询条件的字段才去创建索引 3 更新频繁字段不适合创建索引 4 若是不能有效区分数据的列不适合做索引列 5 尽量的扩展索引,不要新建索引。
  • B树和B+树的区别? 1 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。2 B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。
  • 使用B树和B+树各有什么优缺点呢?1 B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。2 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。

第五章 持久化

两种持久化方式:

  • RDB,二进制持久化,恢复方式更快,但量级太高,没法做到实时持久化,兼容性差。save/bgsave命令执行。
  • AOF,增量持久化,CLI方式,和BACKEND比较相似。bgsave命令执行

习题

  • Redis是怎么持久化的?服务主从数据怎么交互的? RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
  • Redis的同步机制了解么? Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。

第六章 复制与REDIS的复制

首先是《数据密集型应用系统设计》里面的复制章节的笔记。追求副本数据一致性常见的方法为下面三节:主从复制,多主复制,无主复制。最后则是redis的主从设计。

6.1 主从复制

6.1.1 主从复制的步骤

具体步骤是:

  • 指定一个节点为主节点。客户写数据库时,必须将写请求首先发送给主副本,主副本首先将新数据写入本地存储。
  • 其他副本成为从节点。主副本把新数据写入本地存储后,将数据更改为复制的日志或更改流发送给所有从副本。每个从副本获得更改日志之后将其应用到本地,且严格执行与主副本相同的写入顺序。
  • 客户端从数据库中读数据可以从主节点或节点执行查询,写操作只有主节点可以执行。再次强调,只有主副本可以执行写请求,其他副本都是只读的。

6.1.2 同步复制与异步复制

6.1.3 配置新的从节点

锁定数据库会导致高可用的降低,那么如何添加从节点还不会破坏高可用呢,步骤如下:

  • 在某个时间点对主节点的数据副本产生一个一致性快照,避免长时间锁定数据库。
  • 拷贝快照到新的从节点
  • 获取快照之后的数据更改日志。
  • 从节点利用这些日志执行数据更改,这个过程被称为追赶。

6.1.4 节点失效

6.1.4.1 从节点失效

根据副本日志来确定如何进行追赶

6.1.4.2 主节点失效

主节点常见的自动切换步骤是:

  • 确认主节点失效
  • 选举新的主节点
  • 重新配置系统使新主节点生效

6.1.5 复制技术的实现

  • 基于语句的复制:任何主节点记录的所执行的写请求并将该操作语句作为日志发送给从节点。但是这个方法有问题:1非确定性函数,比方说NOW()或者RAND()不同节点值不一样。2如果某些语句使用自增列或者依赖于数据库的现有数据,所有副本必须按相同顺序执行3副作用语句(比方说触发器,存储过程,用户自定义函数)可能会产生不同的副作用
  • 基于预写日志传输
  • 基于行的逻辑日志复制:将复制与存储逻辑玻璃,这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。对于行插入,日止包含所有相关列的新值。对于行删除,日志里需要有足够信息来标识已经删除的行,比方说主键。对于行更新,日志需要包含足够信息来唯一标识新的行,以及所有列的新值。这种方式兼容性很好。

6.1.6 复制滞后问题

三种复制滞后问题并且各有各的解决方式:

  • 读自己的写:主从复制的系统如何保证实现写后读一致性?方法很多:比方说:1如果访问修改数据就走主节点2客户端发送自己带的时钟,从而实现有效性。
  • 单调读:这个东西不就是内存模型一致性里面提供的模型吗?保证单调读的顺序,但是不保证每个看到的都是一样的。
  • 前缀一致性

6.2 多主节点复制

优点很多,写冲突是主要的问题。拓扑结构也可能导致先后顺序的错误

6.3 无主节点复制

无主节点复制的方法是放弃主节点,如果多于n/2(n是数据库副本的个数)那么就认为写入成功(这种想法是最常见的想法),但是对于写入失败的副本,有两种处理方法:

  • 读修复过程,客户端发现有过期的返回值时,就使用新值覆盖旧的值
  • 反熵过程,一些数据存储有后台进程不断查找副本之间的数据差异,将缺少的数据从一个副本复制到另一个副本。

6.3.1 写入仲裁

如何执行仲裁?上面的说法是多于n/2,那么一般化的话是要求w+r>n,也就是保证读写必然会读取到至少一个新版本。但是如果多个节点追求最终一致性,那么就有相应的问题出现了。

6.3.2 检测并发写

由于网络不稳定或者局部失效,存在多个节点对同一地址写入不一致的地方,那么如何避免这个问题呢?

  • 最后写入者获胜,这个方法实际上就是强制对写入的顺序排序
  • 利用可区分的版本来进行鉴别,旧版本的东西会基于旧版本修改,最后进行几个新版本之间的合并。happens-before和并发,和多线程的东西实际上没什么区别。

6.4 redis的主从复制

6.4.1 redis复制的命令

slaveof xxx/slaveof no one。复制操作是异步执行的。断开的时候执行:1>断开与主节点复制关系2>从节点晋升为主节点

其中TCP_NODELAY怎么听怎么像是合并小包

6.4.2 拓扑

  • 单主单从节点时,如果主机没开启持久化且需要重启,那么千万先让从节点断开复制关系再让主节点重启
  • 一主多从关系,适合多读少写的关系,多从可以实现读写分离,同时提供良好的读并发。但是对于写并发高的场景会导致写命令的多次发送,影响主节点负载稳定性
  • 树状主从关系,为了避免挂在多个从节点时,使用这种复制方式。

6.4.3 原理

6.4.3.2 删除过时数据
  • 惰性删除
  • 定时删除

第七章 集群与分区

先针对《数据密集型应用系统设计》做的分区笔记,然后再记录《redis开发与运维》的集群。

7.1 数据分区与数据复制

分区通常与复制结合使用,即每个分区在多个节点上都有副本,所以某条特定的记录会位于 特定的分区上,但同样的内容会保存在不同的节点上以提高系统的容错性。具体见图片

7.1.1 分区方法

7.1.1.1 基于关键字区间分区

最常见的基于关键字的分区方式就是字典序了,recursive地按照英文字母的顺序进行排序带来的好处是方便查找,但是问题在于是数据可能分配并不均匀,某些访问模式会导致热点。另一种基于关键字的方法是利用时间戳,但根据时间戳的分布也可能带来不均匀的问题,一种可能的解决方法是先将传感器名字扔到时间戳之前进行平衡。

7.1.1.2 基于关键字hash分区

利用一个好的hash函数解决热点分配问题,这种方法将数据均匀分布,但这种方法带来的问题是查找的效率不够高,需要整个都进行查询。因此Cassandra在两种策略做了个折中。

7.2 分区与二级索引

二级索引是什么参加第三章的B-tree

7.3 分区再平衡

随着时间的推移,数据库可能再发生变化。比方说数据规模增加/查询压力增加/节点出现故障,因此需要执行动态再平衡。

7.3.1 动态再平衡的策略

  • 问题1:为什么不使用取模呢?因为如果节点数量N发生了变化,那么会导致很多节点上的关键字进行迁移,这种迁移大大减少了迁移数据的方法。
  • 策略1:固定数量的分区:创建远超节点个数的分区数,然后为每个节点分配多个分区。出现新的节点之后再把一部分老的分区迁移过去。
  • 策略2:动态分区:类似B树的分裂方式,参加第三章的B-tree,这种方式一般是需要看分区的数据长度是否达到阈值。
  • 策略3:按照节点比例分区:使得分区数与集群节点数成正比。因此,节点个数的增加会导致原先节点里面分区的数据长度缩小。这种从原有分区里面拿数据的方式适用于基于hash分区的方式。

7.4 请求路由

如何知道请求哪个节点?常见方法有三种:

  • 允许客户端链接任意的节点,如果节点拥有分区那么继续,否则转发到下一个节点
  • 将所有客户端的请求发送到一个路由层,由厚泽负责将请求转发到对应的分区节点。路由层本身不处理请求,只是个分区感知的负载均衡器。
  • 客户端感知分区和节点分配,因此客户端可以直接链接到目标节点不需要中介。

第八章 事务

8.1 事务是什么?为什么需要事务

事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(提交(commit))要么失败(中止(abort)回滚(rollback))。

8.2 事务提供的保证

现在的ACID概念模糊不清,常常带来诸多问题,高层次的想法很美好,但细节往往不堪一击(不符合ACID标准的系统有时被称为BASE,它代表基本可用性(Basically Available)软状态(Soft State)*和*最终一致性(Eventual consistency)。有一点需要注意的是,ACID是不区分多对象还是单对象的,比方说“1写入未读邮件,2更新未读计数器”是两个对象,如果发生了更新未读计数器失败会执行回滚,这是多对象事务。写入20kb的json文件,写入到10kb的时候失败了,执行回滚,放弃写入,这是单对象事务。因此存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复:

  • 原子性(Atomicity),和多线程环境下的原子性不同,事务的原子性并不针对并发,它指的是能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。
  • 一致性(Consistency),这个概念太模糊了,有太多的意义。在ACID中值得是对数据的一组特定约束必须始终成立。即不变量(invariants)。但这种约束往往来自应用层程序,而不是数据库
  • 隔离性(Isolation)同时执行的事务是相互隔离的:它们不能相互冒犯。传统的数据库教科书将隔离性形式化为可序列化(Serializability)。这个概念是针对并发而言的。
  • 持久性(Durability)持久性 是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。

有一种需要注意的问题是,

8.3 弱隔离级别

下面会介绍几种常见的弱隔离级别。

8.3.1 弱隔离:读已提交

读已提交提供两种保证:

  • 从数据库读时,只能看到已提交的数据(没有脏读(dirty reads))。设想一个事务已经将一些数据写入数据库,但事务还没有提交或中止。另一个事务可以看到未提交的数据吗?如果是的话,那就叫做脏读(dirty reads)
  • 写入数据库时,只会覆盖已经写入的数据(没有脏写(dirty writes))。一般而言,两个事务同时更新一个相同对象会导致后面的写入覆盖前面的写入,但如果这两个事务A和B都要写两个对象比方说,A写x=1,A写y=1;B写x=2,B写y=2;系统要求x==y。如果x=2发生在x=1之后,那么B的x写入覆盖A的x写入,导致x==2,而如果y=2早于y=1,那么A的y写入会覆盖B的y写入,导致y==1,从而x!=y了,不满足系统的前提。因此一般两个事务之间互动的时候,会延迟第二个写入,直到第一个写入完成为止。

那么如何实现读已提交呢?针对读已提交的两种属性而言

  • 如何防止脏写?最常见的情况是,数据库通过使用行锁(row-level lock) 来防止脏写
  • 如何防止脏读?一种选择是使用相同的锁,但这种方式效率太低。因此很多数据库采用记住旧值的方法来防止脏读

总结来说,读已提交实现了中止(原子性的要求);它防止读取不完整的事务结果,并且防止并发写入造成的混乱。但读已提交并不是万能的,它不能防备不可重复读(nonrepeatable read)*或*读取偏差(read skew)。那么这个问题怎么解决?快照隔离是常见的想法。

8.3.2 快照隔离

什么是快照隔离?每个事务都从数据库的一致快照(consistent snapshot) 中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。使用MVCC技术实现。快照隔离是一个有用的隔离级别。在Oracle中称为可序列化(Serializable)*的,在PostgreSQL和MySQL中称为*可重复读(repeatable read)

如何实现快照隔离?

  • 快照隔离常常使用写锁,快照隔离的一个关键原则是:读不阻塞写,写不阻塞读
  • 除上面之外,快照隔离的方法:给每个事务赋予一个unique的事务id,然后所有的操作写入到最终要操作的对象的list里,数据库的垃圾收集过程会在稍后来操作这些行

那么快照隔离实现了怎么样子的可见性,或者说保证数据的一致性呢?

  • 在每次事务开始时,数据库列出当时所有其他(尚未提交或尚未中止)的事务清单,即使之后提交了,这些事务已执行的任何写入也都会被忽略。
  • 被中止事务所执行的任何写入都将被忽略。
  • 由具有较晚事务ID(即,在当前事务开始之后开始的)的事务所做的任何写入都被忽略,而不管这些事务是否已经提交。(第三条和第一条是不是矛盾了,存疑)
  • 所有其他写入,对应用都是可见的。

换言之,仅当以下两点的要求成立时,则该对象对事务可见:

  • 事务开始的时候,创建该对象的事务已经完成了提交
  • 对象没有被标记删除;或者已经标记删除了,但是还没执行

那么对这种控制,索引该怎么支持呢?

8.3.3 丢失更新和写入偏差

两个事务并发写入的问题——我们只讨论了脏写,一种特定类型的写-写冲突是可能出现的,丢失更新。丢失更新常常体现在后面的写入没有考虑第一次写入的内容,类似多线程的累加,加到最后小于期望值。

如何避免丢失更新呢?

  • 原子写
  • 显式锁定
  • CAS。

对于无主系统,存在多个副本正确的问题,基于锁/CAS对这个是无效的,因此最好使用线性化的操作。

除此之外还有其他的并发问题,写入偏差,两个事务同时更新不同的对象,从而导致不同的对象都执行操作出现了偏差,导致多个对象之间协同的不变式不再满足。类似哲学家吃饭问题,哲学家们同时举起了筷子导致都吃不了饭。这种效应:一个事务中的写入改变另一个事务的搜索查询的结果,被称为幻读【3】。快照隔离避免了只读查询中幻读,但是在像我们讨论的例子那样的读写事务中,幻读会导致特别棘手的写倾斜(写入偏差)情况。因此我们需要一种新的界别,可序列化:

8.3.4 可序列化

可序列化(Serializability)*隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止*所有可能的竞争条件。

8.3.4.1 2PL

两阶段锁定定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要独占访问(exclusive access) 权限。这不就是读写锁吗:这个东西不就是缓存一致性协议的前提吗?MRSW

  • 如果事务A读取了一个对象,并且事务B想要写入该对象,那么B必须等到A提交或中止才能继续。 (这确保B不能在A底下意外地改变对象。)
  • 如果事务A写入了一个对象,并且事务B想要读取该对象,则B必须等到A提交或中止才能继续。 (像图7-1那样读取旧版本的对象在2PL下是不可接受的。)

具体的流程可以总结为:

  • 事务读取对象,必须获取共享锁。可以多个事务获取一个对象的共享锁,但如果某个事务获取了独占锁,那么所有其他事务必须等待
  • 如果事务要修改对象,那么必须以独占模式获取锁,不允许多个事务持有锁。换言之,如果事务持有锁,修改事务必须等待
  • 如果事务先读取,然后尝试写入。需要先将共享锁升级为独占锁。
  • 事务获得锁以后,一直持有到事务结束(包括提交或者终止)。这也是名字里“二阶段”的由来

实际上,为了避免幻读的问题,操作系统往往要实现“谓词锁”,类似于前面描述的共享/排它锁,但不属于特定的对象(例如,表中的一行),它属于所有符合某些搜索条件的对象比方说。简单而言谓词锁是基于可能会添加的对象(幻想)来加/解锁的

Seletct * from bookings
WHERE room_id = 123 AND
      end_time > '2018-01-01 12:00' AND 
      start_time < '2018-01-01 13:00';

不幸的是谓词锁性能不佳:如果活跃事务持有很多锁,检查匹配的锁会非常耗时。*因此,大多数使用2PL的数据库实际上实现了索引范围锁(也称为*间隙锁(next-key locking)),这是一个简化的近似版谓词锁。

8.3 REDIS的事务

事务习题

  • 什么是数据库事务事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。
  • 常见的事务隔离级别有哪些?1 读未提交 2 读已提交:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 3 快照隔离(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。4 串行化:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读

锁习题

  • 按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法?关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。

    1 行级锁 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。

    2 表级锁 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

    3 页级锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

  • 从锁的类别上分MySQL都有哪些锁呢? 从锁的类别上来讲,有共享锁和排他锁。

    共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。

    排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

  • 什么是死锁?

第九章 不可靠的分布式系统和哨兵

本章节我们会研究不可靠的网络所导致的问题

9.1 不可靠的网络

我们一般关注的是武功乡

9.2 时钟问题与时序问题

现代计算机至少有两种不同的时钟,时钟和单调钟,但这两种钟都不怎么准,因此出现了精确时间协议(PTP)

  • 时钟: 时钟通常与NTP同步,这意味着来自一台机器的时间戳(理想情况下)与另一台机器上的时间戳相同。时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的86,400秒,时钟可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。
  • 单调钟:单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux上的clock_gettime(CLOCK_MONOTONIC),和Java中的System.nanoTime()都是单调时钟。这个名字来源于他们保证总是前进的事实。

对于LWW(最终写入胜利)的策略,只要有多同步的时钟就难以实现,毕竟难以保证三个机器ABC,其中AC的时间戳顺序,在B执行复制的时候一致。那么怎么保证一致性呢?

9.3 不可靠问题的解决方法

  • 真理由多数定义

9.1 为什么需要哨兵

REDIS的主从复制模式带来了优点:

  • 主节点出现故障,从节点可以顶上来,保证数据尽量不丢失。主从复制是最终一致性。
  • 从节点可以拓展主节点的读能力,如果主节点撑不住大并发的读操作,那么从节点就缓解压力。

但也带来缺点:

  • 主节点的写能力受到限制
  • 主节点的储存能力受到限制

因此引入了哨兵,但是哨兵并不一定是redis节点(无论是主节点还是从节点)。哨兵的功能包括:

  • 监控:检测redis节点是否可达,可用
  • 通知:将故障检测的结果传达给应用方
  • 主节点故障转移:实现从节点晋升为主节点并且维护后续的主从关系
  • 配置提供者:客户端初始化的时候连接的是哨兵节点,从而获得主节点信息。

9.2 客户端显式向哨兵请求配置

如何获取配置呢?

  • 遍历节点来获取可用的哨兵节点
  • 通过sentinel get-master-addr-by-name master-name 命令来获取真正的主节点
  • 验证主节点还有效,避免主节点失效
  • 保持和哨兵的联系

9.3 哨兵的原理

9.3.1 哨兵的定时监控方法

  • 每隔十秒,每个哨兵节点向主节点和从节点发送info命令来获取拓展。向主节点发送info,可以轻松获取从节点的拓展
  • 每隔两秒,每个哨兵节点向redis数据节点的__sentinel__:hello节点发送该哨兵节点对主节点的判断和其他信息。每个哨兵节点也会监听该频道来了解其他哨兵节点和他们对主节点的判断从而实现1)发现新的哨兵节点2)哨兵节点之间交换主节点的状态
  • 每隔一秒,每隔哨兵会向主节点,从节点,哨兵节点发送ping来做心跳。

9.3.2 主观下线和客观下线

  • 主观下线是当前的哨兵节点判断节点下线了。
  • 如果发现当前主节点主观下线了,该哨兵节点就会通过sentinel ismaster-down-by-addr命令向其他Sentinel节点询问对主节点的判断,超过<QUORM>个就判断为客观下线。也就是说,主节点down了

9.3.3 领导哨兵选举

因为主节点切换不需要太多哨兵,所以会选择一个哨兵领导者做切换。

  • 每个哨兵都能成为哨兵领导者,如果它确认主节点下线了,就会向其他哨兵节点发送sentinel is-master-down-by-addr命令,要求将自己设置为领导者。
  • 收到命令的Sentinel节点, 如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令, 将同意该请求, 否则拒绝。
  • 如果该Sentinel节点发现自己的票数已经大于等于max(quorum,num(sentinels) /2+1) , 那么它将成为领导者。
  • 如果此过程没有选举出领导者, 将进入下一次选举。

9.3.4 故障转移

那么领导者哨兵怎么做转移呢?

  • 新的领导者哨兵在从节点列表里面选出来一个新节点做主节点,流程:a)过滤不健康/五秒没回复/与主节点失联超过down-time*10的节点b)选择优先级最高的从节点,存在就返回。不存在就继续c)选择复制偏移量最大的从节点(复制的最完整),如果存在就返回,不存在就继续。d)选择runid最小的节点
  • 对新选出的从节点执行slaveof no one升级
  • 哨兵领导者向剩余节点发送命令,让他们成为新节点的从节点
  • 哨兵节点会将原先的主节点更新为从节点,并保持对其的关注

习题

  • Redis还有其他保证集群高可用的方式么?哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用

第十章 一致性和共识与REDIS集群

前面是先针对《数据密集型应用系统设计》做的分区笔记,然后再记录《redis开发与运维》

现在我们将继续沿着同样的路线前进,寻求可以让应用忽略分布式系统部分问题的抽象概念。例如,分布式系统最重要的抽象之一就是共识(consensus)就是让所有的节点对某件事达成一致

但是分布式一致性模型和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】(参见“弱隔离级别”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了,避免由于同时执行事务而导致的竞争状态,而分布式一致性主要关于,面对延迟和故障时,如何协调副本间的状态。

10.1 一致性保证

10.1.1 线性一致性的定义

什么使得线性一致性?线性一致性的基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子性的。

线性一致性在分布式系统往往过于难以实现。就有点像世界杯,不同寝室网络速度不一致,所以有的寝室先看到足球的结果,有的寝室后看到。

10.1.2 何时需要线性一致性

以下场景需要保证线性一致性:

  • 锁定和领导选举,必须保证领导唯一,否则出现脑裂。
  • 约束和唯一性保证,一般是上层协议保证。
  • 跨信道的时序依赖,不过线性一致性并不是避免这种问题的唯一方法,但它易于理解。

10.1.3 如何实现线性一致

回顾复制章节中的内容来考虑是否满足线性一致性。

  • 单主复制,可能满足线性一致,必须从领导节点更新和读取数据
  • 共识算法,线性一致
  • 多主复制,非线性一致
  • 无主复制,也许不是非线性一致的
  • 基于时钟,必然是线性不一致的

10.1.4 线性一致的代价

CAP定理:

  • 如果应⽤用需要线性⼀一致性,且某些副本因为⽹网络问题与其他副本断开连接,那么这些副本掉线时不能处理理请求。请求必须等到⽹网络问题解决,或直接返回错误。(⽆无论哪种⽅方式,服务都不可⽤(unavailable) )。
  • 如果应⽤用不不需要线性⼀一致性,那么某个副本即使与其他副本断开连接,也可以独⽴立处理理请求(例如多主复制)。在这种情况下,应⽤用可以在⽹网络问题前保持可⽤用,但其⾏行行为不不是线性⼀一致的。

10.2 顺序保证

10.2.1 顺序与因果

不同的上下文,顺序有不同的定义:在单主复制中,写入顺序解决冲突问题;事务的序列化是指事务表象某种顺序存在;实际上顺序保证包含多个方面:

  • 线性一致性,在线性⼀一致的系统中,操作是全序的:如果系统表现的就好像只有⼀一个数据副本,并且所有操作都是原 ⼦子性的
  • 因果性,意味着与因果关系保持⼀一致(consistent with causality) :如果快照包含答案,它也必须包含被回答的问题

总结:线性一致性强于因果一致性。

令人高兴的是,我们经常不需要线性一致性,只需要因果一致。如何确定是否因果一致性?它需要跟踪整个数据库中的因果依赖,⽽而不不仅仅是⼀一个键。可以推广版本向量量以解决此类问题

10.2.2 序列号顺序

由于因果关系的过于复杂, 我们可以使用来自逻辑时钟(logical clock)时间戳来追踪因果。单不同复制模式,不是那么简单:

  • 因果时间戳:单主模式中,版本号的追踪就是个典型的因果性。
  • 非因果时间戳:多主/无主模式中,1使用特定的位标示不同节点2将物理时钟附加到每个操作上3预先分配序列号区间。即可有效的区分不同节点的因果性。
  • 兰伯特时间戳:使兰伯特时间戳因果⼀一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为⽌止所⻅见到的最⼤大计数器器值,并在每个请求中包含这个最⼤大计数器器值。当⼀一个节点收到最⼤大计数器器值⼤大于⾃自身计数器器值的请求或响应时,它⽴立即将⾃自⼰己的计数器器设置为这个最⼤大值。

10.2.3 全序广播

全序广播指的是:如果吞吐量量超出单个主库的处理理能⼒力力,这种情况下如何扩展系统;以及,如果主库失效(“处理理节点宕机”),如何处理理故障切换。

10.3 分布式事务和共识

分布式的共识在很多场景下都很重要:

  • 领导选举,防止脑裂
  • 原子提交,我们重点关注这个

10.3.1 原子提交与二阶段提交

2PC是分布式环境下的跨节点的原子事务提交的算法,2PC的流程如下:

  • 调者开始阶段 1 :它发送⼀一个准备(prepare) 请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应:

  • 如果所有参与者都回答“是”,表示它们已经准备好提交,那么协调者在阶段 2 发出提交(commit) 请求,然后提交真正发⽣生。

  • 如果任意⼀一个参与者回复了了“否”,则协调者在阶段2 中向所有节点发送中⽌止(abort) 请求。

有点类似司仪问新郎新娘是否结婚,两个都答应才成功,否则就失败。

习题

  • 如何保持mysql和redis中数据的一致性?

    1 Cache Aside: 第一种的方式是失效 应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。命中:应用程序从cache中取数据,取到后返回。更新:先把数据存到数据库中,成功后,再让缓存失效;第二种Cache Aside的是更新DB时总是不直接触碰DB,而是通过代码。而代码做的显式更新DB,然后马上del掉redis里的数据。在下次取数据时,模式就恢复到了上一条说的方式。这也算是一种Cache Aside的变体。

    2 Read/Write Through Pattern :可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。实际上类比于MRSW的原子态缓存一致性协议,或者说可以理解为写直通模式的缓存管理方式。如果更新数据的话,先更新缓存中的数据,再同步更新DB中的数据。

    3 Write Behind Caching Pattern:可以类比写回策略,但是有个背景更新任务(“定时执行的代码” 或者 “被队列驱动的代码)读取db,把最新的数据塞给Redis。这种做法将Redis看作是“存储”。访问者不知道背后的实际数据源,只知道Redis是唯一可以取的数据的地方。当实际数据源更新时,背景更新任务来将数据更新到Redis。这时还是会存在Redis和实际数据源不一致的问题。如果是定时任务,最长的不一致时长就是更新任务的执行间隔;

第十一章 缓存设计

11.1 缓存的收益和成本

优点包括:

  • 加速读写: 因为缓存通常都是全内存的(例如Redis、 Memcache) , 而存储层通常读写性能不够强悍(例如MySQL) , 通过缓存的使用可以有效地加速读写, 优化用户体验。
  • 降低后端负载: 帮助后端减少访问量和复杂计算(例如很复杂的SQL语句) , 在很大程度降低了后端的负载。

缺点包括:

  • 数据不一致性: 缓存层和存储层的数据存在着一定时间窗口的不一致性, 时间窗口跟更新策略有关。
  • 代码维护成本: 加入缓存后, 需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本。
  • 运维成本: 以Redis Cluster为例, 加入后无形中增加了运维成本。

适合场景:

  • 开销大的复杂计算: 以MySQL为例子, 一些复杂的操作或者计算(例如大量联表操作、 一些分组计算) , 如果不加缓存, 不但无法满足高并发量, 同时也会给MySQL带来巨大的负担。
  • 加速请求响应: 即使查询单条后端数据足够快(例如select*from tablewhere id=) , 那么依然可以使用缓存, 以Redis为例子, 每秒可以完成数万次读写, 并且提供的批量操作可以优化整个IO链的响应时间。

11.2 缓存更新策略

总结就是:

  • 低一致性业务建议配置最大内存和淘汰策略的方式使用。
  • 高一致性业务可以结合使用超时剔除和主动更新, 这样即使主动更新出了问题, 也能保证数据过期时间后删除脏数据
  使用场景 一致性 维护成本
LRU/LRF/FIFO 缓存使用量超过最大值。 最差 不需要,只知道配置策略就成
超时删除 可以容忍一段时间内存储层和缓存层不一致 时间窗口内存在一致性 不高
主动更新 对一致性要求高,要求立刻更新数值 最高 高,开发者自己维护更新

11.3 缓存粒度控制

11.4 穿透优化

缓存穿透指的是缓存和存储都不命中。方法两种:

  • 缓存空对象。问题在于可能造成内存浪费和缓存层和存储层的数据不一致。
  • 布隆过滤器。提前拦截保护存储层

redis缓存穿透

11.5 无底洞优化

无底洞优化是因为网络和命令执行的延迟造成的。

redis无底洞

11.6 雪崩优化

缓存层不可用导致数据大量到达存储层。针对方法三种:

  • 保证缓存层服务高可用
  • 依赖隔离组建为后端限流降级
  • 提前演练降低问题

11.7 热点Key重建优化

常见的热点key重建优化的方法:

  • 互斥锁(mutex key) 此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可 。但是存在一定的隐患, 如果构建缓存过程出现问题或者时间较长, 可能会存在死锁和线程池阻塞的风险, 但是这种方法能够较好地降低后端存储负载, 并在一致性上做得比较好。
  • 永远不过期 令数据不过期,如果数据超时了就让线程去读新的值。其他线程还使用老值。这种方案由于没有设置真正的过期时间, 实际上已经不存在热点key产生的一系列危害, 但是会存在数据不一致的情况, 同时代码复杂度会增大。

热点KEY重建方法

习题

  • Redis雪崩了解么?缓存同时失效,同一时间大面积失效,那一瞬间Redis跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的,解决方法是:在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了。或者设置热点数据永远不过期,有更新操作就更新缓存就好了

  • 缓存穿透和击穿是什么?缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求;至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。

    解决方法:缓存穿透我会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return。或者使用布隆过滤器

总结下,三个维度针对这些问题:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免** MySQL** 被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

第十二章

第十三章

结尾

唉,尴尬

狗头的赞赏码.jpg