AEA Cheatsheet

AEA CheatsheetChapter 1. Miscellenous1.1 Cookie & Session1.2 Java Spring Bean 实例池1.3 数据库连接设置Chapter 2. 异步通信:Message Queue2.1 同步通信 versus 异步通信2.2 Kafuka2.2.1 Kafka 的高性能2.2.2 Kafka 的高可扩展性2.2.3 Kafka 的高可用性2.2.4 Kafka 的持久化 & 过期策略2.2.5 Kafka Consumer Group2.2.6 ZooKeeper & KRaft2.2.7 Kafka Serializers & Deserializers2.2.8 实践:如何使用 Kafka?Chapter 3. Web Socket3.1 Definitions3.2 Coding WebSocket3.3 WebSocket Encoder & Decoder3.4 STOMP over WebSocketChapter 4. SQL 数据库进阶:事务4.1 Definitions of Transaction4.2 Problems of Transaction for Database I: Isolation4.2.1 Conflicts4.2.2 Isolation Level4.2.3 Implementations of Isolation in DB I: Locks4.2.4 Implementations of Isolation in DB II: MVCC4.2.5 Implementations of Isolation in DB III: Scheduling4.2.6 补充:Design Pattern of Locks4.3 Problems of Transaction for Database II: Atomicity & Consistency4.3.1 Rollback & Faults Recovery4.3.2 Implementation of Atomicity & Consistency in DB: LogsA. 事务故障 / 系统崩溃补充:数据库日志的实现方式4.4 Problems of Distributed Transaction in Different Database (Atomic)4.5 Transaction in SpringChapter 5. SQL 数据库进阶:优化5.1 索引5.1.1 Overview5.1.2 Implementations5.1.3 MySQL 何时会使用索引5.1.4 列索引优化前缀索引聚簇索引全文索引稀疏索引 (Sparse Index)空间索引 (Spatial Index)多列索引(复合索引)内存索引Hash 索引降序索引5.2 主键优化5.3 外键优化5.4 数据库结构优化5.4.1 数据大小5.4.2 MySQL 数据类型5.5 数据库多表优化5.5.1 MySQL 如何管理 Open/Close Tables5.5.2 数据库数量限制调优数据库/数据库表的数量数据库表的大小数据库表的列数和行的大小5.6 InnoDB 表优化5.6.1 InnoDB 存储效率优化5.6.2 InnoDB 事务优化5.6.3 InnoDB 载入大量数据时优化5.6.4 InnoDB 查询优化5.6.5 InnoDB Disk I/O 优化5.6.6 InnoDB DDL 操作优化5.7 MEMORY 表优化5.8 Buffering and CachingChapter 6. 数据库备份与恢复6.1 备份和恢复的类型6.2 实践6.3 备份和恢复的策略Chapter 7. 数据库分区7.1 Types of Partitioning7.1.1 RANGE Partitioning7.1.2 LISTING Partitioning7.1.3 HASH Paritioning7.1.4 KEY Partitioning7.2 Subpartitioning7.3 How About NULL in Partitions?7.4 Partitioning Management7.5 分区与表 的交换Chapter 8. NoSQL8.1 Why we need it?8.2 MongoDB8.2.1 DefinitionsDocumentCollectionDatabase8.2.2 Indexing8.2.3 ShardingReasonsShard & Chunks8.3 Neo4J8.3.1 Definitions8.3.2 Data Model8.3.3 Storage Mechanism8.4 Log-Structured Database日志结构数据库中的读放大和写放大8.5 Vector Database8.5.1 Basic Concepts8.5.2 ANN Search Algorithms8.5.3 Similarity Measurement8.6 Timeseries DatabaseChapter 9. Concurrency Control9.1 Thread in Java9.1.1 Usage9.1.2 Synchronized Methods9.1.3 Reentrant Synchronization9.1.4 Atomic Access & Keyword volatile9.1.5 Dead Lock, Starvation, Live Lock9.1.7 Immutable Objects9.1.8 High Level Concurrency ObjectsLock ObjectsExecutorsConcurrent CollectionsAtomic VariablesVirtual ThreadsChapter 10. Memory Caching10.1 Background10.2 Memcached10.3 Distributed KV Store10.4 Redis10.4.1 为何需要?10.4.2 缓存读写策略10.4.3 缓存 Evict 策略10.4.4 缓存击穿 & 缓存雪崩Chapter 11. Full-text Searching11.1 Lucene11.1.1 Concepts11.1.2 Metrics11.1.3 Core Classes11.1.4 Searching Procedure11.1.5 Java Example11.1.6 Field 域类型11.1.7 维护索引11.1.8 Tokenism & Analyzers11.1.9 Advanced Search11.1.10 Similarity SortChapter 12. RESTful Web Service12.1 SOAP & WSDL12.2 RESTful Web Service12.2.1 Definitions12.2.2 Principles of REST12.2.3 Design Standards of RESTful API12.3 ConclusionChapter 13. Revisit: Microservices13.1 注册中心 & 微服务网关13.2 微服务雪崩13.3 微服务保护Chapter 14. HTAP14.1 Business Logic14.2 SolutionsChapter 15. Data Lake15.1 Concepts15.2 Evolution History15.3 Data Source 从哪来?Chapter 16. Cluster16.1 Why Cluster?16.2 Load Balance16.3 MySQL 集群和 Nginx Load Balance Policies16.4 Proxy & Reverse ProxyChapter 17. Cloud Computing & Edge Computing17.1 MapReduce17.2 Distributed File System17.3 Google BigTable: KV Store 鼻祖17.4 Summary: Components of Cloud OS17.5 Definitions of Edge ComputingChapter 18. GraphQL18.1 为什么需要 GraphQL?18.2 GraphQL Grammar18.3 GraphQL with Spring BootChapter 19. HadoopChapter 20. Spark20.1 Overview20.2 Spark Components20.3 Spark RDD (Resilient Distributed Dataset)20.3.1 Definitions20.3.2 RDD Operations20.3.3 RDD Partition20.3.4 RDD Dependencies20.4 Spark's Usage20.5 流式处理 & 批处理 & 流批一体架构Chapter 21. StormChapter 22. HDFS22.1 Definitions22.1.1 Design Assumptions: environments22.1.2 ArchitectureComparison between Improved NFS & HDFSInteraction ModelInterface22.2 Operations22.2.1 Reading a file in GFS22.2.2 Writing a file in GFS22.3 Features22.3.1 Safe Mode22.3.2 Rack Awareness22.3.3 Robustness & Fault ToleranceChapter 23. HBaseChapter 24. Hive24.1 Definitions & Meanings24.2 特性 & 与关系型数据库比较A. Scalability and PerformanceB. Data ModelC. Concurrency and Transaction (OLTP) SupportD. Data Processing TypeE. Data Storage24.3 再谈数据湖、数据仓库Chapter 25. Flink25.1 Scene25.2 The States of Flink25.3 Watermarks of Flink25.4 The Architecture of FlinkChapter 26. AI26.1 Full-Connected NN26.2 分类神经网络构建26.3 CNN26.4 TLP26.5 RNN & LSTM26.6 ChatGPT & Transformer

Chapter 1. Miscellenous

核心问题是什么呢? Http协议是一种无状态的协议!也就是说,每次请求都是独立的,服务器并不知道你是谁,你上次请求的信息是什么。

所以,怎么解决这个问题呢?

1.2 Java Spring Bean 实例池

"对象池"(Object Pool)是一种设计模式,它是一种用于管理和重用对象实例的机制,以提高性能和资源利用率的方式。对象池通常用于减少创建和销毁对象的开销,特别是在对象的创建成本较高或频繁创建和销毁对象可能导致性能下降的情况下。 在 Java 中,对象池通常是一个集合,用于存储和管理多个对象实例。当需要使用对象时,可以从对象池中获取一个可用的对象,而不是每次都创建新的对象。一旦使用完成,可以将对象返回到对象池中,以便稍后重用,而不是立即销毁它。

实例池的数量一定有上限的,不可能运行每一个用户上来都能创建一个对象,否则请求频率较高的时候内存直接爆炸。假设我们只能创建两个对象,这样内存就不会爆炸了。那是怎么服务于多个用户呢?

  1. 假设 A 用户来了,我们创建一个实例 A',然后 B 来了创建一个 B',现在实例池满了;

  2. C 来了之后,根据 LRU Algorithm,把 A' 从内存里面换出落盘,然后创建一个 C 的实例;

这个过程就称为 Java 实例的 swap in 和 swap out;使用类似页表换入换出的方式,实现服务超过当前实例数量的 clients;

因此,系统尽量要无状态的,减少或集中有状态的服务(否则需要创建多个 Bean 实例)。

两次调用不会相互影响(不会改变系统状态),或者说幂等的。

  1. 比如说我们要统计一个网站在线用户的数量,这样所有用户公用的一个域就是count:反应用户数量,这样就是无状态的(这个状态是公用的,不是每个用户都有一个);

  2. 如果是每个人来了以后,每个人的对象都不一样,那么就是有状态的。但是有状态的就需要针对每个用户单独存储,占用空间,所以尽可能的少或者避免。

在 Java Spring 中,管理 Bean 实例创建策略的注解是 @Scope(Bean 作用域),它可以管理 Bean 实例的生存周期以及可见性:

  1. Singleton(单例): 这是 Spring 默认的作用域。在单例作用域下,Spring IoC Container 对一个类型只创建一个 bean 实例,并在应用程序的整个生命周期内重用该实例。这意味着每次请求该 bean 时,都会返回相同的实例;

  2. Prototype(原型): 在原型作用域下,每次调用(invoke)涉及该 bean 的类型时,Spring IoC Container 都会创建一个新的 bean 实例。这意味着每次请求都会返回一个不同的实例;

  3. Request(请求): 这个作用域适用于 Web 应用程序,每次 HTTP 请求(不管是否来自同个客户端 / 会话)都会创建一个新的 bean 实例,每个请求之间的实例不共享(位于 AppContext);

  4. Session(会话): 类似于请求作用域,但在 HTTP 会话的整个生命周期内创建和维护一个 bean 实例。不同用户的会话之间的实例不共享(位于 AppContext);

  5. Application(应用级别):在 ServletContext 的整个生命周期内创建和维护一个 bean 实例(位于 AppContext);

  6. WebSocket:在 WebSocket 的整个声明周期内创建和维护一个 bean 实例(位于 AppContext);

  7. Custom(自定义): 您还可以定义自定义的作用域,以满足特定需求。要使用自定义作用域,您需要实现 Spring 的 org.springframework.beans.factory.config.Scope 接口,并将其配置到Spring容器中。

1.3 数据库连接设置

连接池(Connection Pool)是一种用于管理和重用数据库连接、网络连接或其他资源连接的技术,旨在提高应用程序性能和资源利用率。连接池通过维护一组已创建的连接实例,并在需要时分配这些连接,以减少创建和销毁连接的开销

这里的连接池本质是一个线程池,里面是一大堆线程。

以下是连接池的工作原理和好处:

工作原理:

  1. 初始化连接池: 在应用程序启动时,连接池会初始化一定数量的连接实例,这些连接可以立即使用。

  2. 连接分配: 当应用程序需要使用连接时,它向连接池请求一个连接。连接池会检查是否有可用的连接实例,如果有,则分配一个给应用程序。

  3. 连接使用: 应用程序使用连接执行数据库查询、网络通信或其他操作。

  4. 连接释放: 当应用程序完成连接的使用时,它将连接释放回连接池,而不是立即关闭连接。连接池会重新标记这个连接为可用状态。

假如要支持10万用户,需要在连接池里面配置多少数据库连接?

连接池的建议计算公式为:

connections=core count×2+effective spindle count

理论依据:

超出建议的连接数后,过多的连接数反而会导致很多无意义的 context switch,提升了 switch overhead,降低 CPU 资源利用率;

因此我们可以说,数据库连接数只与机器资源有关,与外部的连接情况无关

 

Chapter 2. 异步通信:Message Queue

2.1 同步通信 versus 异步通信

同步通信的缺陷:

异步通信的优势:

异步通信缺点:无论是消息还是异常,都通过异步通信,并且通信实现编码比较麻烦。

2.2 Kafuka

一个开源的分布式事件流处理中间件,以发布订阅模式进行事件处理。

Kafuka 基于日志(Log)管理消息。

这里的 “日志” 是一种仅追加(append-only)的数据结构,常用于捕获有序事件序列。

仅追加的好处是顺序写,充分利用磁盘特性,提升写的性能(Logged-Structure Merge Tree 就是利用这种特性的键值存储系统);

现在将 Kafuka 想象成一种消息队列,然后消费者利用记录的 offset 读取队列中的消息,能消费多少是多少。

2.2.1 Kafka 的高性能

那么 Kafuka 如何应对并发量更高的场景?

为了提升消息队列的吞吐量,可以将队列分类为多个队列,每个队列对应一类消息。“一类消息” 就被称为一个 topic;

生产者按 topic 向对应队列投递消息,消费者则针对性地按 topic 订阅,大大减小一个队列的压力;

但是如果这样做还不够呢?

Kafuka 将每个 topic 拆成多个 partition,多个消费同一个 topic 的消费者就可以消费不同的 partition,继续提升队列吞吐量;但是不同 partition 不保证消费消息的先后顺序(原理:利用 hash 随机拿到 topic 中的 partition 下标,负载均衡地投递和消费),但每个 partition 内是有序的;

其中,一般可以对单个消息指定 key,key 可以确保相同的 key 被放到同一个 partition 中,确保必要的数据是有序的;

详细消息体组成如下:

2.2.2 Kafka 的高可扩展性

单个机器的性能总归是有上限的,我们需要提供可扩展性(scale 到其他机器上)。

Kafuka 通过将同个 topic 中的不同 partition 分配到不同的服务节点上。不同 MQ 服务节点位于不同物理机器上,这些节点被称为 broker,多个 brokers 组成了 Kafuka cluster;

2.2.3 Kafka 的高可用性

我们解释了 Kafuka 实现应对高 QPS 情况、高可扩展性的要求,不过还没有保证高可用性。

设想一种情况,如果 Kafuka cluster 的某个 broker 挂掉了,如何保证消息不会丢失?

和大多数解决可用性的分布式系统一样,直接采用主从的 replicas 副本,主称 leader,从称 follower;leader 同时承担生产者写和消费者读的请求,follower 仅同步 leader 的消息作为备份,并且 leader 和 follower 保证异地亲和性(不在同一 broker 上确保集群容灾);

这样当一个 leader 挂掉后,从同一个 partition 的 followers 中重新选举出 leader 服务,然后在之后的一段时间内补充 replicas 数量;

2.2.4 Kafka 的持久化 & 过期策略

到目前为止,数据全部位于内存。但是需要考虑最坏情况:所有 broker 全部挂掉,这样难道数据就丢失了吗?

所以为了保证高可用性和数据安全,还需要将数据持久化到磁盘中。问题是磁盘的容量有限,持续写盘总有一天会爆满,因此我们需要指定消息的保留策略(retention policy)。常见的保留策略可以是:磁盘大小超过一定比例、消息放置超过一定时间。

2.2.5 Kafka Consumer Group

目前还有个问题,现在读消息队列的方式还是通过消息队列中的 offset 方式来读,但是如果多个消费者想访问一个 partition,那么它们将不得不共用一个 offset,首先是降低并发性能,其次是不灵活,没法满足不同的消息消费需求。

所以 Kafuka 的 partition 引入了消费者组(consumer group)的概念,让同一个消费者组内维护一套针对各个 partition 的 offset,实现更加灵活的消息订阅;

2.2.6 ZooKeeper & KRaft

在分布式系统中,有很多状态需要维护,比如说,之前我们提到多个 brokers 有几个挂了、哪些 partition 需要重新选举 leader、具体怎么选举,消费组的 offset 给谁维护,等等。这些状态一般可以用分布式协调组件完成。其中 Apache ZooKeeper 就是一种选择。

但是 ZooKeeper 的开销相当大,在一般小规模的分布式应用上很不划算,因此人们开发出轻量级的协调算法 KRaft,现已广泛应用在多种分布式系统中。

2.2.7 Kafka Serializers & Deserializers

和 RabbitMQ 一样,我们可以自定义序列化方式:

2.2.8 实践:如何使用 Kafka?

先介绍 Spring + Gradle 引入:

我们再介绍使用 KRaft 启动并管理 Kafuka 集群(而非 ZooKeeper):

下一步介绍如何在 Spring Boot 中编码使用 Kafka:

和 RabbitMQ 类似,在 Consumer 类中:

在 Producer 类中:

使用 KafkaTemplate<TopicType,DataType>.send(topic: String, data: Object) 来发送数据;

也可以使用 KafkaTemplate.send(topic, key, data),不过接收时需要 CustomerRecord<KeyType,ValType> 来处理;

 

Chapter 3. Web Socket

3.1 Definitions

一种全双工(full-duplex)的应用程序协议,基于 TCP 传输层协议(意味着需要 handshakes)。

它能帮助 Web Application 摆脱传统的 HTTP 请求-响应式通信模式,使通信方式更灵活。

其中 WebSocket URI scheme:ws://<host>:<port>/[path](SSL 加密 wss);

3.2 Coding WebSocket

  1. 创建一个 endpoint 类(继承于 Endpoint 类型),并向 ServerEndpointConfig 注册它:

  2. 实现 endpoint 的生命周期方法(如 onOpenonMessageonErroronClose);

    有两种方法可以实现,一种是原生实现,例如:

    或者是简洁的 annotation 实现(无需继承):

  3. 向 endpoint 中添加业务逻辑;

  4. 在前端添加相应的请求代码,将 endpoint 应用在一个 Web Application 中;

下面用一个例子说明:

假设有一个需要实时显示股票信息的 Web Application,我们先定义后端的 Endpoint:

然后定义 WebListener 以及用于定时触发的工具类 ReportBean(用于后端 Web Application 处理定时任务):

然后在前端完成对应任务:

我们可能还需在此基础上继续改进,例如定制消息的编解码过程,将这段业务逻辑从 WebSocket 连接中解耦出来,还能实现代码复用。这个时候就需要我们定义 WebSocket 的 encoder 和 decoder 了;

3.3 WebSocket Encoder & Decoder

实现 Encoder.Text<T>(文本消息)或 Encoder.Binary<T>(二进制消息)其中一个接口:

注:解码器就是 Decoder.Text<T>Decoder.Binary<T>

然后在 @ServerEndpoint(value = <path>, encoders = {}, decoders = {}) 中指明添加的编解码器,并使用 Session.getBasicRemote.sendObject() 发送;

接受端的 onMessage 函数参数除了 Session 以外,还需要适配解码器输出的数据类型。

3.4 STOMP over WebSocket

我们现在知道如何在 Web Application 中使用 WebSocket 了,但如果有更高阶的需求呢?例如,如果我想模仿发布-订阅模式对一类 clients 发送消息,或者说为 WebSocket 添加一个含 topic 的消息队列的高级包装,应该怎么办?可以考虑使用 STOMP 来完成这个需求。

后端以 Java Spring 为例,先引入依赖:

其内置了 STOMP(simple/stream text oriented message protocol 流式文本定向消息协议),这种协议基于 Web Socket 规范了简单的面向文本的消息传输的方案/机制。

然后对于前端需要引入:

加入项目的方法分几步:

  1. 配置 Web Socket Endpoint 以及 STOMP 的 broker 信息:

  2. 配置 Web Socket 消息处理方法(WebSocket 处理前端主动请求的方法体):

  3. 最后在前端编写代码,例如主动请求后端并通过订阅 STOMP 消息队列(这种订阅的方式是 STOMP 规定的),来接收后端传来的消息,并且通过设定的端口主动向后端发送 WebSocket 请求:

上面就是大致的接口和使用方法。不过这些接口还可以有一些使用经验技巧,能够完成一些特殊的需求。例如:

 

Chapter 4. SQL 数据库进阶:事务

4.1 Definitions of Transaction

4.2 Problems of Transaction for Database I: Isolation

考虑一个问题,有没有可能多个事务操作一个数据状态?

这个问题一般出现在数据库中,因此特别地,下面的问题都在讨论数据库的事务处理。

肯定是有的。这样就可能会发生数据读写的问题:

4.2.1 Conflicts

问题的严重性从上到下依次减弱。

在 MySQL 中,使用的默认事务引擎是 InnoDB,可以用于处理大量短期事务(尤其适合的扁平事务)。那么 MySQL 是如何解决以上的问题的呢?

注:扁平事务 Plain Transaction,这种事务通常的操作周期很短,比较少发生回滚事件。

4.2.2 Isolation Level

在事务引擎中存在 4 个事务隔离级别:

Isolation LevelDirty ReadUnrepeatable ReadPhantom Read
Read UncommittedYESYESYES
Read CommittedNOYESYES
Repeatable ReadNONOYES
SerializableNONONO

隔离级别的性能从上到下依次降低,但是处理问题的有效性依次上升。

数据库使用方应该根据业务场景进行 trade off,最后在数据库连接上说明配置;

4.2.3 Implementations of Isolation in DB I: Locks

现在我们考虑,数据库如何实现这些隔离级别

其实,数据库和普通程序在处理数据并发问题的思路是类似的:加锁。

我们记两个同时发生的事务 A、B,共同访问资源 R。现在有几种锁 / 措施可以选择:

于是利用上面的措施,我们尝试实现这些隔离级别:

4.2.4 Implementations of Isolation in DB II: MVCC

除了用锁实现隔离级别,以 MySQL 为首的数据库大多还使用 MVCC 的机制。

4.2.5 Implementations of Isolation in DB III: Scheduling

两个事务如果真的要同时操作一个数据,就一定需要报错吗?能否通过某些手段正确地完成双方的并行操作?

事实上真正的数据库想要实现隔离环境下的并发,不仅仅依赖于锁,还可以使用合适的调度策略来完成。

或者我们可以从另一个理论层面表达:“是否真正需要报错”。我们现在从理论层面探究:什么时候并发处理会出错(不可串行化调度)、什么时候可以通过适当的调度策略实现完美高效的并发(可串行化调度)。

为了更好地解释并发控制过程中,数据库对事务的处理流程,首先引入一个概念:调度。调度为事务的并发过程中,决定事务中每个操作的执行顺序

为了方便讨论,我们将所有操作抽象为读、写操作(使用读写序列描述事务执行过程)。例如更新:read(X) -> offline edit(X) -> write(X)

这样数据库调度问题,就是数据库决定哪一种读写序列是正确的。

考虑一次场景的两种调度:

第一种调度恰好是事务的串行执行,因此被称为 “串行调度”;第二种调度是某一种(没有锁措施)事务的并发执行,被称为一种 “并发调度”;

我们定义哪些事务能够通过适当的调度实现高效并发:

给定一个并发调度 S存在一个串行调度 S, 在任何数据库状态下,按照调度 S 和调度 S 执行后所产生的结果都是相同的,则称 S可串行化调度(serializable schedule)。

可串行化调度的数量十分巨大,且难以校验,数据库中一般通过找到可串行化调度的子集(充分条件),即找到能够提前确认是可串行调度的并发调度,进而提升调度效率;

再考虑一个例子:

这种事务场景下,我们找到了(存在)一种并发调度方案(4)使得它与串行调度方法的结果是一致的,因此我们认为这个(4)调度方案是可串行化的调度(就是说,这种并发的调度可以实现和串行化一样的效果)。

那么如何判断一个调度是否可串行化?如何实现可串行化调度(也就是用性能好的并行调度实现串行化的效果)?就像前面介绍的,我们一般通过充分条件找容易验证的方案:

其属性关系如下:

这里讨论一下 “冲突可串行化” 的判断方案:操作交换。我们定义:交换事务相邻两操作的顺序,如果不改变最终结果相同,则称这是一次等价交换(两个调度是等价的)。并且,如果调度 S 仅通过等价交换,就能变成串行调度,则称 S冲突可串行化调度

这相当于建立了一个等价类。就像你在解线性方程组,初等行变换不会改变最终结果,因此 “初等行变换” 是秩等价变换。

另外,我们在实际进行交换操作时,可以通过判断交换后是否会产生上面的 3 种冲突(写写冲突、写读冲突、读写冲突)。

根据交换等价以及 冲突可串行化调度的定义,我们直接有结论(通过等价类理解):若冲突可串行化调度 S 中调度的分别属于两事务 Ti,Tj 的某两个操作 Om,On 间存在冲突(不可等价交换),则在 S 等价的串行调度 S 中,Ti,Tj 中的 Om,On 一定和 STi,Tj 中的 Om,On 的顺序相同(保序性)。

因为这个保序性,由离散数学的理论,我们可以借助拓扑排序描述等价类间互不可等价交换的关系,称 “优先级图”。若调度 Si 与调度 Sj 是进行一次不等价交换后的两类调度,且 SiSj 中事务 Ti,Tj 交换顺序的冲突操作对 OmTi,OnTj 中,实际执行顺序 OmOn 之前,则构建一条 (Ti,Tj) 的有向边。最终会在两事务所有操作顺序的等价类间形成一张图。

数学上可以证明:这个图有环等价于 Ti,Tj 间的所有调度不是冲突可串行化调度。

这也是可串行化调度中,最容易代码实现、并验证的一种。因此多数数据库采用这种方式,结合 2PL(2-phase lock)来实现事务隔离机制。

 

另一个是视图可串行化调度,它的条件更宽松,因此也能识别更多的可串行化调度,但是它的计算难度更大。

这种情况有可能是 “盲写”。也就是某个事务的写直接覆盖了共享资源上一个写,期间不存在读操作。如下:

也就是这时 T1 -> T2 边可以被 “擦掉”。或者说,这三个事务本质上是可串行化的(视图可串行化)。

但识别这个事没有好的方法,只能穷举,因此我们说冲突可串行化更容易判断。

4.2.6 补充:Design Pattern of Locks

以上的隔离级别在数据库中自行帮我们实现。实际上,事务的锁还可以有其他的设计模式。

例如实现 Repeatable Read 级别时,数据库使用 exclusive write lock 锁定正在写的记录,这就是一种悲观锁。不过我们也可以使用乐观锁来实现这个目的。

数据库访问中,这几类锁(或者说设计模式)能充分利用现有的知识,提高数据的访问效率:

上面讨论的是在一条记录 / 一个表的层面的锁可以如何设计。如果我想跨表锁住整片对象呢?

举例:

网上书店下达订单的过程,适用于乐观离线锁。大部分人在浏览书籍,只有少部分人真正正在下订单,而且修改的是各自购物车 / 订单的数据,很少买同一本书,冲突的概率会更低;

记录用户访问次数的过程,适用于悲观离线锁。在没有缓存的情况下,用户每访问一次几乎都会触发一次写操作,如果用乐观锁,那么频繁的错误处理会降低事务的效率。

4.3 Problems of Transaction for Database II: Atomicity & Consistency

4.3.1 Rollback & Faults Recovery

讨论完数据库如何实现隔离性之后,我们再讨论一下数据库如何实现事务的原子性和持久性。或者说:数据库是如何实现事务回滚、故障恢复的。

故障类别:

补充:系统的高可用指标

  • 通用高可用指标:

    • 平均故障间隔时间 MTBF(Mean Time between Failures):系统在两相邻故障间隔期内正确工作的平均时间;

    • 平均恢复时间 MTTR(Mean Time to Repair):系统平均从故障中恢复需要的时间;

    • 平均损坏时间 MTTF(Mean Time to Failure):系统出现损坏的平均时间;

  • 数据库容灾指标:

    • 恢复点目标 RPO(Recovery Point Objective):业务系统在系统故障后所能容忍的数据丢失量;

    • 恢复时间目标 RTO(Recovery Time Objective):业务系统所能容忍的业务停止服务的最长时间;

4.3.2 Implementation of Atomicity & Consistency in DB: Logs

为了应对上面的问题,一般情况下数据库的应对机制概括如下:

问题类型 出现频率 对事务的影响 解决思路
无故障下事务回滚 原子性 单机数据库恢复
事务故障 较高 原子性
系统崩溃能重启 中等 原子性/持久性
系统崩溃不能重启 持久性 一主多备
磁盘故障 持久性 数据多副本
自然灾害 极低 持久性 异地多机恢复

 

我们详细讨论数据库针对上面的情况作出的具体解决方案:

A. 事务故障 / 系统崩溃

先定义一些概念:

脏页:内存页面已更新,磁盘页面未更新;

刷脏:将内存脏页刷到磁盘;

原因:可能是 操作系统中止/软件故障、死锁等等。

考虑下面的例子:

T1 和 T3 在崩溃前已经提交了事务。是否说明不会有问题了呢?

不一定。在 T1/T3 提交事务后,不一定会完成落盘(分布式系统更需要考虑这个情况)。如果崩溃时没有落盘,就需要 重做(redo,以保证持久性)

同样的道理,T2 如果在进行事务时,存在落盘操作(可选的策略),但是在中止(回滚)事务后、崩溃前没有来得及重新刷盘(恢复数据,以保证一致性),则也需要 重做

T4/T5 在崩溃时刻并没有结束事务,则考虑它们是否落盘,如果落盘了就一定需要回滚当前事务已做的部分(恢复到进行事务前的状态,以保证原子性);不落盘就皆大欢喜。

针对上面的策略,我们已经发现了有几种不同的刷盘策略了:

为了保证原子性,未结束事务可以采取两种刷盘方式:

为了保证持久性,已完成事务可以采取两种刷盘方式:

最终,“重做” 交给一个文件结构 redo log(重做日志)、“回滚” 交给另一个文件结构 undo log(回滚日志);

AspectsFORCE(事务提交强制刷盘)NO-FORCE(事务提交非强制刷盘)
NO-STEAL(执行期间不刷盘)❌ redo log & ❌ undo log✅ redo log & ❌ undo log
STEAL(执行期间可刷盘)❌ redo log & ✅ undo log✅ redo log & ✅ undo log

补充:数据库日志的实现方式

那么在众多策略中,MySQL 这种主流的数据库管理系统采用的是什么策略?

答案是 STEAL 配合 undo log + NO-FORCE 配合 redo log(刷盘时机:全部异步刷盘);

注:刷盘时机设计

  • 数据库关闭时,缓冲区中的所有脏页需要写回磁盘;

  • 缓冲区中的数据页面已经满了,如果需要继续读入数据页面,就必须将被替换的脏页写回磁盘;

  • 数据库会设置一个单独线程定时刷脏(全量 / 增量);

知识补充:日志、数据库日志、预写日志

日志是日志记录(log record)的序列,也是一种数据结构。所有的日志内容顺序写入磁盘,写入后不会修改(即不会随机写),能保证高效的写入效率。正因为这个特性,所以才有学者提出 LSM Tree(Log-Structured Merged Tree)作为一种高效键值存储数据结构;

数据库日志是数据库系统内一系列执行事件的记录,它与数据库事务是密切相关的,事务的执行过程会反映在日志中,数据库可以通过对日志的分析实现对事务的回滚(原子性)或重做(持久性);

预写日志(Write Ahead Log),即日志先于数据写入硬盘。这样可以确保在系统崩溃重启后有效恢复。

数据库日志的共性是:

按照我们上面对数据库日志的需求来讨论:

除了按照 undo/redo 的功能区分日志,为了进一步了解这些日志本身的实现,还需要从日志性质上分类并讨论:

比较一下 3 种日志:

其实无论从哪个方面分,数据库日志评判的 3 个重要性质,分别是:

我们再借助这 3 条特性来对比这 3 种日志:

Aspects解析速度日志量可重做性幂等性可逆性应用场景
物理日志redo log
逻辑日志undo log
物理逻辑日志较快undo log

因此逻辑日志不能用来作为 redo log,只能作为 undo log(回滚);

因此逻辑日志一般用于 redo log;不能用于 undo log;

另注:物理逻辑日志用于回滚时,特别是索引页面分裂,可通过页面前后指针来完成回滚;

具体实现方法

 

4.4 Problems of Distributed Transaction in Different Database (Atomic)

这里我们继续讨论数据库事务。上面关于事务隔离性、原子性和持久性的解决方案还算好懂,那么我们能说完全掌控事务了吗?

可惜没有。有一类重要并且比较困难的事务:分布式事务,它尤其难以保证 ACID。为什么?主要是因为我们上面的措施大多是都是针对单个数据库中执行的事务。

如果是在多个数据库(多个数据源)中的操作组成的事务,我们想保证这个事务的原子性就比较困难,因为事务的 Part A 在一个物理节点上完成,Part B 在另一个物理节点上故障了,那 Part A 是很难感知到另一个物理节点的 Part B 的故障的。

为了保证分布式事务的原子性,人们提出了 Two-Phase Commit 的机制。在分布式事务提交前,实现了 two-phase commit 的事务管理框架会做两件事:

  1. 事务管理框架会分别向多个数据源发送检查申请。只有所有数据源都为事务准备就绪,才会进入下一阶段;

  2. 事务管理框架接着逐一质询多个数据源 “事务是否可以提交”,并采取一票否决制:任何一个数据源需要回滚时,事务管理框架会决定全局事务全部回滚;

  3. 只有当事务管理框架在质询结束、确定决策,并且回复各个数据源,各个数据源才能继续进行提交或回滚操作;

注:Resource Manager 不仅仅指数据库。只要是支持事务的系统,在这里都可以称为 Resource Manager。例如邮件服务器、消息中间件等等。现在我们讨论的事务可以从数据库事务抽象为一般 Resource Manager 的事务。

好,这里考虑最坏情况。如果第 3 步发送决策信息时网络中断了:

假设 X,Y 能提交、Z 需要回滚,然后事务管理框架在向 XY 发送回滚信息时与 Y 的连接掉线了,导致 Y 事务超时。

注意,因为事务的隔离级别造成的性能消耗,所以一般情况下事务总是设定了超时时间。事务超过超时时间后会对提交和回滚操作进行猜测。

所以在 Y 超时后,如果 Y 猜测应该进行事务提交(有概率猜错),就会造成不一致性,并且事务原子性失效。

这种错误概率相当小(因为可以做重连等补救措施),但很难避免:如果你要更强的校验机制,那就会降低性能以及可用性。这也是分布式系统的 CAP 问题。

又由于日志中会体现超时的警告,因此可以后续人为地修复数据一致性。

所以总结:分布式事务的 Two-Phase Commit 协议不能避免 stage 2 网络或其他原因引发的启发式错误,进而导致的数据不一致性。分布式事务始终是有概率出错的。

Sidebar:在 Spring 框架中,Spring IoC Container 如果发现同一个事务内部操作了两个不同的数据源,则会被判定为一种分布式事务,进而使用 Two-Phase Commit 协议,不需要在业务逻辑层面进行适配。

另外,请注意区分 Two-Phase Lock 和 Two-Phase Commit。前者作为 Lock 可以是帮助实现数据库事务隔离性的一种方法;后者是分布式事务原子性的保证协议。

4.5 Transaction in Spring

总结:

Propagation Type当前线程存在事务当前线程不存在事务
REQUIRED_NEW❗️挂起,创建新的,结束后恢复✅ 创建新事务
REQUIRED✅ 加入当前事务✅ 创建新事务
SUPPORTS✅ 加入当前事务🚫 没有动作
MANDATORY✅ 加入当前事务❌ 抛出异常
NOT_SUPPORTED❗️挂起,不创建新的,结束后恢复🚫 没有动作
NEVER❌ 抛出异常🚫 没有动作

:新事务的起点为修饰的作用域起点,新事务的终点为修饰的作用域终点(在修饰方法时,新事务就仅仅位于当前方法体内);

注意,以上注解如果针对 Method,那么会在方法的生命周期两端进行(方法进入、方法退出)检查。

 

Chapter 5. SQL 数据库进阶:优化

优化思路如下:

  1. 数据库表的结构是否恰当?更具体地:

    • 每一列的数据类型(Number 使用 Integer 8 bytes 还是 String 11 bytes?Char 还是 Varchar?);

  2. 是否对正确的列建立合适的索引?

    • 对 title / author / price 哪种数据建立索引更有利于查询?

  3. 是否使用合适的存储引擎?

    • 例如 MySQL 中支持事务的引擎 InnoDB 和并发性能好的引擎 MyISAM;

  4. 每个表是否有合适的行格式?

    • 是否动态:还是以 Char/Varchar 为例,究竟是节约空间、牺牲查询性能,还是将表构建得更规则,空间换时间?

    • 是否进行压缩:数据库很多记录前面一部分列都相同,能不能就存一份?

  5. 使用什么锁机制?使用什么隔离级别?

  6. 如何在内存中为缓存配置合适大小?

硬件级别优化:

还有注意平衡可扩展性和性能:

5.1 索引

5.1.1 Overview

在 SQL 中,索引就是适当地排序数据,以加快搜索的速度。并且,索引是按照列(数据项)来建立的。

建立索引可以对不是 UNIQUE 的列。

MySQL 中,索引也是一个文件,如果是基于 B+ 树的引擎,那么一个索引的 block 中可能含有若干个树的结点。例如,B+ 树的 1 个结点放一个 block 中,那么读一个结点可以排除结点分叉数量的分支数,大大增大了查找速度。

有一些情况下不适合使用索引,比如从本身的性质上说,可能有如下问题:

适用索引的场景:

索引还需要管理员定期检查。索引的效率随表数据的增加或改变而变化。许多数据库管理员发现,过去创建的某个理想的索引经过几个月的数据处理后可能变得不再理想了。最好定期检查索引,并根据需要对索引进行调整。

5.1.2 Implementations

其实 MySQL 中不仅可以用 B/B+ 树来实现普通索引。例如:

5.1.3 MySQL 何时会使用索引

5.1.4 列索引优化

前缀索引

使用 index prefixes 在建立索引时更快。并且越短越好,因为一个 block 中可以盛放更多的记录。

可以只对一个列的前 N 个字符作为索引,尤其是对比较大的数据(BLOB / TEXT)建索引,这可以让索引文件更小一点。以 MySQL dialect 为例:

对于索引前缀的长度:

对于使用 REDUNDANT 或 COMPACT 行格式的 InnoDB 表,前缀长度最多可达 767 字节。对于使用动态或压缩行格式的 InnoDB 表,前缀长度限制为 3072 字节。对于 MyISAM 表,前缀长度限制为 1000 字节。

如果搜索词超过了索引前缀长度,索引将用于排除不匹配的行,并检查剩余行是否可能匹配。

聚簇索引

索引的顺序和数据存储的顺序完全一致的数据存储方式叫做聚簇索引(又称 “主键索引”)。在 InnoDB 中,如果采用聚簇索引,那么表数据文件本身就是按 B+Tree 组织的一个索引结构,而且是以每张表的主键构造一颗 B+ 树,同时叶子节点中存放的就是表的行记录数据,也将聚集索引的叶子节点称为数据页。

作为 InnoDB Table 的存储结构,只要有主键哪怕不建索引也是有一个主键索引。

相对的是次级索引(secondary index,或 “辅助键索引”),一般是用户建表时指定的非主键的索引。这种索引建立的新 B+ 树,叶结点只放这个索引的键(列)以及对应的主键。所以查询时需要拿着主键二次查询才能找到记录,这就是 “次级索引” 名字的由来。

聚簇索引性能好的原因是顺序读

因为索引记录的位置和存储的位置在顺序上是一样的,这样方便查找和读取。并且聚簇索引尤其适合按主键范围查找。例如:某个节点有多个子节点,从第一个节点到最后一个节点排出来顺序是依次递增的时候,这样显然在查找一个范围的时候就非常快,可以按照顺序来读取磁盘。

聚簇索引的缺点是,

  1. 聚簇索引的更新代价比较高,如果更新了行的聚簇索引列,就需要将数据移动到相应的位置。并且,在插入新记录或者更新时如果页满了,可能导致 “页分裂” 的问题;

  2. 插入速度严重依赖于插入顺序,按照主键进行插入的速度是加载数据到 InnoDB 中的最快方式。如果不是按照主键插入,最好在加载完成后使用 OPTIMIZE TABLE 命令重新组织一下表;

  3. 聚簇索引可能导致全表扫描速度变慢,因为可能需要加载物理上相隔较远的页到内存中(需要耗时的磁盘寻道操作)。

全文索引

MySQL 也支持全文索引的索引。MySQL 的 InnoDB 和 MyISAM 都支持针对 Char/Varchar/Text 列的索引。

只有当 entire column 和 column prefix indexing 不支持时才会使用。

MySQL 还针对单个 InnoDB 表的某些类型的 FULLTEXT 查询进行了优化。具有以下特征的查询尤其高效:

稀疏索引 (Sparse Index)

稀疏索引是相对于密集索引而言的,我们前面讨论的索引都是密集索引。

密集索引和稀疏索引的区分在与是否为每个索引键的值都建立索引,简单来说就是比如有一列的值 1、2、3、4、5、6、7,密集索引的做法是为这 7 个值都建立索引记录,那么就有 7 条索引记录;

而稀疏索引的做法是将这个 7 个值分组,1、2、3 和 4、5、6 和 7 分为不同的 3 组,取这三组中最小的索引键值作为索引记录中的索引值。

这两种索引都要通过剪枝来确定数据位置,不同的是密集索引,只需要找到叶结点就能确定准确的数据位置,而稀疏索引则需要先定位到目标结点后,从起始位置继续查找,以此定位具体的偏移量。

这两种不同的索引实现,一种建立了索引值与数据位置的 1:1 的关系,一种建立了索引值与数据位置 1:n 的关系。在大多数场景密集索引查询效率更高,在大多数场景稀疏索引占用空间更小。

总结与密集索引相比的优点:

缺点:对于精确查找的效率较低,因为需要扫描更多的数据块。

空间索引 (Spatial Index)

对于空间索引,MySQL 的 InnoDB 和 MyISAM 引擎都支持 R-Tree 数据结构来存放索引。

那么什么时候会用 R-Tree 来建立空间索引呢?答案是碰到高维数据的时候。举个例子:

从这个例子注意到,空间索引对高维数据的临近查询比较友好。

多列索引(复合索引)

对使用多列的索引,MySQL 支持组合索引(composite indexes)。例如:

多列索引的优势:

建立三个单独的索引比建立一个复合的索引要浪费空间,B+树的叶子节点存储要索引的值还有一个指向 硬盘的位置,而建立三个单独的索引,就需要三个树,叶子结点存储的同理,也就是说建立复合索引相 对来说更好。而且调整一棵树的速度比调整三棵树的效率显然要快的;

哪些查询 pattern 适合使用 multiple-column indexing?

例如这么建索引:

这种查询就能用上(只有一定先判断 last_namefirst_name):

而这种查询就没法用上:

也就是必须是 leftmost prefix of the indexes

值得注意的是,即便某次查找用上了复合索引,也不代表对每一列的筛选条件都能真正利用这个索引。这很抽象,我们用一个案例解释一下:

假设数据库中一个表包含:{ a: int(PK); b: int(PK); c: int }

然后 MySQL 对两个主键自动建立复合索引,我们记为 PRIMARY_IDX

我们手动对 b, c 建立复合索引(bc 后),记为 NEW_IDX,我们讨论:

内存索引

MySQL 默认使用 Hash Indexes,不过也支持 B-Tree;

Hash 索引

比较一下 Hash Indexes 和普通 B+ 数索引的异同:

降序索引

指定让索引存储的键值降序摆放(因为有建索引后 ORDER BY XXX DESC 的需求,不用 descending indexes 会造成性能 penalty);

倒序索引还可以支持 multiple-column indexes 的混合升降序的优化(表事先定义一些索引方法);

 

5.2 主键优化

有几条策略:

另外,有一种热门的问题,“究竟选 Java UUID 还是自增主键?哪个更好?

这里涉及 UUID 和自增主键的选取问题。

UUID 的优势:

UUID 的劣势:

自增主键的优势:

自增主键的劣势:

5.3 外键优化

如果一个表有很多列,而您要查询的列有很多不同的组合,那么将不常用的数据分割成单独的表,每个表只有几列,并通过复制主表中的数字 ID 列将它们与主表关联起来,可能会比较有效。

这样,每个小表都可以有一个主键,以便快速查找数据,而且可以使用 JOIN 操作只查询所需的列集。

根据数据的分布情况,查询可能会执行较少的 I/O,占用较少的缓存内存,因为相关列都集中在磁盘上。(为了最大限度地提高性能,查询会尽量少从磁盘读取数据块;只有几列的表可以在每个数据块中容纳更多行)。

5.4 数据库结构优化

Look for the most efficient way to organize your schemas, tables, and columns.

Minimize I/O, keep related items together, and plan ahead so that performance stays high as the data volume increases.

这首先从设计好的数据库开始,能让团队写出更高性能的代码,并让数据库可以承受 application 的迭代和修改的需求。

5.4.1 数据大小

从数据大小的角度,尽可能减小 table 在磁盘上占用大小。这能影响到一个事件内磁盘 I/O 的总体效率,并且总体内存占用少、索引占用也少。

5.4.2 MySQL 数据类型

5.5 数据库多表优化

5.5.1 MySQL 如何管理 Open/Close Tables

对于数据库中的很多表而言,MySQL 需要对这些表进行管理,让不同的 client(connections)都能获得比较快的速度。

有一种方案是和 OS 的 File 一样,在内存中管理 File Descriptor 和 Open File Table,后者可以在所有进程间复用。

在 MySQL 中,每个表可能在不同的 connections 中都打开过,因此当你执行 mysqladmin status 发现 open tables 数量多于实际表数时也不要疑惑。

由于 MySQL 是多线程程序,为了让多个 client 访问的 table 不至于冲突(尤其是事务冲突),会有 锁 和 MVCC 来管理修改操作。其中对每一个 session 而言都有一个独立的 open table,以防止数据依赖,达到空间换时间的效果。

对于 MyISAM 引擎的表来说,每个打开的数据文件还需要多存一个 file descriptor。

我们一般通过配置信息来影响数据库效率:

MySQL 会在如下场合关闭 opened table(必要时写回):

由上面的策略,我们可以侧面看出,如果 MySQL 的 table_open_cache 设置偏少,会出现以下现象(可以作为检查的方案):

检查 Open tables 数据非常大(远大于设定值),并且从数据库刚启动时就快速增长,而此时 FLUSH 操作并不多

同时,MyISAM 这样保留 open tables 还是有弊端的。主要是拿空间换时间,而且可能过犹不及。

Disadvantages:

5.5.2 数据库数量限制调优

数据库/数据库表的数量

MySQL 默认不限制数据库和数据库表的数量。

数据库表的大小

MySQL 最大表大小默认不限制,取决于文件系统单个文件大小。

一旦出现达到表最大大小的情况,MySQL 会抛出 full-table error,主要原因如下:

数据库表的列数和行的大小

MySQL 硬编码表的列数限制:一个表中不得超过 4096 个列。InnoDB 表额外限制不允许超过 1017 个列。

最终,最大表列数应该取决于:

值得注意的是,表的最大行大小不取决于表的最大大小

哪怕存储引擎能够放的下更大的行大小,实际上 MySQL 也限制了表的最大行的总大小为 65535 bytes

注:其中 BLOB 和 TEXT 这种大数据类型,MySQL 直接使用指针存储(指针指向另一个文件中,专门用于存储此类数据)

因此 BLOB 和 TEXT 总是仅占用一行 9~12 bytes 的大小。

在这个前提基础上,还有一些规则会影响实际使用中的表的最大行的大小:

举例说明:

上面这个表默认 row format 是 dynamic 的,一行长度为 66000 bytes,超过限制,因此无法创建成功;

下面这个表用 MyISAM 引擎也是一样不允许(MySQL 规定)。

由于 BLOB 和 TEXT 采用指针存储,因此这么定义又没问题(InnoDB 同理):

还需要注意,NOT NULL 这类限定词也会占用一行中的空间(通常是每个字段 1 byte)。

这种不行:

但这种(65533)可以:

 

5.6 InnoDB 表优化

5.6.1 InnoDB 存储效率优化

5.6.2 InnoDB 事务优化

对于 InnoDB 只读事务优化(InnoDB 自动对这些只读事务优化,我们只需要知道如何让 InnoDB 知道就行):

因此能作为只读事务时,就标记 READ ONLY,以方便优化。

5.6.3 InnoDB 载入大量数据时优化

5.6.4 InnoDB 查询优化

InnoDB 内部也会对单个只读查询的事务优化,见前文。

5.6.5 InnoDB Disk I/O 优化

注意:不应该首先考虑对 InnoDB 的 disk I/O 优化。当你很好地遵循了数据库设计原则、tuning operations 后,性能瓶颈仍然在 disk I/O,例如性能很慢但 CPU 占用小于 70%,可以再考虑下面的优化:

5.6.6 InnoDB DDL 操作优化

 

5.7 MEMORY 表优化

 

5.8 Buffering and Caching

InnoDB 维护了一个称为 “buffer pool” 的存储区域,来在内存中缓存数据和索引。

InnoDB 在管理 buffer pool 的策略上使用了 LRU Algorithm,确保热点数据驻留在内存中:

对 InnoDB Buffer Pool 进行优化:

 

Chapter 6. 数据库备份与恢复

回忆为什么需要备份数据库?

这些是我们之前提到的 Fault Recovery Mechanism(redo/undo)解决不了的。

如何备份?

6.1 备份和恢复的类型

物理备份:consist of raw copies of the directories and files that store database contents.

逻辑备份:save information represented as logical database structure (CREATE DATABASE, CREATE TABLE statements) and content (INSERT statements or delimited-text files).

 

在线备份:take place while the MySQL server is running so that the database information can be obtained from the server.

离线备份:take place while the server is stopped.

无论是备份时,还是恢复时,都有上述的讨论。

二者差别也被称为 “hot backups vs cold backups”;

Note

折中方案:a “warm” backup is one where the server remains running but locked against modifying data while you access database files externally(server 正在运行,但只允许读操作).

补充:

Customers of MySQL Enterprise Edition can use the MySQL Enterprise Backup product to do physical backups of entire instances or selected databases, tables, or both.

This product includes features for incremental and compressed backups.

And InnoDB tables are copied using a hot backup mechanism.

  • (Ideally, the InnoDB tables should represent a substantial majority of the data.)

Tables from other storage engines are copied using a warm backup mechanism.

 

本地备份:备份操作机器和 server 运行的机器是同一台;

远程备份:与本地备份相反,备份操作的机器和 server 运行的机器不是同一台(例如远程的 mysqldump);

但是:

 

快照备份(snapshot backup):使用特殊的文件系统,允许使用 logical copy(Copy On Write 延迟复制);

MySQL 自己不支持对文件系统快照,需要使用第三方解决方案。

全量备份:includes all data managed by a MySQL server at a given point in time.

增量备份:consists of the changes made to the data during a given time span (from one point in time to another).

通过 enable server 的 binary log(bin log)来让 MySQL 记录 data changes;

 

备份调度:对自动化备份至关重要;

备份压缩:减小备份带来的空间开销;

备份加密:提升对未授权访问 backed-up data 的安全防护;

这三个功能在 MySQL 社区版中都没有。

 

6.2 实践

使用 mysqldump 进行逻辑备份。对 InnoDB(支持事务)表,可以通过传入 --single-transaction 来实现无需加锁的 online backup.

如果进行物理备份:

如何使用 bin log 完成增量备份?

使用 FLUSH LOGmysqldump --flush-logs 向文件中写入自从上一次备份结束后更改的信息。

使用 SHOW BINARY LOGS 查看当前哪些增量日志文件;

使用 SHOW MASTER STATUS 查看当前正在向哪个增量日志文件写;

使用 mysqlbinlog <binlog> 读未加密的 bin log 内容;

使用 mysqlbinlog --read-from-remote-server --host=host_name --port=3306 --user=root --password --ssl-mode=required binlog_files 读加密的 bin log 内容;

使用主从备份(using replicas):

官方文档说:

如果要备份副本,无论选择哪种备份方法,都应在备份副本数据库时,备份其连接元数据存储库(connection metadata repository)和应用程序元数据存储库。

在恢复副本数据后,总是需要恢复这些元信息。例如如果 replica 正在复制 LOAD DATA 语句,则还应备份目录中存在的任何 SQL_LOAD-* 文件。replica 需要这些文件来恢复任何中断的 LOAD DATA 操作。

恢复被破坏的表:

对 MyISAM 表,只需要执行 REPAIR TABLE / myisamchk -r 就能解决 99.9% 的问题;

使用第三方工具做 snapshot 备份,例如 LVM、ZFS;

 

6.3 备份和恢复的策略

现在我们讨论更多的故障种类中,MySQL 现在可以进行的恢复机制。

对于 事务崩溃 / 系统崩溃 / 掉电,我们假设重启后磁盘没有问题,那么:MySQL 使用 redo log 和 undo log 找到 “已提交未刷盘事务” 重做刷盘、“未提交暂时刷盘事务” 撤销动作;

对于 文件系统崩溃 / 硬件(如磁盘)故障,我们假设重启后数据没法恢复,那么需要 主从备份/异地容灾的机制,并且重新格式化磁盘、安装新的文件系统,看看能否解决问题,并从其他物理结点恢复。

对于第二种情况,MySQL 没法完全帮我们恢复数据,因此我们需要一些策略来主动备份数据。大致的策略可以是 对数据库表周期性自动化备份。举个例子:

  1. 在星期天下午 13 点进行一次全量备份:

    在执行这个全量备份时,需要对所有 tables 上读锁。

  2. 为了方便和性能起见,全量备份虽然必要,但不应该频繁。因此接下来利用增量备份(需要启动时 --log-bin 或者通过配置 enable bin log)自动完成;

    能在数据库目录中看到 *-bin.0000xxx/index 的名称格式的文件,它们就是 bin log;

    为了节省空间,可以时不时清空这些 log(建议放到之前的全量文件中):

  3. 假设在星期三早上 8 点,数据库崩溃,那么我们进行下面的恢复过程:

    • 先恢复星期天创建的全量备份:

      现在数据库所有信息全部恢复到星期天下午 13 点的状态(如果期间做过 bin log 删除,那么可能更新一点);

    • 恢复从上次创建全量备份以后的增量备份文件,例如:

      如果是硬盘损坏丢失了部分的 bin log,则数据就真的丢了。但如果我们一开始指定的 bin log 使用异地容灾的思想,记录在其他物理节点上,那么数据的丢失就可以避免了。

总结,定期使用全量(mysqldump)和增量(FLUSH LOGS / mysqladmin flush-logs + enable bin log)备份,其中全量备份的频率小一点,并且可以考虑异地容灾。

 

Chapter 7. 数据库分区

分区不等于分表!

对 MySQL 引擎 InnoDB 和 NDB 都支持分区。

本质上是将表拆成不同粒度的集合,每一块可以单独处理以提升查询速度。

分区的方式被称为 “partitioning function”,可以是用户指定的,也可以内置 hash / 线性 hash、分区列表,等等。

分区方式可以是:

Horiztonal Partitioning:different rows of a table may be assigned to different physical partitions;

到 2024 年为止,MySQL 没有计划支持 Vertical Partitioning;

因为意义不大,真要这么做,不如拆成两张表。

分区的好处:

7.1 Types of Partitioning

Horizontal Partitioning 主要可以被分为 4 类:

7.1.1 RANGE Partitioning

RANGE partitioning: 根据记录列在某一范围内的值为依据分区;

举例:

如果希望按时间戳范围,可以用类似 UNIX_TIMESTAMP('2008-01-01 00:00:00') 的方式转换成整型,然后使用 UNIX_TIMESTAMP(<col>) 做 range partitioning;

其中,RANGE partitioning 如果按 Column 分区(就是加上 COLUMNS),不仅可以不用整型,而且还允许多列分区:

其中建议确定数据是线序的再这么建立,不然会对 MySQL 存储引擎的记录比较造成疑惑。

但如果按照 Column 分区,只接受 column 名称,而不能是表达式。好处就是能不用整型、接受多列

7.1.2 LISTING Partitioning

LIST partitioning: 与按范围分区类似,只是分区的选择基于与一组离散值之一相匹配的列。

考虑实际应用,对下面的雇员表:

按雇员的不同地区分区:

问题是,如果插入的一些记录不属于这些定义的列表内,那么就无法插入(报错)。如果:

因此,我们建议对某个固定的枚举量的列来这么做 LIST partitioning;

 

同样,LIST 分区也可以用 COLUMNS 来不使用整型。

其中,RANGE 和 LIST 分区,如果指定 COLUMNS(表示按 Column 分),则都支持不按非整型数据(如日期、字符串)类型来分区。例外如下:

7.1.3 HASH Paritioning

HASH partitioning: 使用这种类型的分区时,分区的选择基于用户定义的表达式(某种方式计算要插入到表中的行中的列值)返回的值。

用户可以指定在 MySQL 中产生非负整数的所有非负整数表达式来作为 hash 函数。

举例:

但需要考虑数据散列均衡,例如下面的例子:

这比较奇怪:按插入年份 hash,可能全年的雇员全部被放到一个分区中,短时间内没有很好的散列效果;

但是这么做的意图大多是为了加快查找,那么为何不将 YEAR 和 MONTH 一起 hash 呢?所以这种写法比较少。

在这种分区方法中,还支持另一种算法:Linear Hash。

它和普通散列的区别是,普通散列使用的只是散列函数值的模数,而 linear hash 采用 linear powers-of-two algorithm(线性二幂次算法);

优势是分布地更均匀(?)

算法如下:

使用如下:

7.1.4 KEY Partitioning

KEY partitioning: 这种类型的分区与 HASH 分区类似,只是只提供一个或多个要评估的列,MySQL 服务器提供自己的散列函数。

这些列可以包含整数以外的值(就像 RANGE 和 LIST 的 COLUMNS 修饰一样),因为无论列的数据类型如何,MySQL 提供的散列函数都能保证得到整数结果。

注意,KEY() 可以为空!MySQL 一般会默认主键。

举例:

其实本质上还是 hash partitioning,只不过这个 hash 函数交给 MySQL 实现

同样可以用 LINEAR KEY 来指定 “线性的”(分布更均匀,但计算量更大);

7.2 Subpartitioning

可以按照不同维度来切分表。

如果不需要子分区名字,也可以不指定:

7.3 How About NULL in Partitions?

如果分区后插入的依据列的值是 NULL 会出现什么?

我们知道,NULL 数据列在任意判断语句中都会返回 TRUE,因此:

7.4 Partitioning Management

在后期修改表的分区时,会比较耗时,因为不同分区确实可能在不同物理位置,需要物理上的转移过程。

完全修改、追加、覆盖。

re-organize 整个表的分区和完全修改一样非常耗时;

注意,对于 LIST partitioning 而言,ADD 追加的新 list 中,如果有重复元素则会报错。需要 REORGANIZE 来覆盖前面的定义。

而且需要考虑到分区(尤其是 RANGE/LIST)的实际含义(例如时间代表了特定历史阶段),那么应该按照这些含义来分,使得一次查询尽可能落在一个分区内,方便 Query Plan 进行优化。

7.5 分区与表 的交换

允许一个表的某一分区在另一张表中维护。

如果有些时候,需要一个表和另一个表中的某个分区的内容交换,可以使用:

但是要注意一些显然的条件:

注意,如果交换的表中有些记录不符合交换入的分区条件,则会报错。如果仍然希望继续插入,则在上面的语句中追加 WITHOUT VALIDATION

 

Chapter 8. NoSQL

8.1 Why we need it?

为什么需要 Not-Only SQL?

虽然 NoSQL 能保证大量非结构化数据的存储性能,但规范上来说不需要保证 ACID 事务特性(可能仅有最终一致性等 Weak Consistency Model)。

8.2 MongoDB

一个基于文档的(document-based)数据库。有几个概念:

8.2.1 Definitions

Document

一个 simple document 可以包括一个或多个键值对(类似 JSON),但不以 MySQL 中的 Blob 相同方式存储。

注意,我们也不能只用一个 collection,例如:

有几个问题:

Collection

命名规范:

使用 sub-collections:

Database

有个实践经验和建议:建议将单个应用的数据都放在一个 database 中;

有几个保留数据库:

8.2.2 Indexing

MongoDB 这类 NoSQL 如何建立索引?

8.2.3 Sharding

Reasons

考虑一种情况,当 MongoDB 的数据量很大的时候,我们需要将数据切片,并 scale out 到不同集群上去。

除此以外,还有哪些情况需要切片?

这个将数据切片(把一个 Collection 中的 Documents 拆成多个 Chunks 负载在不同物理节点上)的动作在 MongoDB 中被称为 “Sharding”;

Shard & Chunks

MongoDB Sharding 非常类似 MySQL 的 Partition 机制。但二者有区别:

它们二者的 Chuck/Partition 具体存在集群的哪里,都对用户透明。

例如在 MongoDB 中,多个 chunks 的查找工作是由 router 决定的,它会记住每个 chunk 存在集群的位置。

除了 router,MongoDB 还会为每个 shard server 分配合理数量的 chunks,确保每两个 shard server 存放 chunks 数量相差不超过 2(可以配置、可以关闭)。

为什么有些时候需要关闭?考虑数据的热点程度不一样,有些热点数据可能占用大小较小,但访问次数远高于其他数据,因此为了 CPU / Memory 负载均衡,这种情况下如果只是用 chunk 数量来决定分配显然是不合适的;

Shard Mechanism 如下:

主要关注 MongoDB 何时 split、sharding chunk 存放的位置,等等。

实际上当 MongoDB 插入一个数据后,生成的 _id 并不是自增键,而是类似 Java GUID 一样的随机标识数。

8.3 Neo4J

8.3.1 Definitions

一种图数据库,专门用来存储图(graph)数据结构。在这种需求下,使用传统的关系型数据库、基于文档的 NoSQL(例如 MongoDB)可能不再胜任。

例如,一个存在用户、订单、订单项、产品信息 4 个表的关系型数据库,如果想要找用户买过的产品详细信息 / 产品被哪些用户买过(查看实体 User 和实体 Product 间的相互关系),则至少需要进行 3 次 JOIN 操作。如果这种查询比较常见的话,性能问题就无法被忽视了。

再比如,如果想要存放两个人的 follow 关系。将用户信息、follow 信息放在两个表中,follow 信息记录 persion ID 和 friend ID(为了方便查找存在一倍数据冗余),

  • 那么查找某人的好友,至少需要 2 次 JOIN 操作(先查用户的所有 Friend IDs,然后找这些 Friend IDs 对应的用户);

  • 查找谁的好友是某人,同样至少需要 2 次 JOIN 操作(先查 follow 表中的所有 Friend ID 是指定用户的 Person IDs,再找这些 IDs 对应的用户);

  • 查找好友的好友?那至少需要 3 次 JOIN 操作!找 N 轮好友的好友就要 N+1 次 JOIN 操作,这在频繁查询的场景下是不可接受的(JOIN 笛卡尔积再操作,两个表很大时 O(M×N) 不可接受);

因此我们说 “关系型数据库中缺少表示相互关系的手段”。

那我们之前的基于文档的 NoSQL (MongoDB)能否解决问题?

我们以第一个例子为例,在 MongoDB 中可以有两种设计方法:一种,用户和订单、订单项全部嵌套在一起,放在一个 Document 中;另外一种用户和订单分开。这时如果要找用户购买的产品信息,就不需要 JOIN 了!

看起来这两种方法都成功解决了 JOIN 问题?那我如果想看产品被哪些用户买过呢?问题就又出现了!这次问题比关系型数据库还要难以解决(扫描全部用户的)。

那第二个例子呢?我们将朋友作为 ID 数组存储,会遇到相似的问题。找朋友关系变快了,但朋友的朋友还是很慢。

我们认识到,如果业务逻辑需要频繁使用实体间的相互关系时,无论是关系型数据库,还是基于文档的 NoSQL 都没法高效地解决问题。

因此人们提出了图数据库。

图数据结构主要存放一组结点和边(vertices & edges):

图数据库主要是对事务操作进行了优化,能保证事务完整性(fully ACID compliant)。

它的架构一般可以分为 underlying storage 和 processing engine 两个部分。其中处理引擎类似 MySQL 的 QueryPlan,underlying storage 则是指令执行系统和文件管理系统;

8.3.2 Data Model

以 Neo4J 为例,一个图的表示、查询、修改方法都可以使用 Cypher 语言(内置)描述。

8.3.3 Storage Mechanism

使用类似邻接表的方式存储:

8.4 Log-Structured Database

注意到有些情况下,关系型数据库、基于文档或者图的数据库都没法高效解决 “数据热点和创建时间相关” 的数据的查询操作。例如,如果 MySQL 想要在大量数据情况下查找最近几次插入的数据,这个时候由于表中没有与创建时间相关的信息,因此比较麻烦(按时间分区?如果有其他分区需要呢,例如按用户名?创建子分区得不偿失)。

这个时候就需要日志结构型数据库(准确地说,是日志型键值存储系统)。

例如 LSM Tree(适用于写多读少场景,不再赘述)。

优点:

缺点:

适用场景:

日志结构数据库中的读放大和写放大

8.5 Vector Database

8.5.1 Basic Concepts

Embeddings:对信息特征的数据形式的描述。例如一个 RGB 3 通道图片 224 x 224 x 3 数组被 flatten 后转为一个一维数组,这个一维数据可以是这个图片的 embedding;

如果是文本信息,我们可以有几种方法来 embedding:

这些可以被 embedding 的信息统称为 Content;

然后实际使用的方式如下:

其中,从 Content 中抽取 embeddings 的过程由 embedding model 完成(它可以是自己预训练出的,也可以是大语言模型等等)。

输出的向量数据再交给向量数据库存储和查找。

注意几个点:

最终我们会发现,创建索引后,向量数据库实际上只需要索引了(一般不需要原向量进行比较)!

8.5.2 ANN Search Algorithms

在维度不匹配时使用随机矩阵进行 random projection;

向量量化:

可能原始向量的维度较高,在资源缺乏或者其他场景下应用价值不高,因此我们需要进行量化:

  1. 将原始向量切分为若干块,每一小段可以做聚类处理(例如 123 213 132 可以划分到同一个 group 中);

  2. 一个 group 对应一个 code,这样向量的复杂度就会下降,虽然精度也会下降,但客观上提升了向量处理的性能;

局部相似性 hashing(Locality-sensitive hashing):

HNSW(Hierarchical Navigable Small World):

8.5.3 Similarity Measurement

余弦相似度、欧式距离、曼哈顿距离、点积……

8.6 Timeseries Database

InfluxDB 的概念:

带有 _ 开头的,都是系统保留的字段,也就是一定会有的一个列,反之都是用户自定义的字段。

_time:时间戳,数据对应的时间,因为可能同一个时间会接收到很多数据,所以很可能同一个时 间接收到很多数据,所以时间戳不能唯一的标识。时间戳非常准确,精确到纳秒级别。

_measurement:起一个名字,一个统称,这个表格在干啥。census 就是调查种群数量。这里我们 发现它是共享的,所以存储的时候会优化,只存储一次。

_field:存储的是 key,比如下表里面存储的就是某个物种的名字;和 _value 构成键值对。Field 可以 作为筛选的依据,得到一个 Field Set。

_value:存储的是 value,类型可以是 strings, floats, integers, or booleans,之所以不能是别的,是 因为如果是复杂的数据类型转换会耽误时间,效率降低,所以就只能存这些基础的数据类型。

Series:measurement, tag set, 和field key都相同的点集合。

Point:一个数据点,带有时间戳的数据点。E.g. 2019-08-18T00:00:00Z census ants 30 portland mullen

Bucket:存储桶,归属于一个组织,存储相对应的数据点集合。

Organization:组织,里面有一组用户,里面有若干个bucket。

注意到 InfluxDB 的一些注意事项:

Quiz:如何判断将数据存为 Field 还是 Tag?

在 InfluxDB 中,决定将数据存储为 field 还是 tag 主要取决于数据的查询模式和使用场景。对于服务器的 CPU 占用率和内存占用率,应该存储为 field,原因如下:

  1. CPU 和内存占用率是数值数据

  • field 用于存储数值类型的数据(如浮动点数或整数),并且这些数据通常是变化的;

  • CPU 占用率和内存占用率通常会随着时间变化,具有浮动的特性,因此它们应当存储在 field 中,方便进行聚合(如平均值、最大值等)、筛选和排序等操作。

  1. Tag 的作用

    • tag 主要用于存储具有高基数(不同值的数量非常多)的分类数据,通常是维度字段,作为索引使用。

  • CPU 占用率和内存占用率不是具有高基数的分类数据,而是时间序列的连续数值,因此它们不适合作为 tag 存储。tag 存储的字段通常用于区分不同的数据系列,比如服务器的名称、数据中心的 ID 或操作系统类型等。

  1. 查询性能

  • tag 字段可以用于快速的查询过滤,因为它们是索引的。而 field 不会被索引,查询时需要扫描所有的 field 数据。

  • 存储数值数据(如 CPU 和内存占用率)在 field 中,而不是在 tag 中,有助于避免因 tag 的高基数带来的查询性能问题。

结论:服务器的 CPU 占用率和内存占用率应该存储为 field,因为它们是数值数据,且不会用作分类维度进行查询过滤。而 tag 应用于具有分类属性的数据,能够高效地进行维度查询。

InfluxDB 存储引擎一般要写入几步:

InfluxDB 中的索引比关系型数据的索引(指定列、指定升降序、指定索引数据结构 等等)简单多了,只需要拿着 series key 索引即可。

另外,InfluxDB 有一套自己的文件系统管理方式。TSM 和 WAL 需要放到两个地方,保证异地容灾。

还有,InfluxDB 和 MongoDB 一样,使用 Shards(分裂方法、管理方法都类似)来管理大量文件。不过 InfluxDB 会根据过期时间范围构建 shard group;

shard group 可以 precreation、compaction 过期删除等动作;

InfluxDB compacts shards at regular intervals to compress time series data and optimize disk usage.

InfluxDB uses the following four compaction levels:

  • Level 1 (L1): InfluxDB flushes all newly written data held in an in-memory cache to disk.

  • Level 2 (L2): InfluxDB compacts up to eight L1-compacted files into one or more L2 files by combining multiple blocks containing the same series into fewer blocks in one or more new files.

  • Level 3 (L3): InfluxDB iterates over L2-compacted file blocks (over a certain size) and combines multiple blocks containing the same series into one block in a new file.

  • Level 4 (L4): Full compaction—InfluxDB iterates over L3-compacted file blocks and combines multiple blocks containing the same series into one block in a new file.

 

Chapter 9. Concurrency Control

9.1 Thread in Java

9.1.1 Usage

默认读者已经在 ICS 中学习了很详细的关于 thread 和 process 的知识,并且学会在 C/C++ 中使用线程和进程。

我们本节的目的是在 Java 中使用线程。两种方法:

注意,我们需要特别处理 InterruptedException

9.1.2 Synchronized Methods

Java 线程中的设定和 C/C++ 是类似的,它也会共享线程间的资源,不过 Java 没有指针,只是通过引用共享的。因此会遇到和 C/C++ 一样的问题。

就以共享静态变量为例,多线程同时操作共享静态变量会导致未定义的行为(race condition)。

在 C/C++ 中,一般会通过设立临界区(信号量 semaphore)或互斥锁(mutex)来锁定共享变量,确保同一时间只有一个/指定数量的线程可以访问。

在 Java 中,提供了一种修饰方法的关键字 synchronized,其作用是:

  1. 被该关键字修饰的方法,其所在的类型的任意一个对象,只能被一个线程调用被这个关键字修饰的方法。

    也就是说,相当于在这个方法的类上设一个互斥锁(被称为 intrinsic lock 固有锁,或者 monitor lock),把这个类中所有被 synchroized 修饰的方法锁住;

  2. 当一个线程退出了一个对象的 synchronized 方法,则会与这个对象其他的 synchronized 方法建立一个 happens-before relationship,以确保对象被使用的状态能被所有线程知道;

如果 synchronized 修饰在静态方法上,那么锁住的就是与 intrinsic lock 关联的 class 实例,而不是它的实例的实例。也就是对这个类中静态域的访问会被控制,需要与实例方法的 synchronized 区分开。

Java 甚至支持到 statement 细粒度的 synchronized

9.1.3 Reentrant Synchronization

Java 中提供了一类可重入锁,可以让获得锁的同一个线程多次访问临界资源:

9.1.4 Atomic Access & Keyword volatile

Java 中原生的单步原子访问操作包含:

Java 的原子访问操作可以:

那么,为什么 Java 既然有内置 Atomic Classes、锁、synchronous 关键字等等同步机制,为什么还需要 volatile 关键字?你只需要记住这些:

因此,只有在没有多线程同步的需求volatile 不保证同一线程对变量的一系列操作是原子的),但是又要保证对某一个变量的读和写是准确、及时的时候,可以使用 volatile 关键字,例如状态标志、简单的布尔变量等,这样不需要加锁,规避了死锁以及性能问题。

9.1.5 Dead Lock, Starvation, Live Lock

无论是死锁还是活锁,都是指多个线程之间因互相请求访问资源而导致程序无法继续执行的情况。

它们的不同点是:

对于死锁,它发生的情况是多个线程或进程在互相等待对方释放资源时,自己又不会主动释放自己占有的资源,导致程序永远无法继续的情况。

例如,假设一个程序的两个线程 A 和 B,A 先获得了一个资源 X 并给它上锁,B 获得了另一个资源 Y 也给它上了锁。但是接下来 B 需要资源 X 才能继续、A 又需要 Y 才能继续。所以二者相互等待对方释放资源锁,造成了死锁;

对于活锁,线程并不会阻塞在原地,而是反复地在释放资源和获取资源间横跳,这主要是因为程序有处理资源访问冲突的机制,但是两个存在活锁的线程相互处理访问冲突的时候又造成了访问冲突,也无法继续下去。

例如一个程序的线程 A 和 B,假设 A 先获得了一个资源 X 并给它上锁,B 获得了另一个资源 Y 也给它上了锁。A 想要获取资源 Y 的时候发现 B 占用了,于是 A 主动释放了资源 X 给 B,自己去获取资源 Y;但是此时 B 也主动释放了 Y 资源,去获取 X 资源,双方只是调换了资源持有的顺序,仍然无法继续执行。

线程饥饿是指,因为共享资源调度策略的问题,造成某些线程一直无法获得执行的机会而近乎停止执行,而另一些线程则一直占用共享资源不释放。

9.1.7 Immutable Objects

在很多实际情况下,不可变数据类型的好处:

不可变类和不可变对象(和 Python 思路相似)

不可变类的定义:一个类满足如下三个条件:

  • 类型中的每个数据域都是 私有的、常量的privatefinal);

  • 每个数据域都只能通过 getter 方法获取,不能有任何 setter 方法,并且没有“返回值是指向可变数据域的引用”的 getter 方法;

  • 必须存在公有构造函数,并且构造函数内初始化各个数据域(常量只能这么做);

  • Object 基类继承函数 equals 返回 true 当且仅当类中的每个数据域都相等;

  • Object 基类继承函数 hashCode 在类中的每个数据域都相等时,一定返回一样的值;

  • Object 基类继承函数 toString 最好包含 类名 和 每个数据域的名称和值;

因此如果有一个类数据域都私有、没有修改器方法,但有一个方法:返回内部一个可变数据域的引用(例如数组),则这个类也是可变类

9.1.8 High Level Concurrency Objects

Java 中包装了一些高级并发对象:

Lock Objects

Lock Objects:对常见的并发场景提供了简单的保护;

例如 ReentrantLock(可重入锁),

可以使用 tryLock() 获取锁、unlock() 释放锁。

和 Intrinsic Lock 机制很相似(包括持有规则、通过关联的 Condition 对象 notify/wait)相比更好的一点是 “允许 try”,也就是获取锁不成功的话还可以回到获取锁前的执行状态。

Executors

Executors:为启动、管理线程提供了更高级的 API,可以使用线程池机制为大规模并发应用提供支持;

将线程创建、管理的工作从应用业务逻辑中剥离。Java 中的 Executor 就是来包装这个的接口。

其中,有一些框架 / 库可以实现 Executor 接口。例如:

Executor 接口只有一个:

不需要自行创建 Thread,而是将 Runnable 类放到 Executor 中,让它帮你启动和管理。

类似地,还有 ExecutorService 接口,提供了比 Executor 更灵活的线程提交方式:

类似 Executor,不过它不仅仅允许你提交 Runnable 对象,还允许使用 Callable,并使用 Future<T> 来异步获取返回值,可以通过返回的 Future 对象了解、管理 Runnable/Callable 的执行状态:

ExecutorService 基础上继续包装 ScheduledExecutorService,允许对线程启动提供调度 delay 的时间:

其中,如果 Executor 底层采用 Thread Pools,则大多数用 fixed thread pool 的策略(同时最大只有指定的线程数正在执行)。使用 fixed thread pool 的好处是,使用它的应用可以 degraded gracefully;

Fork/Join 框架是针对 ExecutorService 接口的实现。它可以充分利用多处理器的优势,为那些可以拆成小块递归的任务设计,例如:

ForkJoinTask 子类(RecursiveTask 有返回值、RecursiveAction 无返回值)中定义这些任务。

Concurrent Collections

Concurrent Collections:更容易地管理大规模数据,减少 synchronization 次数;

Atomic Variables

Atomic Variables:针对变量粒度的同步机制,可以在一定程度上避免 data inconsistency;

All classes have get and set methods that work like reads and writes on volatile variables

Virtual Threads

Java 中是一类轻量级线程解决方案。让线程创建、调度、管理的开销最小化。

Virtual Threads 是 Java Thread 的实例,这与任何 OS thread 是相互独立的。

当 virtual threads 内部调用了阻塞的 I/O 操作后,会立即被 JVM 挂起;

virtual threads 有一个有限的 call stack,并且只能执行一个 HTTP client 请求 / JDBC 查询。这对一些异步的耗时任务比较合适,但是不适合 CPU intensive tasks;

所以 Virtual Threads 不是说会比普通线程更快,而是说比普通线程更具可扩展性(provide scale),这在高并发、每次请求处理耗时的服务器网络应用中能提升吞吐量。

 

ThreadLocalRandom:为多线程提供高效的伪随机数生成方案;

 

Chapter 10. Memory Caching

10.1 Background

为什么需要缓存?数据库里面有 buffer、ORM 映射里面也有 buffer 作为缓冲区,那为什么要缓存呢?

因为上面说的这两个缓存都不是开发者可以控制的,完全取决于它们自身的算法或逻辑,没法手动编码控制数据的 deactivate / update;

不仅仅是关系型数据库,像文件系统、网页静态数据、NoSQL 数据库数据等等也都要缓存(它们可能不是面向对象的结构化数据),而且有时还希望主动地提前进行缓存(例如双十一前将预计忙碌的页面事先缓存)。

因此对于读多的数据,比较好的方法就是先缓存起来,而且最好引入负责维护缓存的机制。

Java 网络应用中,在内存中的缓存一般可以有几种方法:

10.2 Memcached

Memcached:一个开源高性能的分布式内存对象缓存系统。

10.3 Distributed KV Store

那么 Memcached 如何设计来高效地利用内存?

首先 Memcached 是分布式的缓存 KVS,必然会存在多个 node 用于存放 cache data;

如果插入一个缓存的键值,那么如何决定缓存的位置?是否直接能用普通 hash 来决定?不行,因为如果分布式的 node servers 的数量改变,难不成还要改变 cache 的位置(涉及大量缓存数据迁移)?

所以我们需要一种算法,能计算出一个要缓存的键值对究竟放在哪台 server、哪个位置,并且在动态的环境中(例如 caching servers 的数量改变)仍然能高效地找到并取出之前缓存的数据。

一种解决方案是:一致性哈希(Consistent Hashing);consistent hashing 可以这么实现:

这样的方案能在分布式场景下尽可能减少缓存失效和变动的比例;

但这种方案仍然存在问题:当集群中的节点数量较少时,可能会出现节点在哈希空间中分布不平衡的问题(hash 环的倾斜和负载不均),甚至引发雪崩问题(最多数据的 A 故障,全转移给 B,然后 B 故障,并重复下去,造成整个分布式集群崩溃)。

解决 hash 环倾斜的问题的方案之一就是引入 “虚拟节点”(相当于给机器 hash 点创建 “软链接”),将 virtual nodes 和 real nodes 的映射关系记录在 Hash Ring 中;

上面解决方案的具体实现被称为 “Chord 算法”;

10.4 Redis

NoSQL(Not Only SQL)用于存储非结构化数据,不保证 ACID 事务特性(仅有最终一致性等 Weak Consistency Model)。

Redis(Remote Dictionary Server)就是一类基于内存的键值型 NoSQL,不保证数据一致性,但可以保证性能。

10.4.1 为何需要?

持久化在磁盘上的关系型数据库在存储关系数据、处理事务的多数场合下都非常得力,但免不了存在一些问题。

例如,在电商、文章档案等网页应用中,常常是读请求远多于写请求,即便 MySQL 有 cache buffer pool(InnoDB),在大量数据查询的场合下也会出现频繁的 cache evict,究其原因就是 cache working space 太小了。

人们发现只是读请求造成的 Disk I/O 是可以避免的——通过将数据托管到一个更大的内存空间(这段内存空间可以不连续、甚至可以不在单个物理节点上,由一个程序来管理它)中缓存起来,可以有效提升这些应用的处理效率和吞吐量。

结论 1:在庞大数据量的应用场景下,读多写少、数据时间局部性强的应用访问模式可以通过外置的内存缓冲区统一进行缓存,来提升整体性能和接口承载量。这就是 Redis 要解决的需求痛点。

10.4.2 缓存读写策略

结论 2:常用的缓存读写策略有很多种,不过依赖它们制定的缓存模式常见的有 3 种,分别是 旁路缓存、读写穿透、异步缓存

10.4.3 缓存 Evict 策略

不同内存型数据库的缓存淘汰策略不尽相同。下面以 Redis 为例介绍它的 cache evict 方案:

首先,Redis 正常不会主动 evict 数据项,而是先通过数据过期的方式腾出内存空间:

在此基础上,如果:

导致内存空间还是没法及时腾出,那么 Redis 就会采取主动 evict 的方案。

结论 3: Redis 对于缓存使用率过高的解决方案是 数据过期 + 主动 evict。其中数据过期依赖 “定时清理” 和 “惰性删除”,主动 evict 依赖 8 种 evict 策略

10.4.4 缓存击穿 & 缓存雪崩

结论 4:“一直查询不存在的数据” 或者 “某个热点数据被清理” 都会造成缓存击穿、“一批热点数据同时过期”、“内存数据库宕机” 都可能造成缓存雪崩。对应的解决方案是 “添加无效值缓存”、“延长热点数据 TTL”、“随机化批量缓存 TTL”,以及 “适当的缓存持久化”

 

Chapter 11. Full-text Searching

数据库中存放字符串一般使用 CHAR(N)VARCHAR(N),前者整齐,后者由 offset 的偏移量指定。

也会存为 TEXT / BLOB 此时文字专门存在此表以外的结构并使用指针指向它。

如果我们希望在数据库的所有字符串中找到含有单词 Great 的。这样我们会发现,在上面的结构中查找方法(例如使用 LIKE)相当慢(需要扫描所有记录,而且每个记录,都要匹配一次,而且结构不同,难以缓存,等等)。

那如果我们一开始就将关键词做成一张关系表bid - cid - keyword),将表中的关键字和它位于的关联对象映射起来呢?

但是问题是如果一个记录中的关键字很多,那么记录数就会爆炸。

于是人们想出了使用 Keyword 作为键(查找依据),将它们出现的位置作为其他字段(反向查询)。这就是全文搜索。

11.1 Lucene

11.1.1 Concepts

Apache Lucene 是一个高性能可扩展的信息提取(Information Retrieval 全文搜索)库。

Lucene 会从各种非结构化数据源收集数据并建立索引。

因此可以说高效、跨引用 Indexing 是搜索引擎的核心。你可以将 index 想象成能够提供对于存储的非结构化词语的快速随机访问。

最基本的查询方法是,顺序地从头到尾扫描所有文件,匹配给定的词或短语。

缺点明显:效率低下。

我们的目标就是:尽量消除耗时的顺序搜索的过程。

因此我们引入反向索引。针对字符串的多种属性(field)建立索引:

其中 subject 可以向量化,用来检查它与其他记录的相似性;

11.1.2 Metrics

其中衡量查找的指标:

11.1.3 Core Classes

Lucene 的核心索引类如下:

所以总的来说,整体过程就是:

11.1.4 Searching Procedure

与此同时,用户(可以是自然人或被调用的程序)可以对索引库进行查询:

 

11.1.5 Java Example

我们以代码为例讲述索引流程:

最终 Lucene 会在 /home/test/ 下生成若干索引文件,我们可以使用 Lucene 自带的图形化解析工具 Luke 查看。

而查询的代码过程:

11.1.6 Field 域类型

11.1.7 维护索引

11.1.8 Tokenism & Analyzers

11.1.10 Similarity Sort

Lucene 会在检索时实时地为搜索关键字和已有关键字的相关程度打分:

  1. 计算出词(Term)的权重;

  2. 根据词的权重值,计算文档相关度得分;

索引的最小单位是一个分词(Term,就是常说的 token)。Term 对文档的重要性称为权重,影响 Term 权重有两个因素:

我们可以人为地更改 Lucene 内部计算权重的逻辑,进而影响相关度的排序结果。例如,数据记录有一列是专门描述 feature 程度的因子(可能是广告因子),我希望在原始查询的基础上,考虑新的相关度为 原相关度 + 0.7 * feature 因子数,那么:

 

Chapter 12. RESTful Web Service

Web Service 可以提供跨硬件、操作系统、编程语言和应用程序实现真正互操作性的机会。

但是 IPC/RPC 好像也能跨硬件/OS/App 交互?所以提供 IPC/RPC 的应用能否称为一个 web service?

先进行一个说文解字:

如何实现一个 Web Service?人们想出了一些协议来实现上述对于 web service 的愿望。

12.1 SOAP & WSDL

SOAP: Simple Object Access Protocol,是一种基于 XML 格式的协议,它针对 API 纯文本化传递。

这种协议下,暴露接口的方式是借助一种 XML 格式文件,被称为 WSDL(Web Service Description Language,网络服务描述语言),它是一种 Web XML 的规范,相当于一个“菜单”,目的就是用通用格式(XML)告诉不同种类的 client,这个服务可以使用哪些方法。

注意,WSDL 只是一种描述方式,不和 SOAP 强绑定。

而 client 请求的消息就要遵循 SOAP 这个协议,向提供服务的 server 发送一个请求的 XML 信息。

这样做就能真正实现 “platform independent data exchange”;

注:WSDL 文件不需要自己写,通过工具生成。会根据接口、参数类型、参数顺序、接口返回的参数类型,全部用 XML 来表述出来;

Service Bindings:就是服务的具体实现形式,比如我们的服务可以支持 HTTP/SMTP/FTP 这 3 个协议,就说明这个 web service 有 3 个 bindings。

就是说 bindings 是服务在应用层协议上的具体实现。

举个例子:假设一个 Java 程序需要访问一个 C# 程序监听的服务。主要需要以下步骤:

  1. Java Client 将 C# Server 提供的 WSDL 文件取回(请求方法、文件位置由 SOAP 协议规定);

  2. Java Client 的 SOAP 框架根据 WSDL 文件生成一个本语言的 Method Interface;

  3. Java Client 的用户代码进行 Java 自己的 Method Invocation;

  4. Java SOAP 框架中的通信 Proxy Class 拦截这个 Java Method Invocation,将它按照 SOAP 规定翻译为 XML 文本,向 C# server 发送;

  5. C# Server 接收到 XML 信息被 C# 的 SOAP 框架的 Proxy Class 拦截,翻译为具体在 C# 中的调用逻辑。产生结果后用类似的方法转为 XML 消息传递给 Java Client;

  6. Java Client 侧的 SOAP 框架接收到 XML 回复,根据 SOAP 将其转为 Java 中的返回结果,然后 Java Client 代码从 Proxy Class 中得到最终结果;

SOAP 的优点和缺陷:

12.2 RESTful Web Service

12.2.1 Definitions

于是我们想,如果用 “数据驱动” 的传递方法会不会更加高效?

所谓的 “数据驱动”,就是针对数据资源的请求。我们把服务类型、参数传递,这类信息全部变成 “数据资源”,把调用方法蕴含在 HTTP 方法中。

例如获取订单信息,就是 GET 特定 path 下的某个文件,删除书籍也是 DELETE 特定 path 下的某个文件,修改库存是 POST 特定路径下的某个资源。

这样既可以完成跨语言提供服务的需求,又能规避 SOAP 传输不高效的问题(只要我们事先有个 API 文档描述服务和这个 “特定 path” 的对应关系)。

因为我们借助 HTTP 的请求头 + 请求体,可以直接传输纯数据而不需要借助 XML 文本描述它。

并且 server 永远基于数据,返回还是一种数据(所以大多数情况下可以是函数式的、stateless 的)。

这种面向数据资源的思路,就是 RESTful 的思想。

注:2000 年的时候,有个人在他的博士论文中提出了一套软件架构的设计风格(不是标准 / 协议,只是一组设计风格、共同约定),它主要用于 web service 的实现中。这个人就是 Roy Thomas Fielding。

基于这个风格设计的软件不仅仅可以实现上述 Web Service 的要求、规避 SOAP 的缺陷,还能使应用开发更简洁,更有层次,更易于实现缓存等机制(和 SOAP 相比)。

这个设计风格也被命名为 “表述性状态转移”(REpresentational State Transfer,REST)的架构风格。满足这个架构风格的接口设计就被称为 RESTful API。

所谓 “表述性”:

所谓 “状态”:指的是客户端的状态,客户端维护自己的状态,服务器是无状态的。

所谓 “状态转移”:指的是客户端的状态可以在调用接口的过程中发生转移;

12.2.2 Principles of REST

那么 REST 的这个 “风格” 的特征是什么?或者说它的 “共同约定” 是什么?

REST 架构的 6 个限制条件,又称为 RESTful 6 大原则:

当然,RESTful API 也是有缺陷的,例如过于重视资源的作用,导致一些与资源关系不大的场合(例如聊天服务器、通信服务器)如果使用 RESTful Web Service 则反而加重了开发负担。

12.2.3 Design Standards of RESTful API

如果想要自己设计一个 RESTful API,那么就要遵循以上的约定。

资源的 URI path 是需要认真考虑的,而 RESTful 对 path 的设计做了一些规范,通常一个 RESTful API 的 path 组成如下:

version:API 版本号,有些版本号放置在头信息中也可以,通过控制版本号有利于应用迭代; resources:资源,RESTful API 推荐用小写英文单词的复数形式; resource_id:资源的 id,访问或操作该资源;

当然,有时候可能资源级别较大,其下还可细分很多子资源也可以灵活设计 URL 的 path,例如:

此外,有时可能增删改查无法满足业务要求,可以在 URL 末尾加上 action,例如

其中 action 就是对资源的操作。

从大体样式了解 URL 路径组成之后,对于 RESTful API 的 URL 具体设计的规范如下:

  1. 不用大写字母,所有单词使用英文且小写;

  2. 连字符用中杠 "-" 而不用下杠 "_"

  3. 正确使用 "/" 表示层级关系,URL的层级不要过深,并且越靠前的层级应该相对越稳定;

  4. 结尾不要包含正斜杠分隔符 "/"

  5. URL中不出现动词,用请求方式表示动作;

  6. 资源表示用复数不要用单数;

  7. 不要使用文件扩展名;

此外,在 RESTful API 中,不同的 HTTP 请求方法有各自的含义,这里就展示 GET,POST,PUT,DELETE 几种请求 API 的设计与含义分析。针对不同操作,具体的含义如下:

在非 RESTful 风格的 API 中,我们通常使用 GET 请求和 POST 请求完成增删改查以及其他操作,查询和删除一般使用 GET 方式请求,更新和插入一般使用 POST 请求。从请求方式上无法知道 API 具体是干嘛的,所有在 URL 上都会有操作的动词来表示 API 进行的动作,例如:query,add,update,delete 等等。

而 RESTful 风格的 API 则要求在 URL 上都以名词的方式出现,从几种请求方式上就可以看出想要进行的操作,这点与非 RESTful 风格的 API 形成鲜明对比。

在谈及 GET,POST,PUT,DELETE 的时候,就必须提一下接口的安全性和幂等性,其中安全性是指方法不会修改资源状态,即读的为安全的,写的操作为非安全的。而幂等性的意思是操作一次和操作多次的最终效果相同,客户端重复调用也只返回同一个结果。

HTTP Method安全性幂等性解释
GET安全幂等读操作(安全),查询多次结果一致
POST非安全非幂等写操作(非安全),每次插入后与上次的结果不一样
PUT非安全幂等写操作(非安全),插入相同数据多次结果一致
DELETE非安全幂等写操作(非安全),删除相同数据多次结果一致

 

12.3 Conclusion

总而言之,使用 Web Service 有下述优缺点:

优点:

缺陷:

 

Chapter 13. Revisit: Microservices

如果应用中的所有接口都封装为 RESTful Web Service,那么这个系统是否是微服务呢?

微服务的定义:Microservices are a modern approach to software whereby application code is delivered in small, manageable pieces, independent of others.

微服务的优点:Their small scale and relative isolation can lead to many additional benefits, such as easier maintenance, improved productivity, greater fault tolerance, better business alignment, and more.

对比而言,单体架构:

  • 优点:架构简单、部署成本低(适用于开发功能相对简单、规模较小的项目);

  • 缺点:团队协作成本高,系统发布效率低、系统可用性差(软件可靠性差);

13.1 注册中心 & 微服务网关

在微服务架构中,规避微服务间直接远程调用缺陷的一种方式就是引入注册中心机制,借鉴发布-订阅模式,引入注册中心后的主要步骤如下:

  1. 服务发布者向注册中心注册服务信息(提供何种服务,即 topic,还有地址在哪里);

  2. 服务订阅者向注册中心订阅感兴趣的服务。此时注册中心可以将当前可用的发布者信息告诉订阅者;

  3. 订阅者(或者注册中心)可以进行负载均衡,选择一个发布者向其请求服务(远程调用)。

由于我们利用了发布-订阅模式,所以即便是已经获取服务列表的订阅者,也能从注册中心实时获取当前发布者的可用情况。

微服务网关:

13.2 微服务雪崩

在微服务相互调用中,服务提供者出现故障或阻塞。并且:

最终,调用链中的所有服务级联失败,导致整个集群故障。

解决微服务雪崩的思路主要如下:

  1. 尝试避免出现故障 / 阻塞;

    • 保证代码的健壮性;

    • 保证网络畅通;

    • 能应对较高的并发请求;

    • 微服务保护:保护服务提供方;

  2. 局部出现故障 / 阻塞后,及时做好预备方案(积极有效的错误处理);

    • 微服务保护:保护服务调用方;

13.3 微服务保护

为了应对微服务雪崩,我们有许多解决方案。其中,微服务保护是在业务逻辑代码层面以外的一种重要方案。

微服务保护有以下一些思路:

 

Chapter 14. HTAP

14.1 Business Logic

Hybrid Transactional/Analytical Processing,或者说 “混合事务分析处理”,它在正常业务应用的时候,分为两个部分:

在这两种应用场景下,我们发现 OLTP 适合按行存储数据记录,OLAP 适合按列存储数据记录;

因此需要看业务逻辑中哪种业务比较多,就用何种存储方法。

另外,如果行列都存,则需要花费两倍存储空间,会引入空间浪费、数据一致型问题。

能不能让一种 Database 做行列数据转换,让它既对于 OLTP 友好,又对 OLAP 友好?

一种思路是进行 Vertical Partition,给 Columns 分组,不同的组可以使用不同数据表示方式(行/列),这种方法在 MySQL 中不计划支持,但是 LSM Tree 可以支持:

再回过头看 LSM-Tree 的缺陷,由于写阻塞的存在,实际上对于 OLTP 的业务支持不足。

由于牺牲了读性能,所以对于 OLAP 的支持也不足。

14.2 Solutions

首先为了解决写阻塞对于 OLTP 的影响,我们可以在数据源和 LSM Tree 间设置收集分发层(Collectors,例如 Apache Flink);

收集分发层能完成两个作用:

其次,为了解决读性能对于 OLAP 的影响,考虑向 LSM-Tree 中添加列式存储,便于分类压缩、对内存友好。

总结一下:

其实在关系型数据库 MySQL、基于文档型的 NoSQL MongoDB 中也有类似的方案确保对于 OLAP 和 OLTP 的良好支持。

 

Chapter 15. Data Lake

15.1 Concepts

考虑一种 OLAP 的场景:从很多类型的数据库、很多类型的数据表(多元数据)中抽取出有价值的内容,按照同一种方式存储,方便分析。

主要进行 3 个步骤(ETL):Extract(多元数据抽取)、Transfer(单位转换、数据清洗)、Load(加载到内存);

处理的性能代价很大,可以先存起来,用到的时候再做(Lazy Process)

这就是数据湖(Data Lake)的理念。我们将未经处理的多元数据可以直接以原始的格式 存放到数据湖中(肯定需要包含 metadata 以供检索操作等等),需要时再取出处理。

A data lake is a centralized repository designed to store, process, and secure large amounts of structured, semistructured, and unstructured data(可以包含结构、非结构、半结构化数据). It can store data in its native format and process any variety of it, ignoring size limits.

因此,我们需要一个很强的数据接入能力(ingest!例如 flink),并且对上提供一体化的查询功能(用户一个 SQL 可以转换到底层不同类型的数据库的数据查询);

parquet 数据格式几乎所有主流数据库都支持。可以将底层数据都转换成 parquet 格式,方便上层数据处理程序分析。

data lake 的一个实现产品是 delta lake;

我们区分一下 data lake 和 data warehouse 的概念:

"Not Yet Determined":类似 OS 日志,可能这个数据只是记录一下,没有故障的话可能根本不会用上,不确定要怎么用,也就是现在用不上的数据。不会做 ETL 3 步;

数据湖数据仓库
能处理所有类型的数据,如结构化数据,非结构化数据,半结构化数据等,数据的类型依赖于数据源系统的原始数据格式。只能处理结构化数据进行处理,而且这些数据必须与数据仓库事先定义的模型吻合。
读取的时候设计 schema,存储原始原始数据写入时设计数据仓库,存储处理后的原始数据
拥有足够强的计算能力用于处理和分析所有类型的数据,分析后的数据会被存储起来供用户使用。处理结构化数据,将它们或者转化为多维数据,或者转换为报表,以满足后续的高级报表及数据分析需求。
数据湖通常包含更多的相关的信息,这些信息有很高概率会被访问,并且能够为企业挖掘新的运营需求。数据仓库通常用于存储和维护长期数据,因此数据可以按需访问。

数据湖与数据仓库的差别很明显。 然而,在企业中两者的作用是互补的,不应认为数据湖的出现是为了取代数据仓库,毕竟两者的作用是截然不同的

  1. 数据价值性:数仓中保存的都是结构化处理后的数据,而数据湖中可以保存原始数据也可以保存结构化处理后的数据,保证用户能获取到各个阶段的数据。因为数据的价值跟不同的业务和用户强相关,有可能对于 A 用户没有意义的数据,但是对于 B 用户来说意义巨大,所以都需要保存在数据湖中。

  2. 数据实时性:数据湖支持对实时和高速数据流执行 ETL 功能,这有助于将来自 IoT 设备的传感器数据与其他数据源一起融合到数据湖中。形象的来看,数据湖架构保证了多个数据源的集成,并且不限制 schema,保证了数据的精确度。数据湖可以满足实时分析的需要,同时也可以作为数据仓库满足批处理数据挖掘的需要。数据湖还为数据科学家从数据中发现更多的灵感提供了可能。

  3. 数据保真性:数据湖中对于业务系统中的数据都会存储一份“一模一样”的完整拷贝。与数据仓库不同的地方在于,数据湖中必须要保存一份原始数据,无论是数据格式、数据模式、数据内容都不应该被修改。在这方面,数据湖强调的是对于业务数据“原汁原味”的保存。同时,数据湖应该能够存储任意类型/格式的数据。

  4. 数据灵活性:数据湖提供灵活的,面向任务的数据绑定,不需要提前定义数据模型,"写入型 schema" 和"读取型 schema",其实本质上来讲是数据 schema 的设计发生在哪个阶段的问题。对于任何数据应用来说,其实 schema 的设计都是必不可少的,即使是 MongoDB 等一些强调“不需要固定 schema”的数据库,其最佳实践里依然建议记录尽量采用相同/相似的结构。

 

15.2 Evolution History

15.3 Data Source 从哪来?

边缘计算服务:物联网设备 -> 边缘计算节点 -> 云数据中心;

没有持久化服务带来的问题:设备移动性(云边融合的数据存储服务,数据存放在各个边缘结点中,很乱)、数据丢失问题;

怎么定位数据、怎么做并行分析和优化:

Chapter 16. Cluster

16.1 Why Cluster?

需要高性能、高可用性、高并发性;

有许多用户,可能在许多不同的地方(高性能)、 系统是长时间运行的,不能中断服务(可靠性)、 每秒处理大量事务、 用户数量和系统负载可能会增加、 代表可观的商业价值(比如支付系统是一个很关键的系统,因此为了保证支付的可靠性,很可能需要把常规的业务服务和支付服务分开,保证安全和可靠性)、 由多人操作和管理;

16.2 Load Balance

一般有 3 种方法:

补充:集群中的 session 维护

除了 load balancer 使用 IP-hash 的负载策略,还有哪些方法可以在集群中维护 session?

  • 用一台单独的服务器,例如 redis,存储用户的 session,这样的话所有的机器只需要在集群里面的 redis 服务器里面拿就可以找到用户的 session。此外,这种方法后端应用服务器重启之后,用户的 session 不会丢失;

  • 后端服务器间建立通信(不合适,因为会造成模块化的破坏);

16.3 MySQL 集群和 Nginx Load Balance Policies

16.4 Proxy & Reverse Proxy

反向代理,指的是代理外网用户的请求到内部的指定的服务器,并将数据返回给用户的一种方式;客户端不直接与后端服务器进行通信,而是与反向代理服务器进行通信,隐藏了后端服务器的 IP 地址。

反向代理的主要作用是提供负载均衡和高可用性。

 

Chapter 17. Cloud Computing & Edge Computing

常见云计算暴露的形式:

云计算的特点:

17.1 MapReduce

更详细信息参见 Chapter 19;

我们需要在云上进行作业调度,考虑简单的情况:批处理形式(回忆数据湖)。这个时候 MapReduce 应运而生;

注意到,reduce 开展工作之前,所有的统计工作是都必须完成的,否则就会出错。而不是说来一点处理一点,所有的数据在这里是成批流动的。

Map Reduce 中的 Map 定义是:把输入映射成输出,每个机器不会管别的输入,只会管自己的输入部分,把输入结果产生中间结果。Reduce 负责合并的部分,把所有的中间结果合并,得到最终的输出。

17.2 Distributed File System

17.3 Google BigTable: KV Store 鼻祖

针对极大量的、结构化 / 半结构化的数据的分布式存储系统。

17.4 Summary: Components of Cloud OS

现在我们总结一下,云上的操作系统应该具备的组件:

其中,Hadoop 框架就是对上述部分组件的开源实现。我们后面介绍。

17.5 Definitions of Edge Computing

定义:在网络的边缘,临近数据源的地方,来进行数据的处理,是一种优化云计算系统的方式。

与云计算的区别:

In the cloud computing paradigm, most of the computations happen in the cloud, which means data and requests are processed in the centralized cloud.

In the edge computing paradigm, not only data but also operations applied on the data should be cached at the edge.

主要考虑到的就是:

  1. 网络连接可能不是持续稳定的,你的数据未必都能发到云里面去,到达核心的云服务器;

  2. 通信的带宽可能是受限的,如果大家同时发送,可能很快都占满了;

通常情况下,由于:

我们会进行 cloud offloading(计算迁移:把计算从云端迁移到边缘端);但这会引入一些问题:

于是人们提出了云边融合计算(Cloud-Terminal Fusion Computing)的理念;存在 Cloud Servers、Mobile Edge Computing Servers(MEC)、Mobile Devices (Edge Devices);

因此我们可以考虑实际情况进行 Local Execution / Full offloading / Partial offloading;

Chapter 18. GraphQL

18.1 为什么需要 GraphQL?

在开发互联网应用时,必然会遇到 Client 和 Server 的通信的处理问题。我们之前介绍过 REST 方案,是 C/S、B/S 通信中最流行的选型。在 REST 中,所有概念都是在可以通过 URL 可访问的资源这个概念周围演化而来的,操作就是对这些资源的 CRUD(按 URL 设计 API);

但 REST 也有问题。在一个 RESTful 架构下,因为后端开发人员定义在各个 URL 的资源上返回的数据,而不是前端开发人员来提出数据需求,使得按需获取数据会非常困难。经常前端需要请求一个资源中所有的信息,即便只需要其中的一部分数据。这个问题被称之为过度获取(overfetching)。最恶劣的场景下,一个客户端应用不得不请求多个而不是一个资源,这通常会发起多个网络请求。这不仅会造成过度获取的问题,也会造成瀑布式的网络请求(waterfall network requests)。

于是,人们为了让客户端只请求其需要的数据——不多也不少,一切在客户端的主导下,一次只需要发起一个请求,因此 GraphQL 作为一种标准,也是 REST 的替代方案,应运而生。

也就是说,客户端可以按需描述需要的数据,让后端处理后返回。这可以极大地减少数据传输量。

但 GraphQL 也有问题:

  • 后端实现难以优化,前端过来的一次请求可能会导致后端的 n 次数据库查询,要保证接口效率就不得不设计一系列数据库缓存机制;

  • 其次依此还可以延伸出安全问题,即使只是一次请求也有可能可以爬取整个数据库,针对这个问题的安全防范措施不同于传统方式;

18.2 GraphQL Grammar

GraphQL 不与任何特定数据库或存储引擎绑定。重要的是在其上抽象的查询语义。

现在来看 GraphQL 的查询和更改语法。

18.3 GraphQL with Spring Boot

引入依赖:

例如查询书籍,在 *.qgls / *.graphqls 文件中先定义 SDL(或者说 schema):

注意,Spring Boot 会自动在 src/main/resources/graphql 目录下扫描 *.qgls / *.graphqls

当然我们可以在 application.yaml/properties 中配置 spring.graphql.schema.locations 来指定扫描位置;

再在前端定义查询语句:

对应的后端处理接口:

 

Chapter 19. Hadoop

和上一章说的一样,云上的操作系统应该具备的组件:分布式文件系统(如 GFS)、任务调度(如 MapReduce)、数据存储(BigTable/LSM Tree)、内存管理(Various);

社区中有一个开源的对于以上全套技术的开源实现,它就是 Hadoop,能够完成一些常用的分布式计算任务。Hadoop 包含几个模块:

MapReduce 1.0:

如何确定 Mapper 和 Reducer 的数量?

MapReduce 的实例如下:

我们注意到,MapReduce 重度依赖 Disk I/O 操作,会有性能问题。当然,人们也有在内存中操作的想法,这就是后来的 Spark 框架。

人们对 MapReduce 进行改进,于是出现了 MapReduce 2.0(YARN)。它的思路是将之前统一的 “资源管理”、“任务调度” 这两个任务分开:

这里一个 Application 可以是一个 Job,也可以是一个 computing graph(计算图,通常是 DAG);具体流程如下:

Decentralized Application Master 比 MapReduce 1.0 中的 JobTracker 的可用性更强;

这同时解决了性能瓶颈(RM 的工作负载并不高,不需要关系容器存活情况)以及可用性问题。

 

Chapter 20. Spark

20.1 Overview

内存中进行分布式计算和大规模数据分析的框架,性能较好(会比 Hadoop 性能好两三个数量级)。

但内存不够的话也会发送频繁 swap,造成性能下降;

换出策略:LRU(remove)/ Swap(to disk);

20.2 Spark Components

TermMeaning
ApplicationSpark 应用程序,由集群上的一个 Driver 节点和多个 Executor 节点组成。
Driver Program主应用程序,该进程运行应用的 main() 方法并且创建 SparkContext
Cluster Manager集群资源管理器,Spark 可以运行在多种集群管理器上,包括 Hadoop YARN、Apache Mesos、Standalone。
Worker Node执行计算任务的工作节点
Executor位于工作节点上的应用进程,负责执行计算任务并且将输出数据保存到内存或者磁盘中
Task被发送到 Executor 中的工作单元
Job多个并行执行的 Task,合起来就叫做一个 Job

执行过程:

  1. 用户程序创建 SparkContext 后,它会连接到集群资源管理器,集群资源管理器会为用户程序分配计算资源,并启动 Executor;

    • SparkContext 是 Spark 应用程序执行的入口,任何 Spark 应用程序最重要的一个步骤就是生成 SparkContext 对象。SparkContext 允许 Spark 应用程序通过资源管理器访问 Spark 集群;

  2. Driver 将计算程序划分为不同的执行阶段和多个 Task,之后将 Task 发送给 Executor;

  3. Executor 负责执行 Task,并将执行状态汇报给 Driver,同时也会将当前节点资源的使用情况汇报给集群资源管理器。

20.3 Spark RDD (Resilient Distributed Dataset)

20.3.1 Definitions

Spark 的基础数据结构,RDD 具有不可修改的特性(immutable,线程安全,集群中方便安全地并行使用)。

20.3.2 RDD Operations

RDD 操作分为两类:transformation 和 action。

Spark 中,所有的 transformation 操作都是 lazy 的,也就是说,只有当 action 操作发生时,才会触发 transformation 操作的执行

为什么每个 stage 中的 transformation 操作是 lazy 的?

我们刚才说 RDD 是不能修改的,是只读的。transformation 的结果是一个新的 RDD,那就是说你在执行这个 transformation 之后,一定会生成新的 RDD,它就会占内存。但是这个 RDD 什么时候被会被用到?不知道,因为你的 driver 最后一定是想得到一个值,那既然不知道,你为什么先让他把内存先占着(不需要先存空间占用很大的中间结果),你应该一直到最后他要做一个 action,要得到一个值的时候,这时候才要迫不得已往前去推(使用之前构建的 computing graph),他是经历了哪些 transformation 得到了,再把前面的 transformation 都做一遍,这样的话我们可以以最节省的方式去使用内存。

20.3.3 RDD Partition

在一个文件读入 Spark 并创建 RDD 后,会立即进行 partition(根据集群数量,方便并行运算)。

RDD 分区的好处有哪些?

要知道分区的好处,我们先了解一下 Spark 中的 Shuffle

在 Spark 中,某些特定的操作会触发 shuffle 动作(例如数据 JOIN)。shuffle 则作为一种针对数据的重分布操作,让数据得以在不同的 executors 间传递,获取必要的计算信息。

但是 shuffle 操作有个致命的问题是性能开销很大:包括大量 disk I/O、data serialization、network I/O(计算机系统中最慢的 3 巨头它都有)。

现在假设程序在内存中持有一个非常大的用户信息表 (UserIDUserInfo) ,其中 UserInfo 包含用户订阅的主题列表;另外有一个很小的表,记录过去 5 分钟里在网页上点击过链接的事件,键值对为 (UserID, LinkInfo),现在我们需要程序周期性地将这两个表合并、查询。

如果没有 partition,那么我们是不知道数据主键是如何分布在集群的各个机器上的(例如如果想找 A 开头的,已分区的数据可以很快确定在某些机器中,而未分区则无法做到)。而且 userData 需要周期性进行 hash 和 shuffle,即使可能并没有发生变化。

而如果 partition(hash 分区)了,那么 JOIN 可以利用这个信息,在 userData 不改变时直接取数据,无需再 shuffle,节省大量资源,如下左图:

20.3.4 RDD Dependencies

我们发现,在像上面的计算过程中,JOIN 操作依赖于 userDataevents 的数据,这就是 RDD 间的依赖关系。我们通过分析它们的依赖关系能够方便优化。

窄依赖的好处:

有了宽依赖和窄依赖,我们可以在此基础上建立 code stage(即:划分 stage 的依据是计算图中的宽依赖和窄依赖):

如上图的例子,实线方框表示 RDD,实心矩形表示分区(黑色表示该分区被缓存);

这样的好处就是只有在 F 要被使用的那一刻,而且需要被宽依赖的使用的那一刻,我们才去创建f。在此之前无论是 C/D/E/F 四个当中哪一个都在内存里是不存在,因此节省了空间;

20.4 Spark's Usage

 

20.5 流式处理 & 批处理 & 流批一体架构

流处理:对数据进行实时处理的方式,数据会以流的形式不断地产生和处理。流处理可以快速响应数据的变化,及时地进行数据处理和分析,适用于需要实时处理数据的场景。

优点

  1. 实时性:数据在产生的时候就立即被处理,能及时反馈结果。

  2. 高效性:不间断接受新数据并进行处理,因此可以更加高效利用硬件资源。

缺点

  1. 数据突发性:因为流式数据具有不可预测性,可能会突然出现突发的高峰,会导致系统压力急剧增加。

  2. 处理复杂度高:实时处理可能需要更高的处理能力和更复杂的算法。

批处理:对数据进行离线处理的方式,数据会按照一定的时间间隔或者数据量进行批量处理。批处理可以对大量数据进行高效处理和分析,适用于需要对历史数据进行分析和挖掘的场景。

优点

  1. 处理复杂度低:通常不需要考虑数据的顺序、时间窗口等因素。

  2. 容错性高:数据多批次集中处理,通常一条数据的失败不会影响后续数据的处理,也可以采用多种容错机制来确保任务正确完成。

缺点

  1. 响应速度慢:由于批处理是周期性执行,不能及时响应数据变化。

  2. 处理结果滞后:由于批处理是周期性执行,在某些场景下可能会出现数据结果滞后的情况。

以前很多系统的架构都是采用的 Lambda 架构,它将所有的数据分成了三个层次:批处理层、服务层和速率层,每个层次都有自己的功能和目的。

这样可以方便完成 HTAP(混合事务处理)的业务逻辑。但是这导致了一些问题:

  1. 资源浪费:一般来说,白天是流计算的高峰期,此时需要更多的计算资源,相对来说,批计算就没有严格的限制,可以选择凌晨或者白天任意时刻,但是,流计算和批计算的资源无法进行混合调度,无法对资源进行错峰使用,这就会导致资源的浪费。

  2. 成本高:流计算和批计算使用的是不同的技术,意味着需要维护两套代码,不论是学习成本还是维护成本都会更高。

  3. 数据一致性:两套平台都是不一样的,可能会导致数据不一致的问题。

 

Chapter 21. Storm

Storm 就是一类比较经典的流式处理框架。

它的基本框架如下:

举例:"Twitter 分析"的输入来自 Twitter 流 API。 Spout 将使用 Twitter 流 API 读取用户的推文,并以元组流的形式输出。 来自 spout 的单个元组将具有 twitter 用户名和单个 tweet 作为逗号分隔值。 然后,这组元组将被转发到 Bolt,Bolt 会将推文拆分为单个单词,计算字数,并将信息保存到配置的数据源中。 现在,我们可以通过查询数据源轻松获得结果;

也就是说,微观上是批处理,宏观上是流处理;

 

Chapter 22. HDFS

学习类比 GFS。

22.1 Definitions

22.1.1 Design Assumptions: environments

HDFS 不适合应用于:

22.1.2 Architecture

注意 GFS 只是把 NameNode 的称呼换成 Master,DataNode 换成 chunk server;

GFS 还有更多的机制:

Comparison between Improved NFS & HDFS

Interaction Model

Interface

 

22.2 Operations

22.2.1 Reading a file in GFS

  1. Client 联系 NameNode、获取文件的 Metadata(哪些 chunks/blocks?);

  2. 获取文件的每个 blocks/chunks 的具体位置(多个 replicas 的位置可选);

  3. 从 DataNode 获取任意一个中获取 chunk/block;

22.2.2 Writing a file in GFS

特点:

设计目标:

为了保证一致性、消除并发写冲突,需要在同一组 replicas 中选一个 DataNode 作为 a single primary(leader)来统一协调写操作。有两个问题:

NameNode 如何选择一个 primary?这个 primary 不能持久,因为每台机器都有可能故障,而是定期随机地在每组 replicas 中通过给予 “a chunk lease”(租约)来选 primary,在这些 replicas 中只有这个 DataNode 才能修改 chunk(并且心跳连接);

允许允许续租机制;

更改 primary 后,NameNode 会通过更新(增加)chunk version 并通知 replicas 来完成。

因此写操作分为以下几个阶段:

总之,写操作对 atomic append 非常友好:Append 写法总是能保证最终一致性(哪个行数多哪个新,不需要考虑覆盖问题),因此 HDFS 的 weak consistency model 是有效的;

 

22.3 Features

22.3.1 Safe Mode

所有 DataNode 都会向 NameNode 后发送 Blocks Report。我们定义一份数据(block/chunk)的 replicas 满足最小的要求时称为 “safely replicated”;

我们可以配置 HDFS 的 NameNode,在启动后/热插拔 DataNode 后,只有当至少一定比例的 DataNode 的 chunks 都是 safely replicated,才会退出这个模式,向外提供服务(一般耗时 30s 左右)。

之前 block 第一次被写的时候不是已经写了满足要求的副本数目了,为什么现在启动了还要做这个检查?

因为这次启动不一定启动了之前的所有 DataNode;

也正因如此,HDFS 支持对于 DataNode 的 “热插拔”,一个 DataNode 加入和离开集群时,不需要人为额外的操作,只需要等待 NameNode 接收 Blocks Report 后统筹 Replicas 就行。

22.3.2 Rack Awareness

之前我们介绍到 HDFS 使用机架感知作为依据来存放 replicas,它的原理是什么?

它利用了网络拓扑结构间的网络距离

在海量数据处理中,主要限制原因之一是节点之间数据的传输速率,即带宽。因此,将两个节点之间的带宽作为两个节点之间的距离的衡量标准。

Hadoop 为此采用了一个简单的方法:把网络看作一棵树,两个节点之间的距离是他们到最近共同祖先的距离总和。该树中的层次是没有预先设定的, 但是相对与数据中心,机架和正在运行的节点,通常可以设定等级。具体想法是针对以下每个常见,可用带宽依次递减:

  1. 同一节点上的进程;

  2. 同一机架上的不同节点;

  3. 同一数据中心中不同机架上的节点;

  4. 不同数据中心的节点。

因此 HDFS 在创建 replicas 时,可以指定 replicas 存放的策略。我们以 replica=3 的情况为例:

22.3.3 Robustness & Fault Tolerance

 

Chapter 23. HBase

其实是 Google Big Table 的开源实现版本。就像 HDFS 和 GFS 的关系。

HBase 是面向列存的、分布式的数据存储系统(基于 HDFS);

关系型数据库(如 MySQL)一般不做垂直分区。

关系型做分布式的问题是,有两个巨大的表,如果我们没有外键关联,那无所谓;但是如果存在了外键关联,如何划分就是一个大问题——否则如果切分得不好,你在做 JOIN 的时候,某台机器可能要和很多其他机器通信,就很慢。

总体来说,HBase 的表在概念上长这样:

物理存储上长这样:

Q&A:

Chapter 24. Hive

Hive 是一个建立在 Hadoop 之上的数据仓库工具,它允许用户使用类似 SQL 的语言(称为 HiveQL)进行数据查询和分析。

HiveQL 遵循 SQL 标准,但支持不全,并且和 Hibernate SQL 的语法不一样。

24.1 Definitions & Meanings

Hive 的定位:Hive 是一个数据仓库工具,它的设计目标是简化 Hadoop 上的数据处理和分析,可以将结构化的数据文件映射为一张数据库表,并提供 HiveSQL 查询功能;

其本质是将 SQL 转换为 MapReduce/Spark 的任务进行运算,底层由 HDFS 来提供数据的存储,说白了 Hive 可以理解为一个将 SQL 转换 MapReduce/Spark 的任务的工具;

使用 Hive 的原因:学习 MapReduce 的成本比较高、项目周期要求太短、MapReduce 如果要实现复杂的查询逻辑开发的难度是比较大的(太底层了)。 而如果使用 Hive,简单的语句能够提高快速开发的能力;

24.2 特性 & 与关系型数据库比较

对应的问题,例如:大数据场景下,都是用 SQL 存,那为什么不直接用 MySQL 这种关系型数据库呢?

在大数据场景中,HiveMySQL 等关系型数据库用于不同的目的,它们的优势在不同的情况下发挥得淋漓尽致。下面将详细介绍在处理大规模数据时,为什么 Hive 往往比 MySQL 更受青睐。

A. Scalability and Performance

Hive 建立在 Hadoop 之上,可在多台机器上扩展。它允许您通过将数据分布到机器集群中来存储和处理 PB 级的数据

与关系型数据库 MySQL 对比:MySQL 通常是为事务性工作负载设计的,最适合较小的数据集(尽管它可以通过分片和复制进行扩展)。它不是为高效处理大规模数据而设计的,不方便扩容(GB 级别,而且有表大小限制,再大通常就需要分表,这也是 MongoDB 出现的原因之一)。

总结一下 Hive 的主要优势: Hive 可以处理 MySQL 难以管理的海量数据集,而 MySQL 在大数据处理的水平扩展性(Horizontal Scalability)方面存在固有的局限性。

B. Data Model

Hive 使用 schema-on-read 模型,即在查询数据时应用模式。这种灵活性允许处理非结构化或半结构化数据(如 JSON、Parquet、Avro)。

MySQL 使用的是 schema-on-write 模型,即数据在插入数据库之前必须符合严格的模式。

Hive 的主要优势: Hive 可以处理更多样、更复杂的数据类型,尤其是大数据应用中使用的数据类型,这些数据可能是非结构化或半结构化的(日志、传感器数据等)。

C. Concurrency and Transaction (OLTP) Support

MySQL 专为事务性工作负载(OLTP)设计,具有 ACID 特性(原子性、一致性、隔离性、持久性),可确保并发访问和操作过程中的数据一致性。

Hive 最初是为重度读取、面向批处理的工作负载而设计的,并不支持 ACID 事务。不过,随着最近的改进(例如 Hive 3.x 中的 ACID 事务),Hive 增加了对更好地处理事务一致性的支持,但在 OLTP 场景中,它仍不能直接替代 MySQL。

Hive 的主要优势: 虽然 Hive 在事务处理能力方面正在迎头赶上,但它仍主要用于大规模批量数据处理,而非实时事务一致性,因此 MySQL 是 OLTP 的更好选择。

D. Data Processing Type

Hive 通常用于 batch processinganalytics。它利用了 Hadoop 的 MapReduce 框架,该框架专为分布式并行数据处理而设计。这使得 Hive 能够以分布式方式高效处理庞大的数据集。因此 Hive 非常适合对大量数据进行复杂查询,并生成报告或汇总。

MySQL 是为实时事务处理(OLTP)而设计的,通常以单节点事务方式运行,并没有针对分布式并行数据处理进行优化,因此适用于需要即时数据更新和低延迟查询的应用程序。

因此总的来说,如果目标是为 long run batch processinganalytics,在多个节点上并行执行对海量数据集的查询,那么 Hive 比 MySQL 更为适合,后者在处理此类工作负载时会非常吃力——如果不对架构进行重大调整(如 sharding、cluster),MySQL 就无法高效地执行查询。

E. Data Storage

Hive 通常将数据存储在 HDFS(Hadoop 分布式文件系统)等分布式文件系统或云存储(如 Amazon S3、Azure Blob)中。HDFS 针对存储大型文件进行了优化,可确保高吞吐量和容错性。

Hive 首先是同时支持列式存储和行式存储,可以在建表的语句中看到(请回想 OLAP 和 OLTP 的相关内容,为什么 OLAP 适合列存)。

此外,Hive 的文件存储有一批文件格式:

Hive 支持压缩(LZO, lossless data compression、Gzip、Bzip2)。

MySQL 则可以通过插件的形式支持 Parquet File 的导入导出。

 

总结一下,Hive 的使用场景:

MySQL 的使用场景:

 

24.3 再谈数据湖、数据仓库

我们发现,Hive 实际上是一个数据仓库,但是却使用 schema-on-read 模型,这说明 Hive 会像数据湖一样,先把原始数据保存起来。只有当使用 SQL 查询时才会尝试构建一个 schema。

因此 Hive 是一种混合系统(Hybrid System):

数据仓库里的数据是文件格式区别的,数据湖可以翻译不同格式的数据,就是直接把数据湖作为数据仓库,都可以用 SQL 方式访问数据湖;

介于前两种之间,当数据的使用高于一定频率时移动到数据仓库里,即热数据存在数据仓库里,可以提高处理这些数据时的效率,并且还支持非 SQL 的查询方式,这种架构也称湖仓一体 (lakehouse);

 

Chapter 25. Flink

25.1 Scene

Flink 要处理流式数据。所谓的流式数据,就是我们看到有很多的事件不断发生,以某种时序被处理系统接收到。

Apache Flink is a framework and distributed processing engine for stateful computations over unbounded and bounded data streams.

其中 Streams:

  • bounded and unbounded streams;

  • real-time and recorded streams

    • recorded:如果处理速度没有那么快,我就在 flink 前面加一层 kafka 消息队列,存在 topic 里面;

Flink 要做哪些事情?

考虑数据传输中存在的问题:

为了处理状态的需求,Flink 引入状态的概念。状态可以用对象保存,每个 field 和 variable 可以用键值存储起来(注意落盘)。

键值可以使用类似 IP Hash 方法来确保放在某个特定机器上处理。

为了处理顺序问题,Flink 引入水位线的概念。

如果我是 8 点到 9 点的数据,一个时间窗去处理, 9 点到 10 点一个时间窗去处理,但是在这个 9 点零二分的时候来了一个 9 点的数据,因为网络传输延迟。最简单的解决方案就是提供一个你可以接受的延迟时间,比如我定义九点零五分为 DDL,他在他九点零二分来的,那他仍然可以当成这个时间窗户数据处理。

窗口机制:

总结:我们是要靠这种时间戳加水位线的这种方式在告诉每一个算子过来的事件,它们的先后顺序什么样,你应该怎么把它组织到你这样的一个时间窗里面,在这个时间窗里激发对所有的时间的处理。

Flink 的状态是要频繁的使用内存的,而且他这个就是我们刚刚看到的,就是所谓的状态,是指你在每一次你的逻辑在处理这个事件的时候,在拿到事件的信息之后,你这个逻辑程序还要去读一下状态,才能知道怎么处理这个事件,而这个状态就在内存或硬盘上。

 

Chapter 26. AI

26.1 Full-Connected NN

26.2 分类神经网络构建

26.3 CNN

例子:

26.4 TLP

26.5 RNN & LSTM

Recurrent Neutral Network

26.6 ChatGPT & Transformer

Input -> [多头注意力层 -> 前溃网络层 ] 编码器 -> 重复 -> 特征值 -> 解码器 -> Output;