Index
第一章:Stateful and Stateless ,Cookie and Session, 实例池 , Scope ,数据库连接池
第二章:Messaging , 同步异步通信,三种订阅方式对比 ,Kafka
第三章:Websocket
第四章:Transaction(ACID 脏读幻读),两阶段提交,乐观/悲观离线🔒
第五章:调度(可串行化调度),各种日志(逻辑/物理/redo/undo/redoundo/补偿),系统崩溃恢复实现
第六章:线程(多线程,线程池,线程干扰,Synchronized),高级并发特性,Fork/Join
第七章:Cache,Memcache,Redis,一致性哈希
第八章:反向索引,Lucene
第九章:SOAP,Restful,Web Service,SOA
第十章:MicroService,Euraka,Serverless
第十一章:Mysql优化1(施工中⚠️)In a big mess , Who can help me😭
第十二章:Mysql优化2(施工中⚠️)In a big mess , Who can help me😭
第十三章:Mysql数据库恢复与备份(逻辑物理备份,binlog,snapshot)
第十四章:MySQL分区
第十五章:Nosql,MongoDB,DAO和Repository差异,sharding过程
第十六章:图数据库,Neo4j
第十七章:LSM-Tree, ssTable,向量数据库
第十八章:时序数据库,influxDB
第十九章:GaussDB,云数据库
第二十章:Data Warehouse,Data Lake
第二十一章:Cluster,集群,负载均衡,session维护,Nginx,反向代理
第二十二章:cloud computing,MapReduce,edge computing
第二十三章:GraphQL(以及和 RESTful API)对比
第二十四章:Docker,Image,Volume,mounts
第二十五章:Hadoop, Reducer数目 ,Yarn
第二十六章:Spark
第二十七章:Strom,Hadoop 和 Storm对比
第二十八章:Hdfs
第二十九章:Hbase,Web Table
第三十章:HIVE,ETL,HIVE和传统数据库的对比
第三十一章:FLINK
第三十二章:AI
第一章
Stateful and Stateless
对比维度 | Stateful | Stateless |
---|---|---|
定义 | 服务器维护客户端状态 | 服务器不维护客户端状态 |
状态存储 | 服务器保存状态(会话、上下文) | 服务器不保存状态,客户端每次发送完整请求信息 |
请求独立性 | 请求之间依赖历史状态 | 每个请求独立,无需历史信息 |
资源占用 | 高,占用服务器内存或存储资源 | 低,服务器无状态存储 |
扩展性 | 难以扩展,需要同步状态 | 易于扩展,无需状态同步 |
容错性 | 容错性低,服务器故障会导致状态丢失 | 容错性高,服务器无状态影响 |
复杂性 | 服务器设计复杂,需维护会话信息 | 客户端设计复杂,需提供完整请求信息 |
示例协议/技术 | FTP、Telnet、WebSocket | HTTP、RESTful API、DNS 查询 |
适用场景 | 实时互动、长时间会话(银行、游戏) | 高并发、无状态事务(搜索引擎、微服务) |
优点 | 便于处理复杂交互,支持实时状态管理 | 简化服务器设计,易扩展 |
缺点 | 高资源占用,难以扩展 | 网络开销大,不支持复杂会话 |
Cookie and Session
Http协议是一种无状态的协议,但是有时需要记录状态,达成Stateful。为此,有了Cookie和Session。
- Cookie:客户端浏览器用来保存用户信息的一种机制;当我们通过浏览器进行网页访问的时候,服务器会将一些数据以Cookie的形式保存在客户端浏览器上,当下次客户端浏览器再次访问该网站时,会将Cookie数据发送给服务器,服务器通过Cookie数据来辨别用户身份。(Cookie存的是key-value键值对)
- Session:表示一个会话,是属于服务器端的一种容器对象;默认情况下,针对每个浏览器的请求,server都会创建一个session对象,生成一个sessionId,用于标识该session对象,同时将 sessionId以cookie的形式发送给客户端浏览器,客户端浏览器再次访问该网站时,会将cookie数据发送给服务器,服务器通过cookie数据来辨别用户身份,从而找到对应的session对象,如果找不到,就会创建一个新的session对象。简单来说,Session的作用就是帮助我们实现一个有状态的 Http协议。
实例池(对象池)
“对象池”(Object Pool)是一种设计模式,它是一种用于管理和重用对象实例的机制,以提高性能和资源利用率的方式。对象池通常用于减少创建和销毁对象的开销,特别是在对象的创建成本较高或频繁创建和销毁对象可能导致性能下降的情况下。
在Java中,对象池通常是一个集合,用于存储和管理多个对象实例。当需要使用对象时,可以从对象池中获取一个可用的对象,而不是每次都创建新的对象。一旦使用完成,可以将对象返回到对象池中,以便稍后重用,而不是立即销毁它。但是实例池数量是有上限的,不可能无限制变大(DDos攻击内存直接爆了)。
因此,为了服务多个对象,在实例池满了后,可以把最不常用的对象放到硬盘上。如果有需要再换出。
这是因为Stateful引入的问题,因此系统要尽量Stateless,或者尽可能控制实例,也就有了Scope。
Scopes
“scope”(作用域)用于定义Spring容器如何管理bean实例的生命周期和可见性。其中singleton和prototype可以在每一个Bean对象前都定义,而其余的作用于整个Web。
Scope | Description | 解释 |
---|---|---|
singleton | (默认)将单个 Bean 定义的作用域限制为每个 Spring IoC 容器的单个对象实例。 | 整个 Spring 容器中只创建一个实例,全局共享 |
prototype | 将单个 Bean 定义的作用域限制为任意数量的对象实例。 | 每次获取 Bean 时都会创建一个新实例,比如controller调用service也会创建新的。 |
request | 将单个 Bean 定义的作用域限制为单个 HTTP 请求的生命周期。仅在支持 Web 的 Spring ApplicationContext 中有效。 | 每次HTTP请求都一个新实例,适合与请求相关的数据处理,比如请求参数校验等。 |
session | 将单个 Bean 定义的作用域限制为 HTTP 会话的生命周期。仅在支持 Web 的 Spring ApplicationContext 中有效。 | 每个 HTTP 会话周期内都会有独立的 Bean 实例,适合用户登录状态,在一个session内多个http只有一个实例。 |
application | 将单个 Bean 定义的作用域限制为 ServletContext 的生命周期。仅在支持 Web 的 Spring ApplicationContext 中有效。 | Bean 的生命周期与整个 Web 应用的生命周期一致,适合存储全局配置、共享资源等。和singleton区别是用在web。 |
websocket | 将单个 Bean 定义的作用域限制为 WebSocket 会话的生命周期。仅在支持 Web 的 Spring ApplicationContext 中有效。 | 每个 WebSocket 会话创建一个 Bean 实例,适合实时通信场景下与单个用户连接相关的数据或状态。 |
用两个浏览器(模拟2个隔离的session),各自发送1个request
Controller | |||
Prototype | session | ||
Service | prototype | 4 service instances 4 controller instances | 4 service instances 2 controller instances |
session | 2 service instances 4 controller instances | 2 service instances 2 controller instances |
数据库连接池
连接池(Connection Pool)的本质是线程池,是一种用于管理和重用数据库连接、网络连接或其他资源连接的技术,旨在提高应用程序性能和资源利用率。连接池通过维护一组已创建的连接实例,并在需要时分 配这些连接,以减少创建和销毁连接的开销。
连接池数量(经验公式)
connections = ((core_count * 2) + effective_spindle_count)
- core_count: CPU 核心数。
- effective_spindle_count: 有效磁盘数
当线程数过大,线程上下文切换会降低效率。而选择2倍CPU核心数是因为这样上下文切换比较少(最合理),而加上有效磁盘数是因为往磁盘写时认为CPU基本空闲,主要占用通道。
这说明数据库连接数和访问的数量没关系,只和自己硬件因素有关,理想的连接池是 “小池子,大压力”:你希望线程等待获取连接,而不是连接过多导致管理成本高。处理不过来数据应该换好机器。
第二章
Messaging – JMS
Messaging:是一种在软件组件或应用程序之间进行通信的方法
是一种peer-to-peer的方式,大家都一样可以接收也可以发送消息,每个客户端都连接到一个消息代理,该代理提供创建、发送、接收和读取消息的功能,这样就可以解决松散耦合的问题,使得分布式成为可能(可以转发给其它服务器消息了,而不是只可以去处理消息
JMS(Java Mesage Service):是一种 Java API,允许应用程序创建、发送、接收和读取消息,开源的实现有kafka,JMS还具备以下特点:
- 异步(Asynchronous):
- 接收客户端不需要在发送客户端发送消息时立即接收消息。发送端可以发送消息并继续执行其他任务;接收端可以稍后接收这些消息。
- 可靠(Reliable):
- 实现 JMS API 的消息提供者可以确保消息仅被传递一次且仅一次。对于不需要高可靠性的应用,也可以选择较低的可靠性水平,比如允许丢失消息或接收重复的消息。
JMS消息传输模型
- 点对点(Point-to-Point,P2P)模型:
- 在这个模型中,消息生产者发送消息到队列(Queue),消息消费者从队列中接收消息。
- 每个消息只能被一个消费者接收,一旦消息被消费,它就会从队列中移除,确保消息不会重复处理。
- 这种模型适用于需要确保消息只被处理一次的场景,例如订单处理、用户请求处理等。
- 发布/订阅(Publish/Subscribe,Pub/Sub)模型:
- 在发布/订阅模型中,消息生产者(发布者)将消息发送到主题(Topic),而消息消费者(订阅者)订阅感兴趣的主题。
- 一个发布者发送的消息可以被多个订阅者接收,这使得发布/订阅模型非常适合需要一对多通信的场景。
- 订阅者可以设置过滤条件,只接收满足特定条件的消息,这增加了消息传输的灵活性。
两种模型的特点对比:
- 一对一 vs 一对多:
- P2P模型是一对一的消息传输,每条消息只有一个消费者。
- Pub/Sub模型是一对多的消息传输,一条消息可以被多个订阅者接收。
- 队列 vs 主题:
- 在P2P模型中,使用队列作为消息的容器,队列保证了消息的顺序性和独立性。
- 在Pub/Sub模型中,使用主题作为消息的发布点,主题允许多个订阅者接收消息。
- 消息确认:
- 在P2P模型中,消费者通常需要显式确认消息,告知消息服务器消息已被成功处理。
- 在Pub/Sub模型中,订阅者可能需要确认消息,也可能不需要,这取决于具体的实现和配置。
- 消息持久性:
- 在两种模型中,都可以配置消息的持久性,确保消息不会因为系统故障而丢失。
JMS的这两种消息传输模型为不同的应用场景提供了灵活的消息传递机制,允许开发者根据业务需求选择合适的模型来实现消息通信。
JMS消息格式(不详细展开了,感觉不重要)
- 消息头 (Header)
- 必需部分,包含与消息相关的基本信息,比如消息的目的地、标识符、时间戳等。
- 作用:帮助消息传递和路由。
- 属性 (Properties)(可选)
- 用户或系统自定义的键值对(key-value pair)。
- 作用:携带额外信息或元数据,帮助消息接收方理解消息内容或进行筛选。
- 消息体 (Body)(可选)
- 消息的主要内容,通常包含业务数据。
- 格式可以是 文本(如 JSON、XML)、字节流、对象等。
同步客户端-服务器模型问题和局限
同步客户端-服务器模型(Synchronous client-server model):客户端发送请求后等待服务器响应,阻塞式执行。基本的好处是很简单但在高并发场景下效率低下。
特点 | 描述 | 影响 |
---|---|---|
紧耦合(Tightly Coupled) | 客户端和服务器高度依赖,协议、数据格式需完全一致。 | 缺乏灵活性,难以扩展,修改一方需改动另一方。 |
没有交付保证(No Delivery Guarantees) | 消息传递可能失败,服务器可能丢失请求(比如服务器不在线),客户端可能未收到响应,无冗余保证 | 增加数据丢失的风险,系统可靠性降低。 |
软件物种化(Software Speciation) | 不同系统难以兼容或协同,随着需求变化,客户端和服务器逐渐演化为独立的“物种”。 | 好处是提高了灵活性,可以让其更专注自己的需求。但系统难以集成,降低维护性和扩展性。 |
没有请求缓冲(Without a Request Buffer) | 服务器收到请求后需立即处理,无缓存机制。 | 高并发或高负载时,容易出现性能瓶颈或系统崩溃。 |
过于强调请求和响应(Too Much Emphasis on Requests and Responses) | 强调请求与响应的交互,忽略异步通信或事件驱动等更高效的模式。 | 系统灵活性不足,难以应对复杂业务需求。 |
通信不可重放(Communication is not Replayable) | 消息丢失后无法重放,客户端或服务器无法恢复之前状态。 | 数据可能丢失,系统容错性差,影响用户体验。 |
解决方法:异步模型
加一个消息中间件,服务端和客户端都与中间件交互,再由中间件转发。 比如controller收到消息,给kafka,service监听kafka去处理。这样实际上双方都是对等的,都可以接收发送消息。
此时消息可能十分钟后才处理(业务繁忙下),且在controller下已经返回结果,要告诉前端处理完成,就有了三种方式:
实现方式 | 原理 | 优劣势 |
JavaScript监听 | 把处理完成的消息也推到Topic里面,再由前端查询,原理如寄信 | 优点:直接交互,不需要单独写新接口(都运行kafka监听),复杂,客户端直接与Topic连接,Topic直接暴露,需要确保消息传递的安全性,防止未授权的订阅和消息窃取。 |
Ajax | 前端再发Ajax请求,轮询机制查询 | 无需中间件比较灵活,但严重增加服务器负载,造成不必要的带宽浪费 |
websocket | 服务器客户端websocket链接 | 实时更新,双向工作,节省了带宽通信量,可以保持持久连接。但实现复杂度高,保持连接讲持续占用服务器资源,老浏览器不兼容。 |
二者并不存在绝对的优势劣势,例如登录需要及时的反馈,那么同步模型更好。
Kafka
一种消息队列;主要用来处理大量数据状态下的消息队列,一般用来做日志的处理。主要作用有解耦合,异步处理,流量削峰。
- kafka的数据存储在log里面,数据往文件后面不断追加,追加的速度更快
- log里面都是有序的事件,每个事件都有位移,偏移量
- 当然有很多个消费者,把消费者分成不同的组。当一个消息被所有的消费者读走了之后,这个消息 才会被删除掉;多个用户可以读取同一个log,并且维护他们各自的文件位置(都到哪里了)
- 这样就能保证log文件不可能无限制的增长
- 对于集群的容灾,用空间换可靠性,比如每个数据存储两个副本,这样比如有一个卡夫卡服务器崩溃了,还有一个备份能够使用。对于consumer容灾,此时就需要一个协调器在kafka集群里面,consumer一直发心跳包给协调器,如果不发协调器就不会再把消息给故障的机器。
第三章
WebSocket
WebSocket是一种应用协议,通过TCP协议在两个对等体之间提供全双工(双向都工作)通信,即用户可以主动给服务器发消息,服务器也可以主动给用户传递消息。
- 在WebSocket应用程序中,服务器发布WebSocket端点,客户端使用端点的URI连接到服务器。
- WebSocket协议在连接建立后是对称的:
- 客户端和服务器可以在连接打开时随时互相发送消息,也可以随时关闭连接。
- 客户端通常只连接到一个服务器,服务器接受来自多个客户端的连接。
WebSocket协议有两部分:
握手(handshake)和数据传输(data transfer )。
基本过程
客户端发起 WebSocket 握手请求
客户端通过使用其URI向WebSocket端点发送请求来启动握手。
握手(handshake)与现有的基于HTTP的基础设施兼容,Web服务器解释其为HTTP连接升级请求,一个例子如下:
GET /path/to/websocket/endpoint HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost
Sec-WebSocket-Version: 13
服务器响应 WebSocket 握手请求
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=
Sec-WebSocket-Accept
的生成
- 服务器对Sec-WebSocket-Key标头的值应用已知操作,以生成Sec-WebSocket-Accept标头的值。
- 客户端对Sec-WebSocket-Key标头的值应用相同的操作,如果结果与从服务器接收到的值匹配,则连接成功建立。
建立 WebSocket 连接
客户端收到服务器的响应后,验证 Sec-WebSocket-Accept
是否正确。如果验证通过,服务器和客户端就可以互相发送消息了。
ws://host:port/path?query
wss://host:port/path?query
示例实现
- 创建端点类(endpoint)。
- 实现端点的生命周期方法。
- 将您的业务逻辑添加到端点。
- 在web应用程序内部部署端点。
对于具体逻辑实现,用户应当先发送验证,服务器返回token,再根据token建立连接保证唯一性(防止其他人也可以连接)。
endpoint
在WebSocket中,endpoint 是指WebSocket通信的目标或终点。它是WebSocket连接的另一端,可以 是服务器或客户端,用于接收或发送WebSocket消息。
共有两种类型endpoint:
- WebSocket服务器端(endpoint):这是WebSocket通信的服务器端,它等待客户端的连接请求并处理它们。一旦连接建立,服务器端就会监听来自客户端的消息,并可以向客户端发送消息。 WebSocket服务器通常用于实时通信、在线游戏、聊天应用等场景。
- WebSocket客户端(endpoint):这是WebSocket通信的客户端,它与WebSocket服务器建立连接,并可以向服务器发送消息,同时接收服务器发送的消息。WebSocket客户端通常用于与服务器进行双向通信,以获取实时数据或与服务器进行交互。
消息代理(Message Broker)
在WebSocket通信中,消息代理是指WebSocket服务器和客户端之间的中间件,用于转发WebSocket消息。它充当了WebSocket服务器和客户端之间的桥梁,使它们可以相互通信。消息代理可以是一个独立的服务器,也可以是WebSocket服务器本身。
第四章
Transaction
transaction是一系列必须全部成功完成的操作,要么都执行,要么都不执行(事务回滚),保证 数据库
中的数据始终处于一致的状态。事务的主要目标是确保数据的一致性、完整性和可靠性,尤其是在并发访问和异常情况下。
实现的本质是依赖于 日志
实现的,对于内存变量比如临时变量int x , Int y,不会触发事务回滚
事物属性
事务处理的ACID
ACID 是数据库事务中的四个关键属性,确保数据库在执行事务时能够正确、安全地处理数据。
- 原子性:不可分割,确保事务中的每一步都执行成功或不执行,避免部分失败。
- 一致性:正确一致,保持数据库的完整性,避免违背约束条件。
- 隔离性:互不干扰,避免事务间的干扰,确保并发执行的事务彼此独立,和串行得到的结果一致。
- 持久性:永远保持,一旦事务提交,结果永久生效,避免数据丢失。
A – Atomicity(原子性):
- 定义:事务中的所有操作要么全部成功,要么全部回滚。如果事务中的任何一步操作失败,则整个事务失败,数据库状态会回到事务开始之前的状态,确保不会有部分操作成功的情况。
- 解释:事务要像一个不可分割的原子一样执行。即便事务中包含多步操作,这些操作要么全部完成,要么完全不执行。
- 例子:银行转账操作中,转账的两个步骤是从账户A扣钱、向账户B加钱。事务的原子性确保如果其中一步失败,整个转账操作就会回滚,账户A和账户B的余额保持不变。
C – Consistency(一致性):
- 定义:事务执行后,数据库必须从一个一致的状态转换为另一个一致的状态。在一致性约束条件下,数据库中的数据保持完整性。
- 解释:事务在开始和结束时,数据库都必须符合所有预设的规则、约束和数据完整性条件。事务不能破坏数据库的规则或完整性约束。
- 例子:假设有一个数据库规则,规定银行账户的余额不能为负数。一旦事务执行完成,数据库必须确保这个规则依旧成立,如果某个操作试图违反规则,事务将被回滚。
I – Isolation(隔离性):
- 定义:多个并发执行的事务之间彼此隔离,一个事务的执行不应影响其他并发事务的执行结果。每个事务应该好像是独立执行的,即使在多事务并发的情况下。
- 解释:即使有多个事务并发运行,系统也需要确保一个事务中的未提交数据不会被其他事务看到或影响,防止“脏读”、”不可重复读”、”幻读”等并发问题。隔离性通常通过事务的隔离级别来实现,如
Read Committed
、Repeatable Read
等。 - 例子:假设事务A正在修改一个数据,但尚未提交,事务B不应该读取到事务A未提交的中间状态数据。
D – Durability(持久性):
- 定义:事务一旦提交,数据的修改就会永久保存,即使系统崩溃或故障,已提交的事务数据也不会丢失。
- 解释:事务提交后,系统会确保所有数据的修改持久化存储,即使发生硬件或软件故障,系统重启后数据仍然可以恢复。通常通过写入日志或采用数据库备份机制实现。
- 例子:假设用户完成了一次银行转账操作并提交了事务,即使系统在转账完成后马上崩溃,转账的数据也应该永久保留,并在系统恢复后存在。
常见标签解释
Required
- 说明:默认的事务属性。如果当前没有事务,则新建一个事务。如果调用时已经存在一个事务,当前方法将加入到现有的事务中。注意!在进入和退出方法的时候都会做检查!
- 适用场景:适用于绝大多数情况,确保所有相关操作要么一起提交,要么一起回滚。
- 传播行为:加入现有事务或创建一个新事务。
- 具体事例:A中有方法B,若A,B均为Required,则只会创建一个事物
RequiresNew
- 说明:无论调用时是否存在事务,始终新建一个事务。如果已有事务,当前事务会被挂起,待新事务结束后恢复原来的事务。
- 适用场景:用于需要方法独立执行、避免与外部事务干扰的情况。
- 传播行为:始终创建新事务,原有事务被暂时挂起。
- 具体事例:LOG操作不希望被中断,可以单独开RequiresNew。若A中有方法B,若A为Required,B为RequiresNew,那么B发生错误不会影响A
Supports
- 说明:方法可以支持事务,但不强制要求。如果当前有事务,方法将加入现有事务;如果没有事务,方法也可以非事务性地执行。
- 适用场景:用于可选择性的事务操作,事务性并不是必需的。
- 传播行为:如果有现有事务则加入,否则非事务性执行。
- 具体事例:若A中有方法B,且二者均为Supports,则没有事物。
NotSupported
- 说明:当前方法不支持事务。如果调用时有事务存在,事务会被挂起,方法执行完成后再恢复原来的事务。
- 适用场景:当不希望方法在事务上下文中运行时,比如不需要事务保证的一些只读操作。
- 传播行为:挂起现有事务,无事务支持。
- 具体事例:若A中有方法B,B为NotSupported,那么B会不在事物状态执行
Mandatory
- 说明:方法必须在现有事务上下文中执行。如果调用时没有事务存在,会抛出异常。
- 适用场景:强制要求方法必须在事务中执行。
- 传播行为:必须有现有事务,如果没有就会失败。
Never
- 说明:方法不允许在事务上下文中执行。如果调用时存在事务,则抛出异常。
- 适用场景:需要确保方法绝对不在事务中执行时。
- 传播行为:不能有事务,如果有事务则失败。
回滚:抛出 RuntimeException 或者 Error 的时候,spring会自动回滚
事务隔离级别
事务隔离级别控制的是同一时间内多个事务如何相互影响数据可见性的问题。它规定了一个事务内的操作在访问数据时,是否能看到其他事务对相同数据的更改,以及当前事务对其他事务是否可见。不同的隔离级别提供了不同程度的数据一致性和并发性保护。
隔离性冲突总结
读写冲突 | 脏读 | 事务A读取了事务B修改但未提交的数据(读到了事物处理的中间状态),事务B随后回滚,导致A读的数据无效。 |
读写冲突 | 不可重复读 | 事务A第一次读取数据后,事务B修改并提交该数据,事务A再次读取时数据被修改。 |
读写冲突 | 幻读 | 事务A执行范围查询后,事务B插入/删除了满足条件的记录,事务A再次查询时不同。 |
写写冲突 | 脏写 | 事务A和事务B同时修改同一条记录,事务B的提交覆盖了事务A的修改。 |
使用方法
任务本身是在数据库完成,因此我们需要在链接数据库的时候,告诉数据库我们需要什么隔离,数据库可以帮助完成加锁操作。
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)
常见标签解释
Read Uncommitted:
- 特性:允许一个事务读取另一个未提交事务的修改,可能发生脏读(Dirty Read)。
- 优点:并发性能较高。
- 缺点:最低的隔离级别,可能会看到未提交的数据变化。
- 例子:假设车票预订了,此时锁住票,那么如果没有买没退出,其他人也买不了。
Read Committed:
- 特性:只能读取已经提交的事务修改,避免了脏读,但可能发生不可重复读(Non-repeatable Read)。
- 优点:大多数数据库的默认级别,保证每次读取的数据都是已提交的。
- 缺点:同一事务中,连续读取同一条数据可能会不一致。
- 例子:同一个事物内多次对一个值请求,第一次GET为a,第二次GET就可能为b了,因为每次都读取已经提交的事物修改。
Repeatable Read:
- 特性:保证同一个事务中多次读取的数据是一致的,避免不可重复读,但可能会发生幻读(Phantom Read)。
- 优点:确保读取的记录在事务期间不发生变化。
- 缺点:仍可能有幻读问题(即插入新数据时的并发问题)。
- 解释:与不可重复读的区别在于,不可重复读是针对已存在的记录的修改,而幻读则是因为并发事务插入或删除了新的记录,从而影响了查询结果集的条目数量或内容。对一个同一个List在同一事物读取,第一次读到10个,第二次读到11个,因为中间插入了一个数据。
Serializable:
- 特性:最高级别的隔离,事务之间完全隔离,仿佛事务是一个接一个顺序执行的,解决了幻读问题。
- 优点:提供了最高的事务一致性。
- 缺点:并发性差,容易产生性能瓶颈。
Spring中对应使用
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)
分布式系统实现
可能有多个数据库,此时Transaction(Tomecat / Spring)会自动协调多个数据源
两阶段提交
第一阶段:应用程序(User)向协调者(Transaction Manager)发送一个事务请求,通知协调者开始事务。协调者向所有参与者(Resource Manager)(可能是多个数据库、服务等)发送一个“准备提交事务”的请求,询问它们是否能够成功执行事务并提交结果。参与者(Resource Manager)执行事务并回应(参与者执行事务,但不提交,而是将事务操作暂时保存起来(例如锁住相关资源))。
第二阶段:提交前,一票否决制,必须要都可以提交才可以,如果一个说回滚,则都需要回滚;接着,协调者(Transaction Manager)发送提交或回滚请求,参与者执行操作,最后参与者确认结果。
问题:可能存在数据不一致问题,如果在提交阶段,有参与者提交了事务,但协调者崩溃或某些参与者没有收到提交指令,可能导致部分提交成功,部分没有提交,出现数据不一致的情况(通常通过三阶段提交协议等方式来解决)。
离线锁:所谓“离线”指的是flush之前,数据读出来之后,放在了tomcat的内存里;这个时候是离线的;所谓“在线”,就是这个对象实时反映了数据库的数据;但显然OR映射/JDBC都是离线的。
悲观离线锁
定义:悲观锁假定在并发操作时,会经常发生数据冲突,因此每次读取或修改数据时,都直接锁定资源,使得其他事务不能同时对其进行操作。也就是说,它“悲观”地认为会发生冲突,所以在访问资源时会先获取锁,确保当前事务独占资源。
- 当一个事务要操作某条记录时,它会对该记录加锁。其他事务想要操作该记录时,必须等待该锁释放。
- 典型场景下,悲观锁通常通过数据库的锁机制(例如表锁或行锁)实现。
- 在冲突较多、并发修改较多的场景下,如金融交易、库存管理等,可以确保数据的安全和一致性。
乐观离线锁
定义: 乐观锁假设在大多数情况下,多个事务并发操作时不会发生冲突,因此在执行操作时并不会对资源加锁。当事务在提交修改时,系统会通过一些方式(如版本号或时间戳)来检查在此期间是否有其他事务对数据进行了修改。如果发现冲突,则拒绝提交,事务必须重新尝试操作。这种方式“乐观”地认为冲突不会经常发生。
- 乐观锁不依赖数据库的物理锁机制,而是通过版本号(或时间戳)机制来控制。
- 每次读取记录时,读取该记录的版本号。
- 当进行更新操作时,会检查记录的当前版本号是否与读取时一致。如果一致,则更新成功;如果版本号不一致,说明在此期间其他事务修改了该记录,当前事务失败,需要重新尝试。
- 适合于读多写少的应用场景,如报表查询、用户查看数据等操作。因为在这种场景下,数据冲突的可能性较小,乐观锁可以有效提升性能。
两种锁简单对比总结
特性 | 粗粒度锁 | 细粒度锁 |
---|---|---|
锁定范围 | 锁定较大范围的资源(如整张表、整个集合) | 锁定较小的资源单元(如某行、某个对象) |
并发性能 | 较差,容易导致锁竞争 | 并发性较好,资源冲突较少 |
复杂度 | 简单,锁管理较少,避免过多的锁请求 | 复杂,需要管理更多的锁 |
适用场景 | 并发较低、操作范围较大、批量操作 | 并发较高、资源细分、需要精确控制每个操作的锁定 |
锁开销 | 较小,锁管理开销较低 | 较大,锁的管理和维护开销更高 |
死锁风险 | 死锁风险较小 | 死锁风险较大 |
粗粒度锁(Coarse-Grained Locking)
指的是锁定较大范围的资源或数据单元,以控制并发访问。比如说我一张表还控制其它表(外键关联),需要对整个表进行控制。实现原理:两组数据通过引用共享同一个版本号码,通过引用的方法,两个数据引用的 是同一个版本号码(所谓的版本号不是一个数字,而是一个对象,这个对象里面有一个数字属性, 这个数字就是版本号码)。
第五章
调度
作用:调度为事物的并发处理过程中,决定事物中每一个操作的执行顺序,以此来提高并行效率。
串行调度
定义:事物串行执行
可串行化调度
给定一个并发调度S,存在一个串行调度S’,在任何数据库状态下,按照调度S和调度S’执行后所产生的结果都是相同的此时调度S被称之为 可串行化调度
(serializable schedule)。
可串行化调度的数量十分巨大,且难以校验,数据库中一般通过找到可串行化调度的子集(充分条件),即找到能够提前确认是可串行调度的并发调度(部分解),进而提升调度效率。
等价操作
- 交换连续两个相同数据读取操作
- 交换连续两个不同数据读写操作
非等价操作
- 交换连续两个相同数据的读写,写写操作
冲突可串行化调度的验证
数据库原子性和持久性实现
故障系统重启后,内存数据丢失,磁盘数据不丢失
原子性(事物运行中)
- 事物运行期间不刷盘,故障系统重启后自动保证原子性
- 因为整个事物还没有进入硬盘,就可以当没有处理过这个事物,系统重启后重新执行任务则容易保持其原子性
- 占用内存容易很大
- 事物运行期间刷盘,故障系统重启需要回滚事务
- 面对重启比较麻烦,考虑回滚(undo)
持久性(事物已经完成)
- 事物完成时刷盘,故障系统重启后自动保证持久性
- 事物完成时不刷盘,故障系统重启后需要重做该事物(redo)
数据库恢复基本原理
- 无故障事务回滚(例如账户小于0):影响原子性,撤销该事务已做操作
- 故障失误回滚(例如死锁杀死事务):影响原子性,撤销该事务已做操作
- 系统故障(例如重启):内存数据丢失,影响原子性和持久性
- 原子性:撤销未结束(不带Commit、Abort标记)的事务
- 持久性:重做已经结束(带Commit或Abort标记)的事务
- 系统崩溃不能重启:不能提供服务,影响持久性
- 一主多备:主备之间通过日志保持一致性,发生故障后切换到其他系统
- 磁盘故障:磁盘数据丢失,影响持久性
- 磁盘数据多副本;数据备份机制:创建数据备份、日志备份
- 自然灾害:系统宕机不能重启,影响持久性
- 异地多机容灾:多机实时传输日志保证一致性,发生故障后切换到其他系统
高可用性指标
通用高可用指标:
- 平均故障间隔时间 MTBF(Mean Time between Failures):系统在两相邻故障间隔期内正确工作的平均时间
- 平均恢复时间 MTTR(Mean Time to Repair):系统平均从故障中恢复需要的时间
- 平均损坏时间 MTTF(Mean Time to Failure):系统出现损坏的平均时间。
数据库容灾指标:
- 恢复点目标 RPO(Recovery Point Objective):业务系统在系统故障后所能容忍的数据丢失量。
- 恢复时间目标 RTO(Recovery Time Objective):业务系统所能容忍的业务停止服务的最长时间。
- 一般用n个9来表示
- 例如四个九99.99%表示一年99.99%时间可用,即不可用时间为365*24*60*0.01%=52.56分钟
系统崩溃处理方式
FORCE和NO-STEAL方案选择
FORCE 强制 (事务提交强制刷盘) | NO-FORCE 强制 (事务提交不强制刷盘) | |
NO-STEAL非窃取 (执行期间不刷盘) | 无redo日志 | 无undo日志 | 有redo日志 | 无undo日志 |
STEAL窃取(执行期间可刷盘) | 无redo日志 | 有undo日志 | 有redo日志 | 有undo日志 |
数据库日志
- 数据库日志是数据库系统内一系列执行事件的记录,它与数据库事务是密切相关的,事务的执行过程会反映在日志中,数据库可以通过对日志的分析实现对事务的回滚(原子性)或重做(持久性)
- 日志是日志记录(log record)的序列。日志记录是数据库系统活动记录的最小单位,每一条记录反映了数据库系统的一次操作。日志记录不仅包含了数据的更新,还包含了数据库事务开始/结束的逻辑。
- 日志的内容在写入磁盘以后是不会被修改的,因此所有的日志内容可以顺序写入磁盘,这保证了高效的写入速度。该特性也是建立高可用恢复机制的前提即数据的随机读写转换为日志的连续读写。
Undo回滚日志
特点:记录旧值
- 格式 < T , X , Vold >
- T : 事物唯一标识符
- X:数据项
- Vold:数据项修改以前的值
- 产生时机:当数据T修改数据项X(Write(X))时产生,要对数据操作时候,先写日志,日志成功再操作,也叫做
WAL(Write Ahead Log)
- 作用:实现事物回滚
- 注意:一般还包括一个日志序号log sequential number(LSN),便于更快找到位置
Redo重做日志
- 格式 < T , X , Vnew >
- T : 事物唯一标识符
- X:数据项
- Vnew:数据项修改以后的值
- 产生时机:当数据T修改数据项X(Write(X))时产生,同样也是
WAL(Write Ahead Log)
- 作用:实现事物回滚
- 注意:一般还包括一个日志序号log sequential number(LSN),便于更快找到位置
逻辑日志
- 记录事务中高层抽象的逻辑操作
- 举例:记录日志中UPDATE、DELETE和INSERT的文本信息
- 例如小明的年龄由20改成21。
- 好处:数据库版本更新时候,也容易恢复
物理日志
- 记录数据库具体物理变化
- 举例:记录一个被查询影响的数据项前后的值
- 例如第10个页面第100偏移量的值由20改成21。
- 好处:1. 对于Insert,逻辑日志不能执行第二次,而对于物理日志可以(幂等性) 2. 占用小
物理逻辑日志
- 一种结合了物理日志和逻辑日志混合方法
- 日志记录中包含了数据页面的物理信息,但是页面以内目标数据项的修改信息则是以逻辑方式记录
- 例如第100个页面(物理)的小明年龄值由20改成21(逻辑)
数据库日志性质
- 幂等性:一条日志记录无论执行一次或多次,得到的结果都是一致的。
- 例如:x=x+1不幂等;x=0幂等
- 物理日志满足幂等性;逻辑日志不满足
- 失败可重做性:一条日志执行失败后,是否可人重做一遍达成恢复目的。
- 例如:插入一条记录失败,再次插入成功。
- 物理日志满足失败可重做性;逻辑日志不满足:例如插入数据页面成功,而插入索引失败重做插入这个逻辑日志失败。
- 操作可逆性:逆向执行日志记录的操作,可以复原来状态(未执行这批操作时的状态)
- 例如第10个页面第100偏移量的值由20改成21,逆操作由21改成20
- 物理日志不可逆(页面偏移量位置可能被后续记录修改),逻辑日志可逆。
适用性总结
日志类型 | 解析速度 | 日志量 | 可重做性 | 幂等性 | 可逆性 | 应用场景 |
---|---|---|---|---|---|---|
物理日志 | 快 | 大 | 是 | 是 | 否 | redo日志 |
逻辑日志 | 慢 | 小 | 否 | 否 | 是 | undo日志 |
物理逻辑日志 | 较快 | 中 | 是 | 否 | 否 | undo日志 |
数据库恢复算法概述
原子性保证
- NO-STEAL:执行期间不刷盘,不存在原子性问题
- 问题:当事物很大时,Mem占用过大
- STEAL:执行期间刷盘
- 问题:存在原子性问题,如上图的T4如果事务执行期间刷盘了,此时需要undo回滚
持久性保证
- FORCE:事务提交后就强制刷盘,不存在持久性问题
- 问题:数据库IO交互频繁,占用IO资源过多
- NO-FORCE:事务提交不强制刷盘
- 问题:存在持久性问题,如积攒了很多已经提交的事物没做,此时就需要redo重做
影子拷贝方法
原理:事物执行不刷盘,事物一提交就刷盘
如果事物Abort,数据库指针不变
如果事物执行完成,数据库指针就切换过来
基于undo日志的恢复方法
原理:事物执行期间可以刷盘(提交部分事物),且事物一提交就刷盘
恢复流程
例子
问题
基于redo日志的恢复方法
原理:事物执行不刷盘,提交了事物可以不刷盘。因此对于一些没做的事物redo恢复。
问题
基于undo/redo日志的恢复方法
原理:事物执行期间刷盘,事物完成不强制刷盘
Redo是幂等性操作,因为redo是物理日志,具备的是幂等性(见上有介绍)
而Undo不是幂等操作,因为undo是逻辑日志,具备的是可逆性
第六章
Multithreading
基本知识
进程
进程有一个自包含的执行环境。进程通常具有一组完整的、私有的基本运行时资源;特别是,每个进程都有自己的存储空间。进程间通信:RMI / Http,RMI通信方式:类似于Server和Client,用Serializable,流的方式传递
线程
线程有时被称为轻量级进程。进程和线程都提供了一个执行环境,但创建新线程所需的资源比创建新进程少。线程存在于进程中——每个进程至少有一个线程。线程共享进程的资源,包括内存和打开的文件。这有助于高效但可能存在问题的沟通。
多线程通信
创建线程
方法一:手动创建一个线程,这个线程执行完就结束了。(人为的管理、控制线程)
- 一种方法是继承Thread类(Java是单根继承,和cpp不一样,所以这种方法不推荐)
- 一种方法是实现Runnable这个接口(推荐使用这个,因为Java里面只能继承一个类,但是实 现可以实现多个接口)
方法二:通过线程池,如果有需要获取线程就在池子里面找一个,用完之后收回,还是放在在线程 池里面。节约资源,因为创建线程的时候需要消耗资源。(不需要考虑线程的管理,线程是新创建 的还是已有的使用者不需要知道,由线程池管理)
中断与存活函数
中断一个线程,调用Thread.interrupt()方法,让其抛出InterruptedException异常
判断是否存活,调用Thread.interrupted()
join
join方法允许一个线程等待另一个线程的完成。如果t是线程当前正在执行的Thread对象,t.join()
使当前线程暂停执行,直到t的线程终止。和sleep一样,join依赖于操作系统的时间,设备好理论就快,所以你不应该假设join会等待你指定的时间。也和Sleep类似,join通过发出InterruptedException退出来响应中断。
线程间通信方法及问题
线程之间主要通过共享字段以及字段引用的对象来进行通信。这种通信方式非常高效,但可能会导致两类错误:线程干扰(Thread Interference)和内存一致性错误(Memory Consistency Errors)。防止这些错误的工具是同步机制。
然而,使用同步机制可能会引入线程争用,即当两个或多个线程同时尝试访问同一资源时,会导致Java运行时以更慢的速度执行一个或多个线程,甚至暂停它们的执行。线程饥饿和活锁是线程争用的两种形式。
不引入同步机制的问题
Thread Interference(线程干扰)
- If the initial value of c is 0, their interleaved actions might follow this sequence:
Thread A: Retrieve c.
Thread B: Retrieve c.
Thread A: Increment retrieved value; result is 1.
Thread B: Decrement retrieved value; result is -1.
Thread A: Store result in c; c is now 1.
Thread B: Store result in c; c is now -1.
线程A的结果就丢失了
Memory Consistency Errors(内存一致性错误)
假设定义并初始化了一个简单的 int
字段:
int counter = 0;
counter
字段被两个线程 A 和 B 共享。假设线程 A 对 counter
进行递增操作: counter++;
随后不久,线程 B 打印出 counter
的值: System.out.println(counter);
如果这两个语句是在同一个线程中执行的,可以安全地假设输出的值是 “1”。但如果这两个语句是在不同的线程中执行,打印出来的值可能仍然是 “0”,因为线程 A 对 counter
的修改未必对线程 B 可见——除非程序员在这两个语句之间建立了先行发生关系(happens-before)。
解决方法
Synchronized
如果 count
是 SynchronizedCounter
的一个实例,那么将这些方法设为 synchronized
会有两个效果:
- 互斥:无法让两个对同一对象的同步方法调用交错执行。当一个线程正在执行某个对象的同步方法时,所有其他线程调用该对象的同步方法都会被阻塞(暂停执行),直到第一个线程执行完毕。
- 先行发生关系:当一个同步方法退出时,自动建立一个先行发生关系,保证对对象状态的修改对所有线程可见。
在 Java 中,每个对象都隐含地拥有一个内在锁(也称为监视器锁)。引用变量和大多数原始变量(除long和double之外的所有类型),读取和写入都是原子性的
对于所有声明为volatile的变量(包括long和double变量),读取和写入都是原子性的。
- 按照惯例,一个线程在需要对对象的字段进行独占且一致的访问时,必须获取该对象的内在锁,并在操作完成后释放该内在锁。
- 一个线程在获取到锁与释放锁之间的时间段内被认为拥有该内在锁。只要某个线程拥有内在锁,其他任何线程都无法获取相同的锁。当其他线程尝试获取该锁时,它们将被阻塞。
当一个线程释放内在锁时,释放动作与后续对该锁的任何获取操作之间会自动建立一个先行发生关系。
如果调用的是静态同步方法
- 静态方法是与类相关的,而不是与对象相关的。static 静态方法能不能使用同步方法呢?不会,因为每一个静态方法和类关联而不是一个对象
- 在这种情况下,线程会获取与该类关联的 Class 对象的内在锁。
方法一:针对于整个函数 | public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c–; } public synchronized int value() { return c;} } |
方法二:针对于部分 | public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); } |
方法三:对于某一个部分 这里针对于 c1,c2 加操作分别上锁,两个锁之间不干扰 | public class MsLunch { private long c1 = 0; private long c2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object(); public void inc1() { synchronized(lock1) { c1++; } } public void inc2() { synchronized(lock2) { c2++; } } } |
可重入的锁: ReentrantLock
我们有一个类 C ,它有一个Synchronized方法 m , m 里面调用了另外一个Synchronized方法 n , n 也 在 C 里面;然而,调用 n 的时候,发现这个对象的锁 在 m 手里呢! 也有可能 m 是一个递归的方法……
引入概念:可重入的锁。可重入的锁意味着: 一个线程不能获得一个被其他线程占据的lock;但是它能够获得一个它已经拥有的锁。
引入同步机制带来的问题
Deadlock(死锁):A线程等待B,B等待A,这样就会陷入死锁的循环。一般是由于资源竞争和并发处理顺序不当导致的。解决方法:超时释放资源
Starvation(饿死):A想要访问一个共享资源,但是一直获取不到,就像被饿死了一样。(需要消除那些贪婪的 线程,避免一个线程长期贪婪的占用资源;所以tomcat里面用了连接池,避免一个连接长期占用资 源)
Livelock(活锁):活锁是指一个线程对另一个线程的动作做出反应,而如果另一个线程的动作也是对前一个线程动作的反应,那么可能会导致活锁。与死锁类似,活锁状态下的线程也无法继续取得进展。然而,线程并没有被阻塞,而是彼此不停地响应对方,导致无法恢复正常工作。
解决方式:协调资源
- 受保护的块(guarded block)。 最常见的方式,while(!joy) {}简单的循环,但浪费处理器时间。一般不推荐这么做
- 调用
wait
方法会暂停线程,直到另一个线程发出通知,表示某个特殊事件发生(不一定是当前线程正在等待的事件)。在未来的某个时刻,另一个线程将获取相同的锁并调用Object.notifyAll
,通知所有等待该锁的线程,表示某个重要事件已经发生。在第二个线程释放锁后,等待的第一个线程将重新获取锁并从wait
方法的调用中恢复执行。
使用受保护块创建生产者-消费者应用
这种应用在两个线程之间共享数据:
- 生产者:创建数据。
- 消费者:使用数据。
两个线程通过一个共享对象进行通信。
协调是至关重要的:
- 消费者线程必须在生产者线程交付数据之后才能检索数据。
- 生产者线程必须在消费者线程检索完旧数据之前,不能交付新数据。
不可变对象(Immutable Object)
- 如果一个对象的状态在构造之后不能更改,那么该对象被认为是不可变的。
- 依赖不可变对象被广泛认为是编写简单、可靠代码的一种有效策略。
- 巧妙的避免了线程干涉(两个线程都在写W/W),内存不一致(一个在写一个在读W/R)
不可变对象在并发应用中特别有用:不可变对象的状态一旦创建就不能被改变,这使得它们非常适合在并发编程中使用,因为多个线程可以安全地访问不可变对象,而不必担心数据竞争或状态不一致的问题。这有助于简化代码并提高可靠性。
程序员常常不愿使用不可变对象,因为他们担心创建新对象的成本,而不是直接在原地更新对象。但对象创建的影响常常被高估,因为垃圾回收带来的开销减少和无需编写额外的代码来保护可变对象不受损坏,且可以通过不可变对象带来的一些效率来抵消。
高级并发特性
这些特性大多数都在新的 java.util.concurrent
包中实现。
Lock 对象:
Lock
对象提供了一种更灵活的锁定机制。与传统的synchronized
方法或代码块不同,Lock
提供了更多功能,例如可重入锁和条件锁,允许程序在不同条件下进行更细粒度的控制。使用Lock
,开发者可以手动加锁和解锁,这在复杂的并发场景下尤其有用。- 作用:
Lock
提供了对锁的精细控制,特别适用于需要复杂同步控制的场景。
Executors:
Executors
类提供了线程池管理的实现,避免了手动创建和销毁线程的麻烦,尤其适合需要频繁调度任务的应用程序。通过Executors
,可以创建固定大小的线程池、缓存线程池或单线程执行器。- 作用:线程池可以减少系统的开销,并提高资源利用率,适合大规模并发任务管理。
并发集合:
- 并发集合类(如
ConcurrentHashMap
:通过采用了分段锁的设计,允许多个读线程并发地访问map,并且允许多个写线程并发修改不同的段。)允许多个线程安全地访问集合,无需手动添加同步块。通过分段锁定技术,实现高效的并发操作,减少了锁竞争。 - 作用:并发集合适合读操作频繁的场景,避免了传统同步集合中的性能瓶颈。
原子变量:
- 原子变量(如
AtomicInteger
)允许线程安全的变量操作,无需显式使用锁。通过硬件支持的原子操作(例如 CAS 操作)来保证数据的安全修改。 - 作用:原子变量在多线程下提供高效、无锁的操作,减少了锁的开销,适合简单的递增、递减操作。
ThreadLocalRandom:
ThreadLocalRandom
提供了一种每个线程独立生成随机数的方式,避免了传统Random
类在多线程下的竞争问题。- 作用:
ThreadLocalRandom
通过为每个线程单独分配随机数生成器,解决了多线程随机数生成中的性能瓶颈问题。
Lock
- 锁对象的工作方式与同步代码中使用的隐式锁非常相似。
- 与隐式锁一样,一次只有一个线程可以拥有一个锁对象。
- 锁对象还支持
wait/notify
机制,通过它们关联的Condition
对象来实现。
- 锁对象相对于隐式锁的最大优势是能够在尝试获取锁失败时退出。
tryLock
方法在锁不可用时立即退出,或者在超时之前退出(如果指定了超时时间)。lockInterruptibly
方法在另一个线程发送中断信号时退出,前提是锁尚未获取。
Executors
分离线程管理与任务执行:在小规模程序中,任务和线程之间的紧密耦合是常见且可接受的。然而,在大型应用程序中,将线程的创建和管理独立出来能够提高代码的可维护性与扩展性。
执行器的作用:执行器是一种工具,帮助开发者以更灵活的方式管理线程,避免显式创建和控制线程。通过使用 Executor 接口和实现,比如线程池(Thread Pools),系统可以有效地管理多个线程,而 Fork/Join 则可以利用多个处理器来加速任务的并行处理。
线程池
工作线程(Worker Threads):线程池中的线程独立于任务(Runnable 或 Callable),用于执行多个任务。使用工作线程可以减少线程频繁创建和销毁带来的开销,提高系统性能。
固定线程池(Fixed Thread Pool):一种线程数量固定的线程池,通过 Executors.newFixedThreadPool 方法创建。固定线程池的优势是即使任务量增加,它也能通过固定数量的线程稳定运行,避免性能大幅下降,确保系统“优雅退化”。
其他线程池类型:
- CachedThreadPool:动态扩展的线程池,适合大量短期任务。
- SingleThreadExecutor:单线程执行器,保证任务逐个执行,适合依次处理任务的场景。
创建使用固定线程池的执行器的简单方法是调用 java.util.concurrent.Executors 中 的 newFixedThreadPool 工厂方法;此类还提供以下工厂方法: newCachedThreadPool 方法创建具有 可扩展线程池的执行器。此执行器适用于启动许多短期任务的应用程 序。 newSingleThreadExecutor 方法创建一个执行器,一次执行一个任务。
Fork/Join
- Fork/Join 是一种
ExecutorService
的实现,设计用于可以递归拆分为较小任务的工作负载。 - 目标是利用系统的所有处理器,最大化程序性能。
Fork/Join 工作方式:
工作流程类似于:如果任务足够小,直接执行,否则将任务拆分为两部分,分别执行并等待结果。这种方式非常适合并行处理复杂任务,尤其在多处理器系统中可以显著提升性能。
if (my portion of the work is small enough)
do the work directly
else
split my work into two pieces
invoke the two pieces and wait for the results
Fork/Join 框架的代码实现:
- 通过 ForkJoinTask 的子类实现,通常使用 RecursiveTask(可以返回结果)或者 RecursiveAction(无返回值)来处理任务。
示例 – 图像模糊处理:
- 处理图像时,每个像素的颜色值通过平均周围像素值来模糊化。
- 由于图像是一维或二维的大数组,这个过程耗时较长。
- 使用 Fork/Join 框架可以并行处理每个像素,从而显著减少图像处理的时间。
虚拟线程
虚拟线程是轻量级线程,可以减少编写、维护和调试高吞吐量并发应用程序的工作量。更加轻量化,比如在被IO阻塞时,那么就把线程解绑;等IO结束时再绑定线程,但不一定是原先的线程。但对于长时间占用cpu的任务不友好。
平台线程(Platform Thread)
- 定义:平台线程是一个围绕操作系统(OS)线程的轻量级封装的Java线程。
- 特点:
- 它在底层的操作系统线程上运行Java代码。
- 在其整个生命周期中,平台线程始终占用一个OS线程。
- 可用的操作系统线程数量决定了平台线程的数量限制。
- 平台线程通常具有较大的线程栈(存储线程执行状态的内存)以及其他由操作系统管理的资源。
- 支持线程局部变量(即特定于某一线程的变量)。
- 使用场景:适用于运行所有类型的任务,但由于受限于操作系统线程数量,平台线程是一种有限资源。
第七章
回顾Servlet和Tomcat
- Servlet是Java编写的服务器端程序,用于处理Web请求和生成Web响应。
- Tomcat是一个开源的Java Servlet容器,也可以称为Web服务器,用于托管和运行Servlet和JSP应用程序。
为什么需要缓存
Tomcat和数据库都不一定是在一台机器上面,可能是分布式的,可能是多台机器,可能是多个 Tomcat,可能是多个数据库。如果绝大多数情况都是只读的,后端和数据库之间的交互,网络开销很大,此时就直接把数据缓存到Tomcat那里,节省了带宽。
- Q:数据库里面有buffer、ORM映射里面也有buffer作为缓冲区,那为什么要缓存呢?
- A:上面说的这两个缓存都不是你可以控制的,完全取决于他们自身,我们开发者管不了。而使用 redis,我就可以自己的控制缓存!
事实上,我们后台的数据库未必是关系型数据库!我们可能还搞了文件系统、mongo……最终它们还是表示为对象,理论上来说它们也应该进缓存!但是像spring jpa里面的缓存,或者hanibatis里面的缓存,把数据库里面抓取过来的数据已经弄成对象缓存了,它也仅仅针对关系型数据库!(还有,我把东西写到redis里面,写入的是已组装好的对象;原始数据组装为对象也需要消耗呢!所以这也是redis节省的地方)
假如我还有从文件系统里面读取的,nosql数据库里面的读取的东西、动态生成的网页、图片都可以 , 所以使用redis就都可以存储,redis完全在开发者的控制下。比如双十一的时候,我可以趁用户访问量少的时候提前把可能卖爆的网页缓存出来,这样提高访问的速度。 redis甚至还可以作为消息中间件,类似kafka的topic,他和卡夫卡的差别是redis里面的东西在内存里面。
数据库服务器对于内存要求比较高,而Tomcat服务器主要是CPU密集型(因为要处理一堆请求);所 以,对于一些只读的数据,我从数据库拿到之后就缓存在Tomcat本地!
Memory Caching
Memcached
简介
Memcached 是一个高效的缓存系统,帮助提高网站的性能。它存储一些数据在内存中,以减轻数据库的负担。这个系统易于使用,支持多种编程语言。
- 高性能:Memcached 能够快速处理数据请求,适合高流量的网站。
- 分布式:它可以在多台服务器上运行,便于扩展。
- 内存存储:数据被存储在内存中,读取速度很快,适合频繁访问的数据。
- 简单的键值存储:数据以键值对的形式存储,方便快速查找和使用。
原理:存储在内存中的KV
memcached里面,应该主要放置要经常读的数据,可以更快读到,写来说意义不大
使用Caching和不使用Caching时间对比,使用后第二次及以后查询显著加快
内存管理
在没有使用 Memcached 的情况下,每个 Web 服务器的缓存内存是独立的,分别为 64MB,总缓存为 64MB。
使用 Memcached 后,各个服务器的内存可以组合在一起,形成一个 128MB 的总缓存池。Memcached 可以通过分布式缓存使各个节点共享内存,从而更好地利用内存资源。
Memcached 的 Slab 分配机制,它通过将内存划分成多个“Slab Class”来管理不同大小的数据。每个 Slab Class 包含多个 Chunk,每个 Chunk 的大小是相同的。
每个 Slab Class 对应不同的 Chunk 大小,比如图片中的 Slab Class 1 的 Chunk 大小是 88 字节,Slab Class 2 是 112 字节,依次类推。
例如当存储一个 100 字节的项目时,Memcached 会选择一个最适合的 Slab Class(比如包含 112 字节 Chunk 的 Slab Class 2)来存储该数据项。当然对于varchar还是两端放,因为不知道数据多长。
当存满之后,会放在硬盘里面(一般是在服务器硬盘),相比于从数据库读还是快一些。
节点之间不需要通信,执行代码时候会自动取节点寻找,见一致性哈希
一致性哈希(Consistent Hashing)
一致性哈希是一种用于分布式缓存系统的数据分布算法。它通过将节点和数据项映射到一个哈希环上,使得当节点发生增减时,仅会影响一小部分的数据,从而实现高效的数据分配与管理。
工作原理
- 哈希环(Hash Ring):一致性哈希将所有的节点和数据项根据其哈希值映射到一个哈希环上。哈希值的范围通常是
[0, 2^32)
。 - 节点映射:每个节点(如缓存服务器)在哈希环上占据特定的位置,位置由节点的哈希值决定。
- 数据项映射:每个数据项(Key)通过哈希函数计算出哈希值,从而确定在哈希环上的位置。
- 数据存储规则:一个数据项会存储在“顺时针方向上的第一个节点”上。即,哈希值在环上顺时针遇到的第一个节点即为目标节点。
特点
- 减少数据迁移:在节点增加或移除时,仅影响环上与该节点相邻的数据项,其他数据不会重新分配,避免了大规模的数据迁移。这在实际应用中能够显著提高系统的稳定性和扩展性。
- 负载均衡:一致性哈希能够将数据均匀地分布在多个节点上,从而实现负载均衡。
- 扩展性强:支持动态添加或移除节点,并且影响范围小,方便系统的横向扩展。
图解说明
- 初始状态:左图展示了一个包含
node1
、node2
、node3
和node4
的哈希环。数据项根据哈希值在环上找到最接近的节点进行存储。 - 节点添加:右图展示了增加
node5
后的变化。新节点node5
加入后,仅影响了部分数据项的存储位置(位于 node4 和 node2 之间的数据项),而其他数据项的位置保持不变。删除也同理
Redis
Redis 是一种被称为键值存储(Key-Value Store)的数据库类型,通常归类为 NoSQL 数据库。键值存储的核心在于将数据(称为值)存储在一个唯一的键下。
- 开源与许可:Redis 是一个开源项目,基于 BSD 许可证发布。
- 高级键值存储:与其他键值存储不同,Redis 不仅可以存储字符串类型的值,还支持丰富的数据结构,包括:
数据结构类型 | 包含的内容 | 结构读/写能力 |
---|---|---|
STRING | 字符串、整数或浮点数值 | 对整个字符串或部分字符串进行操作,增加/减少整数和浮点数值 |
LIST | 字符串的链表 | 从两端推送或弹出元素,根据偏移量进行修剪,读取单个或多个元素,按值查找或删除元素,范围查找较慢 |
SET | 无序的唯一字符串集合 | 添加、获取或删除单个元素,检查成员关系,进行交集、并集、差集运算,获取随机元素 |
HASH | 无序的键值对哈希表 | 添加、获取或删除单个元素,获取整个哈希表 |
ZSET (Sorted Set) | 有序的字符串成员到浮点数分数的映射,按分数排序 | 添加、获取或删除单个元素,根据分数范围或成员值获取元素 |
内存与持久化
Redis 主要在内存中工作,以此来实现出色的性能。这意味着数据的访问速度非常快。根据不同的应用需求,Redis 提供了多种持久化方式:
- 快照(Snapshotting):定期将数据集快照(dump)保存到磁盘中。
- AOF 日志(Append-Only File):每次执行命令时,将其附加到日志文件中。
Redis 支持简单配置的主从复制(Master-Slave Replication)(缓存怎么会有主从备份?因为有人把它当NoSQL数据库用……),其特性包括:
- 快速非阻塞的初次同步:主节点与从节点的初次数据同步非常快速且不阻塞。
- 自动重连:在网络分裂(如网络故障)后,Redis 可以自动重连并继续同步。
Redis中应该放的有:1. 经常用到的东西 2. 经常涉及到读取操作的,而很少涉及到写的操作的 3. 中间计算的结果 4. 经常要写的东西不要放入缓存
序列化:两个进程之间通信很麻烦,对于Tomcat和redis是两个进程。进程间所谓的通信就是 传输数据;比如我要传输一个object,我把它转化为一个字节数组 byte[] ,这就是序列化;然后你 收到了这个字节数组,你就把它反序列化,就变成了一个object。
应用场景:Redis 由于其丰富的数据结构、内存中操作的高性能和持久化特性,被广泛用于缓存、实时分析、消息队列等场景。
名称 | 类型 | 数据存储选项 | 查询类型 | 附加功能 |
---|---|---|---|---|
Redis | 内存中的非关系型数据库 | 字符串、列表、集合、哈希、有序集合 | 针对每种数据类型的常用访问模式命令、批量操作和部分事务支持 | 发布/订阅、主/从复制、磁盘持久化、脚本(存储过程) |
Memcached | 内存中的键值缓存 | 键到值的映射 | 创建、读取、更新、删除命令以及少数其他命令 | 多线程服务器以增强性能 |
MySQL | 关系型数据库 | 行表、表视图、空间数据和第三方扩展的数据库 | SELECT、INSERT、UPDATE、DELETE、函数、存储过程 | 符合 ACID(使用 InnoDB)、主/从复制和主/主复制 |
PostgreSQL | 关系型数据库 | 行表、表视图、空间数据、第三方扩展、自定义类型的数据库 | SELECT、INSERT、UPDATE、DELETE、内置函数、自定义存储过程 | 符合 ACID,主/从复制,多主复制(第三方) |
MongoDB | 基于磁盘的非关系型文档存储 | 无模式 BSON 文档的表 | 创建、读取、更新、删除、条件查询等命令 | 支持 Map-Reduce 操作、主/从复制、分片、空间索引 |
查找过程:先去内存里面查找,查询失败后就从数据库里面读,读出来后放到内存里面。下次读取直接从内存中获取。当更改可以在内存里面更改值,保证查询时一致性。
分布式缓存
为什么需要分布式缓存:因为一个机器的缓存可能不够,物理上多个机器,实际逻辑上是一个大的 缓存,可以充分利用多个机器的内存; 稳定可以备份,可靠性增加。
第八章
反向索引和正向索引
反向索引,也称为倒排索引(Inverted Index),是一种索引方法,用于存储每个词或短语在文档中的位置信息。倒排索引通过记录每个词出现的文档列表来工作,使得在查询时可以快速找到包含特定词汇的所有文档。这种索引方法在全文搜索中非常有效,因为它允许用户快速找到包含某个关键词的所有相关文档。
Lucene的terms索引属于被称为反向索引,这是因为它可以列出一个item中包含它的文档,这与文档列出术语的自然关系相反
正向索引(Forward Index): 正向索引是一种按文档来组织和存储文本数据的索引方式。每个文档都有一个对应的索引项, 这个索引项包含了文档中的所有信息,通常以文档的标识符(如文档ID)为索引的键。 正向索引适合于需要按文档进行检索的场景,例如在文档管理系统中查找特定文档或根据文档属性进行过滤和排序。 缺点是在处理大量文本数据时,正向索引可能需要大量的存储空间,因为每个文档都需要一个 完整的索引项。
- 正向索引适用于需要按文档检索的应用,例如文档管理系统或内容展示。
- 反向索引适用于需要全文搜索和关键词检索的应用,例如搜索引擎和信息检索系统。
- 正向索引需要更多的存储空间,但在访问特定文档时速度较快。
- 反向索引占用较少的存储空间,但在全文搜索和关键词检索时速度更快。
通常,搜索引擎会结合使用正向索引和反向索引,以满足不同的检索需求,并提供高效的搜索体 验。正向索引用于快速获取文档的详细信息,而反向索引用于高效地找到包含查询关键词的文档。
Lucene
Lucene是一个高性能、可扩展的信息检索(IR)库。用来建立索引(如果不建立索引,每次都要扫描所有的文件,效率太低)和搜索。
适配范围:大量非结构化的文本,例如搜索引擎。
假设需要搜索大量文件,并且希望能够找到包含某个单词或短语的文件,解决方法:
对该文本进行索引,并将其转换为一种可以快速搜索的格式,从而消除缓慢的顺序扫描过程。
这种转换过程称为索引,其输出称为索引。
搜索即在索引中查找单词以找到它们出现的文档的过程。
搜索的质量通常使用精确度和召回率指标来描述。当然也包括速度和快速搜索大量文本的能力,支持单词和多词查询、短语查询、通配符、结果排名和排序也很重要,输入这些查询的友好语法也很重要。
全文搜索
类似百度的网站,用户搜索一个关键词,根据建立的索引,查找包括关键词的相关的非结构化的数据里面有哪些包含目标关键词。
结构化:如数据库,所有数据是schema的,数据有字段,像表格,是有结构的;但这里 比如我给你一个html,或者一段txt,这种东西是没有结构可寻的。
- 结构化数据建立索引的方式:B+树就可以
- 那非结构化数据怎么办呢?比如有一个dir目录,里面有一大堆.txt,和html文件,我要问这个目录里面包含关键词java的文件有哪些?
建立索引
如果我搜索java,你要告诉我java出现在哪些行,以及具体的位置?
- 1. 用某一种tokenizer处理源文件
- 2. preprocess,比如把所有的大写字母都变成小写,相同的单词都合并到一起(比如有些单词是 take、took、taken都是take的不同形式)
- 3. 反向索引:为什么叫反向索引呢,因为不是每一行映射到几个单词,而是某一个关键词映射到哪几 行的哪几个位置。
Core Indexing Classes 核心索引类
- IndexWriter: IndexWriter 是索引创建和维护的核心类。它负责将文档添加到索引、更新索引、删除文档以 及优化索引等操作。 IndexWriter 是在索引建立和更新过程中的主要接口之一。
- Directory: Directory 是索引文件的存储和管理抽象。它定义了索引文件的位置和访问方式,可以是基于 文件系统的目录,也可以是内存中的数据结构。 Directory 提供了对索引文件的读写操作,使 得索引可以被持久化存储和检索。
- Analyzer: Analyzer 是文本分析的关键组件。它定义了如何将文本数据分割成单词或词组,进行词干 化、去除停用词等文本处理操作。正确选择和配置适当的分析器对于索引的质量和性能至关重要。
- Document: Document 表示索引中的一个文档。文档通常由一组字段( Field )组成,每个字段包含了文 档的一部分信息,如标题、正文、作者等。 Document 用于将文本数据添加到索引。
- Field: Field 是文档中的一个字段或属性。它包含了字段的名称、值以及用于指定如何处理该字段的 配置选项。字段可以是文本、数字、日期等不同类型的数据,根据需要进行索引和检索。
Field每个字段对应于在搜索期间根据索引查询或从索引检索的一段数据。
public abstract class BaseIndexTestCase extends TestCase {
protected String[] keywords = {"1","2"};
protected String[] unindexed = {"Netherlands","Italy"};
protected String[] unstored = {
"Amsterdam has lots of bridges",
"Venice has lots of canals"
};
protected String[] text = {"Amsterdam","Venice"};
protected Directory dir; ... }
- Keyword :关键字不被分析(就是不会被拆开),而是被索引并逐字存储在索引中。
- UnIndexed :既不分析也不索引,但其值按原样存储在索引中。也就是说一旦假如通过keyword查 询到了,要把unindex的部分的内容要带出来,但是这部分不索引。(你不需要把荷兰和意大利做 索引,我希望的是我查出来1和2,你把荷兰和意大利的内容带出来)
- UnStored :此字段类型被分析和索引,要放到索引里去建,但不存储在索引中。当然有一些停用词 列表,比如一些非常常用的词,这些东西就被忽略。一般是正文,非常长不应该存在索引里面,非 常浪费。(比如这里我们把Amsterdam,bridge,Venice,canal来放入索引,你按照bridge查是可以查 到这个文件的,但是你索引里面没有存储整个完整的内容,因为完整的内容可能很大,所以我们真 的要取要读硬盘)
- Text :被分析并被索引,存储到索引里面。这意味着可以针对这种类型的字段进行搜索,但要注 意字段大小。可以类比keyword,区别是这个text会被分析一下,而keyword不会(也就是text会被 断开)
字段类型
在Lucene中,字段可以被存储,在这种情况下,它们的文本以非反转的方式按字面意思存储在索引中。反转的字段称为索引。
一个字段既可以被存储,也可以被索引。
字段的文本可以被标记为要索引的术语,或者字段的文本也可以直接用作要索引的词汇。
大多数字段都是标记化的,但有时对某些标识符字段进行字面索引是有用的。
字段是文档的一个部分,每个字段有三个组成部分:
- 名称:字段的标识符。
- 类型:字段的数据类型。
- 值:字段的实际数据,可以是文本(如
String
、Reader
或预分析的TokenStream
)、二进制数据(如byte[]
)或数字(如Number
)。
段(Segments)
Lucene 索引可以由多个子索引(或称为段)组成。每个段都是一个完全独立的索引,可以单独进行搜索。索引的演变方式包括:
- 为新添加的文档创建新的段。
- 合并现有的段。
- 搜索操作可能涉及多个段和/或多个索引,每个索引可能由一组段组成。
文档编号
在内部,Lucene 使用整数文档编号来引用文档。第一个添加到索引的文档编号为零,随后的每个文档编号依次增加一。
在Lucene中寻找和打分
Lucene 提供了多种不同的 Query
实现,允许执行各种类型的查询。
执行搜索
应用程序通常会调用 IndexSearcher.search(Query, int)
来执行搜索操作。一旦 Query
创建并提交给 IndexSearcher
,Lucene 开始进行评分过程。在一些基础设施的设置完成后,控制权最终交给 Weight
实现类及其 Scorer
或 BulkScorer
实例。
查询类
TermQuery
TermQuery
是最容易理解且应用最广泛的查询类。它匹配所有包含指定术语(Term
)的文档,Term
是出现在某个特定字段(Field
)中的一个单词。
例如,构造一个 TermQuery
如下:
TermQuery tq = new TermQuery(new Term("fieldName", "term"));
此查询会识别所有 Field
为 "fieldName"
并包含术语 "term"
的文档。
BooleanQuery
BooleanQuery
通过将多个 TermQuery
实例组合起来,可以构建更复杂的查询。BooleanQuery
包含多个 BooleanClause
,每个子查询都有一个运算符(来自 BooleanClause.Occur
),该运算符描述子查询如何与其他子查询组合。
- SHOULD:子查询可以出现在结果集中,但不是必须的。如果所有子查询都是
SHOULD
,则结果集中的每个文档至少匹配一个子查询。 - MUST:子查询必须出现在结果集中,并且会影响评分。结果集中的每个文档将匹配所有这样的子查询。
- FILTER:子查询必须出现在结果集中,但不影响评分。结果集中的每个文档将匹配所有这样的子查询。
- MUST NOT:子查询不能出现在结果集中。结果集中没有文档会匹配这样的子查询。
构造 BooleanQuery
时,通过添加两个或更多 BooleanClause
实例来实现。如果添加的子查询过多,在搜索时会抛出 TooManyClauses
异常。默认最多允许 1024 个子查询,可以通过 IndexSearcher.setMaxClauseCount(int)
修改这个值。
Phrase Queries(短语查询)
短语查询用于查找包含特定短语的文档,有以下几种实现方式:
- PhraseQuery:匹配一系列的术语。
PhraseQuery
使用 “slop” 因子来确定短语中相邻两个术语之间可以允许的最大距离。默认的slop
值为 0,表示短语必须完全匹配。 - MultiPhraseQuery:
PhraseQuery
的一种通用形式,允许为短语中的某个位置指定多个术语。例如,可以用它来执行包含同义词的短语查询。 - Interval Queries:在
Queries
模块中也提供了区间查询。
PointRangeQuery
PointRangeQuery
匹配位于数值范围内的所有文档。要使用 PointRangeQuery
,必须将值索引为数值字段,例如 IntPoint
、LongPoint
、FloatPoint
或 DoublePoint
。
PrefixQuery、WildcardQuery、RegexpQuery
- PrefixQuery:用于查找所有术语以某个字符串开头的文档。
- WildcardQuery:通配符查询,允许使用
*
(匹配 0 个或多个字符)和?
(匹配 1 个字符)通配符。 - RegexpQuery:比
WildcardQuery
更通用,允许匹配正则表达式模式的所有术语。
FuzzyQuery
FuzzyQuery
匹配包含与指定术语相似的术语的文档,相似度通过 Levenshtein 距离来确定。这种查询在处理拼写变体时非常有用。
打分
Lucene 打分机制
Lucene 的打分机制是其受欢迎的核心原因之一。Lucene 支持多种可插拔的信息检索模型,包括:
- 向量空间模型 (Vector Space Model, VSM)
- 概率模型,如 Okapi BM25 和 DFR
- 语言模型
打分公式中的因素
为了获得更高的得分,尽量增加查询词在重要字段(如标题)中的频率,并使用稀有且相关的关键词,同时保持字段内容简洁以提高权重。
因素 | 描述 |
---|---|
tf(t in d) | 术语 (t) 在文档 (d) 中的词频因子。表示该术语在文档中出现的频率。 |
idf(t) | 术语的逆文档频率。表示该术语在整个文档集合中的稀有程度,稀有的术语对得分影响更大。 |
boost(t.field in d) | 字段提升因子,通常在索引时设置,用于提升特定字段的权重。 |
lengthNorm(t.field in d) | 字段的长度归一化值,根据字段中的词语数量进行归一化。此值在索引时计算,并存储在索引中。 |
coord(q, d) | 协调因子,基于文档中匹配查询项的数量来计算。匹配的查询项越多,得分越高。 |
queryNorm(q) | 查询的归一化值,根据查询项的加权平方和计算。确保查询项更多的查询不会不公平地影响得分。 |
Solr
- 是基于Apache Lucene™构建的流行、快速、开源的企业搜索平台。
- 具有高度可靠性、可扩展性和容错性,提供分布式索引、复制和负载平衡查询、自动故障转移和恢复、集中配置等。
- Solr为世界上许多最大的互联网网站的搜索和导航功能提供支持。
SolrJ
- SolrJ 是 Solr 官方提供的 Java 客户端库,用于与 Solr 服务器进行交互,包括索引文档、执行搜索查询、管理核心(cores)等操作。
第九章
WebService
提供跨硬件、操作系统、编程语言和应用程序实现真正互操作性的机会
Web:相对于IPC(进程间通信),比如java中的RMI(remote method invocation)远程方法调用,或者其他语言中的RPC(远程过程调用),web的访问能力会强得多——只有web才能做到说比如美国大学有个服务器,我在中国也可以访问得到。
Services:独立于具体的实现,解决的是异构的问题(操作系统、编程语言的差异),比如客户端是C#开发的,服务端是java开发的,那为了方便他们之间的交互,我们就要走纯文本的路线,大家都能认识。既然是纯文本,那需要一定的格式,SOAP传的数据里面包括了一系列的operation、参数的名称类型,但是数据驱动型的。
SOAP
SOAP(Simple Object Access Protocol,简单对象访问协议)是一种用于网络服务的协议,它允许应用程序之间通过网络进行数据交换。SOAP 是基于 XML 的一种协议,可以在不同的操作系统和编程语言之间传递信息,具有良好的兼容性和扩展性。
SOAP 的基本概念
SOAP 是一种基于消息传递的协议。每次交互都是通过发送“请求”和接收“响应”的形式进行。SOAP 协议规定了数据应该如何在请求和响应中被格式化,从而确保不同的系统可以理解和处理这些数据。
SOAP 的组成部分
SOAP 消息:
- SOAP 消息通常是一个 XML 文档,主要包括四部分:
- Envelope:消息的外层,定义了消息是 SOAP 消息。
- Header:可选部分,通常包含一些控制信息,比如身份验证、会话信息等。
- Body:主要内容部分,包含了请求或响应的数据。
- Fault:可选部分,用于表示出错信息,比如服务不可用、权限不足等。
WSDL(Web Services Description Language):WSDL 是一种描述 SOAP 服务的 XML 文件。它定义了服务的所有接口、方法、参数以及返回值的格式,帮助客户端知道如何使用这个服务。可以理解为服务的“说明书”。
SOAP 传输协议:SOAP 通常使用 HTTP 或 HTTPS 作为传输层协议,也可以用其他协议(如 SMTP)。HTTP 是最常见的,因为它能穿透大多数防火墙,安全性高。
SOAP 的功能和特点
- 跨平台:SOAP 使用标准的 XML 格式,可以在不同的操作系统、编程语言之间传输数据。比如,一个 Java 服务端的应用可以通过 SOAP 和 C# 客户端的应用进行通信。
- 严格的标准化:SOAP 有一套非常严格的标准和格式。虽然这可能会导致 SOAP 消息的体积较大,但也确保了数据的准确性和一致性。
- 安全性:SOAP 协议通常搭配 HTTPS 使用,支持 WS-Security,可以在 SOAP 消息中添加数字签名和加密,保证数据的安全传输。因此,SOAP 非常适用于银行、金融等需要高安全性的场景。
- 可靠性:SOAP 支持 ACID(Atomicity, Consistency, Isolation, Durability)事务特性,因此可以确保数据的传输和操作的完整性,非常适合需要保证数据准确性的业务逻辑。
SOAP 的工作流程
假设我们有一个天气查询服务的 SOAP 服务,客户端可以请求天气信息,服务端会返回相应的结果。
- 定义 WSDL:服务提供方会发布 WSDL,定义了该服务的接口、方法、参数等信息。客户端可以查看 WSDL 了解服务的使用方式。
- 构建 SOAP 请求:客户端会根据 WSDL 构建一个 SOAP 请求消息。假设客户端想查询某个城市的天气,它会发送包含城市信息的 SOAP 请求消息。
- 发送请求:客户端将 SOAP 消息通过 HTTP(或 HTTPS)发送给服务端。
- 服务端处理请求:服务端接收到 SOAP 消息后,会解析消息内容,提取出请求的城市信息,然后查询天气数据。
- 构建响应:服务端将查询到的天气信息封装成 SOAP 响应消息,然后返回给客户端。
- 客户端解析响应:客户端接收到服务端的响应 SOAP 消息后,解析消息体,获取天气信息并显示给用户。
SOAP 与 REST 的比较
SOAP 和 REST 是两种常用的网络服务协议,它们的区别在于:
- 格式:SOAP 使用 XML,格式严格;REST 通常使用 JSON 或 XML,格式较为灵活。
- 传输协议:SOAP 可以使用多种传输协议(如 HTTP、SMTP),而 REST 通常只使用 HTTP。
- 安全性:SOAP 有较高的安全性,适合高安全性的应用场景;REST 在安全性上则相对简单一些。
- 性能:SOAP 消息比较庞大,性能较慢;REST 因为消息格式灵活,传输效率通常更高。
适用场景
- 企业应用:SOAP 适合那些需要复杂事务管理和高安全性的应用场景,比如银行、政府部门等。
- 跨平台:在需要支持多种编程语言和平台的环境中,SOAP 可以提供统一的接口和标准化的通信方式。
- 高安全性需求:如金融服务、支付网关等需要加密和签名的场景。
SOAP 是一种基于 XML 的网络服务协议,提供了跨平台、高安全性和可靠的通信方式,特别适用于企业级应用和需要高数据完整性的场景。尽管它的消息格式比较复杂,但标准化的格式也让它成为一种稳定和安全的网络服务选择。
SOAP 缺点
- 与消息格式耦合:SOAP 强制使用特定的 XML 消息格式。由于严格的格式要求,不同系统之间的通信需要在格式上保持一致,这导致服务和客户端在数据格式上有很强的耦合性。
- 与编码方式耦合:SOAP 的消息编码方式也很固定,不同系统间必须使用相同的编码方式。这增加了复杂性,也使得服务变得更加僵化和难以适应不同需求。
- 解析和组装 SOAP 消息:SOAP 消息是基于 XML 的,因此需要额外的解析和组装过程。这些操作会消耗资源,增加了系统的负担,特别是当消息内容较大时,效率问题会更加突出。
- 需要 WSDL 来描述服务细节:WSDL(Web Services Description Language)是用来描述 SOAP 服务的接口、方法、参数等信息的 XML 文件。使用 SOAP 服务的客户端需要通过 WSDL 来了解服务的详细信息,这使得整个流程更为复杂和依赖文档。
- 需要生成代理:在基于 SOAP 的网络服务中,客户端通常需要生成代理(Proxy)来与服务端交互。代理是基于 WSDL 自动生成的代码,这一过程既增加了系统开发的复杂性,又增加了维护成本。
由于上述这些耦合性和复杂性,使用 SOAP 实现网络服务会耗费较多的时间和精力,因此建议考虑寻找新的方式来实现网络服务。这也是为什么近年来 REST(Representational State Transfer)等更轻量级的网络服务方式逐渐流行的原因,REST 更灵活、格式更自由,并且更容易理解和实现。因此我们需要数据驱动型的方式来,Restful Web Service应运而生。
REST(Representational State Transfer)
REST 是一种轻量级的架构风格,用于设计网络服务。它基于 HTTP 协议,通过 URI 访问资源,使用统一的接口实现数据的传输和操作。相比于 SOAP,REST 更加灵活、高效,是现代 Web 开发和微服务架构的首选。
- Representational(表述):所有数据都是资源,每个资源都有其不同的表述。资源可以有多种表示方式,例如 JSON、XML 等。每个资源都有一个唯一的 URI 来标识,通过 URI,客户端可以访问并操作资源。
- State(状态):指客户端的状态,服务器是无状态的。资源的表述(Representation)是客户端的状态的一部分。每次请求后,客户端接收到资源的表述并更新自身状态,而服务器无需保存请求之间的状态。
- Transfer(传输):客户端通过请求 URI 获取资源的表述并更新自身状态。每当客户端请求不同的资源时,状态信息通过资源的表述被传输到客户端,这就是“表述性状态转移”。
REST 的核心概念
- 无状态性(Stateless)REST 是无状态的,服务器不保存客户端的状态。每个请求都是独立的,客户端必须在请求中包含所有必要的信息。无状态的设计让系统更易于扩展和维护。
- 资源导向(Resource-Oriented)在 REST 中,每个 URL 都表示一个资源,如用户、订单、商品等。每个资源有一个唯一的 URI(Uniform Resource Identifier),例如
/users/123
表示 ID 为 123 的用户。 - 标准的 HTTP 方法:REST 使用 HTTP 方法来定义操作类型,主要包括:
- GET:获取资源 READ
- POST:创建资源 CREATE
- PUT:更新资源 UPDATE
- DELETE:删除资源 DELETE
- 数据格式灵活
- REST 可以使用多种数据格式,最常见的是 JSON 和 XML,但 JSON 更轻量,便于解析,常用于前后端交互。
- 统一接口(Uniform Interface)
- REST 使用标准化的接口,所有资源访问和操作都通过标准 HTTP 方法进行,降低了系统复杂性。
REST 的功能和特点
- 轻量级和简单REST 的消息格式简单,通常不需要复杂的 XML 解析,传输速度更快,资源消耗更少。
- 灵活性REST 支持多种数据格式,允许根据需求选择最合适的格式(JSON、XML、纯文本等)。
- 扩展性REST 没有严格的标准和耦合,服务端和客户端可以独立演化。新功能可以随时增加,不影响现有接口。
- 浏览器友好REST 利用 HTTP 协议,适合在浏览器环境中使用,尤其适用于前后端分离的单页应用(SPA)。
RESTFul风格示例
RESTful风格的Web服务是一种基于REST(Representational State Transfer,表现层状态转移)架构设计的Web服务,它通过标准的HTTP方法(GET、POST、PUT、DELETE等)提供Web资源的操作接口,利用统一资源标识符(URI)进行资源的定位。通过Http的返回码表示返回的结果,比如201成功,404代表不存在,500代表服务器内部的错 误。RESTful服务以轻量、无状态、简单且符合HTTP协议为特点,在互联网和现代应用程序的开发中被广泛应用。
假设我们要设计一个RESTful风格的Web服务来管理“书籍”资源,资源路径:/books
请求方法 | URI | 操作 |
---|---|---|
GET | /books | 获取所有书籍 |
GET | /books/{id} | 获取某一本书籍(通过ID) |
POST | /books | 添加一本新书 |
PUT | /books/{id} | 更新某一本书籍的信息 |
DELETE | /books/{id} | 删除某一本书籍 |
REST 与 SOAP 的对比
特点 | REST | SOAP |
---|---|---|
消息格式 | JSON(常用)、XML、纯文本等 | XML |
协议依赖 | 基于 HTTP,轻量级 | 基于 XML,协议复杂 |
性能 | 高效,消息体积小 | 消息大,传输速度慢 |
灵活性 | 灵活,可以适应多种场景 | 严格,格式和协议不灵活 |
安全性 | HTTP 自带的 HTTPS 加密 | 支持 WS-Security,高安全性 |
适用场景 | 移动应用、Web 应用、微服务等 | 银行、金融、企业级应用 |
REST 的适用场景
- 移动应用和 Web 应用:REST 是轻量级的,非常适合需要实时数据交互的移动应用和 Web 应用。
- 微服务架构:微服务架构中多个独立服务间需要频繁交互,REST 的灵活性和低耦合非常适合。
- 公共 API:REST API 常用于公开的数据服务,如天气、地图、社交媒体,适合处理大量请求。
- 物联网(IoT):REST 的轻量和灵活性使其适合 IoT 设备间的数据交换。
Restful Web Service特点:1. 典型的客户端、服务端架构,但是是无状态的。2. 客户端和服务端之间传递的都是数据(数据!纯的!)3. 服务端只处理数据,数据的展示完全依赖于客户端
总结
REST 是一种基于 HTTP 的无状态架构,通过传输资源的不同表述来帮助客户端保持状态。其灵活性、可扩展性和简单性使得 REST 成为现代 Web 开发、微服务和移动应用开发的首选方法。
WS 的优缺点(Trade-off of WS)
WS : web Service
优点(Advantages):
- 跨平台(Across platforms):
- 基于 XML,与供应商无关(XML-based, independent of vendors)。
- 表示 WS 不受特定平台或供应商的限制,因其基于 XML 格式,可以在不同平台之间互通。
- 自描述(Self-described):
- 通过 WSDL(Web Services Description Language)描述操作、参数、类型和返回值。
- WSDL 是一种标准化的 XML 格式,用于描述 Web 服务的接口,因此客户端可以通过它理解服务的功能和如何调用。
- Restful就是完全基于URL
- 模块化(Modulization):
- 封装组件(Encapsulate components)。
- Web 服务允许将功能分割成独立模块,使不同的组件可以独立开发、测试和维护。
- 跨防火墙(Across Firewall):
- 使用 HTTP 协议。因为大多数防火墙允许 HTTP 通信,所以 Web 服务可以很容易地穿透防火墙进行数据交换。
缺点(Disadvantages):
- 较低的生产力(Lower productivity):
- 不适合独立应用(Not suitable for stand-alone applications)。
- 表明 Web 服务可能不适合单机应用程序的场景,因为其设计是为了跨平台、分布式的服务调用,而不是本地应用的效率。
- 较低的性能(Lower performance):
- 解析和组装(Parse and assembly),需要把java对象转化为一个纯文本的,或者解析文本作为一个java对象。
- 由于 Web 服务基于 XML 格式的数据交换,解析和组装数据的过程相对复杂,可能导致性能降低。
- 安全性(Security):
- 依赖其他机制,例如 HTTP 和 SSL(Depend on other mechanism, such as HTTP+SSL)。
- Web 服务自身没有内建安全机制,通常需要依赖其他协议(如 SSL/TLS)来保证数据传输的安全性,因为webService的地域性广,就比较容易受到攻击,或者中间人截获了。
使用场景:支持跨防火墙的通讯、支持跨防火墙的通信、支持应用集成、支持B2B集成、鼓励重用软件; 不应该使用WS时:独立应用程序、如MS Office(访问范围小)、局域网中的同构应用(都是java接 口,直接上java就完事了):例如COM+或EJB之间的通信(再比如spring访问mysql、redis、kafka、 elasticSearch)
SOA:面向服务的架构
SOA指的是Service-oriented architecture (面向服务的架构)。我们关注的问题是:我们在系统开发的过程中,异构性和需求的变化是我们永远都会遇到的,那么我们如何让自己的系统去快速响应这些问题呢?就是网上去找别人写好的东西,如果别人写的都是和语言无关的服务,我们就直接拿来集成!
假如我要构建一个电子书店、里面包括了认证登录系统、财务系统、统计系统、订单系统,这么多系统开发下来很复杂,而且假如我是卖家,卖给了1000个新华书店的用户,然后突然发现某个系统存在漏洞,就要告诉一千个用户,这很麻烦。
所以就有一个新方法、登录系统用第三方的比如百度的,订单系统用第三方的,然后我自己就只做一个 集成。如果发现登录系统有bug,就告诉百度的问题,百度只要更新自己的就可以了,我们不用把更新 包分发给1000个用户,因为它们都是直接调用百度的服务(比如你调百度地图,你当然不是把百度的副 本放到本地运行,而是调用人家的api)。这样就可以快速构建一个应用,节约开发成本。
特点:
- 服务之间松散耦合,也就是是登录系统、财务系统、统计系统、订单系统这些系统通过我开发的中 间件联系,而不是producer&consumer之间直接互相通信,这样的好处就是假如我发现登录系统不 好或者太贵了,我可以换一下,换之后别的系统也不会受到影响(当然松耦合的代价就是性能会差 一点)
- 位置透明,中介者负责路由
- 协议独立,从http切换到ftp,但是不要让客户端来实现
第十章
Micorservice
微服务(Microservices)是一种架构风格,它将应用程序分解为一系列小的、独立的服务,每个服务都有其特定的业务功能和目标。这些服务相对独立,可以各自开发、部署、扩展和维护。这种架构的核心特点在于应用程序代码以独立的小块形式交付,每个块都可以单独开发和维护,不依赖于其他部分。这与传统的单体架构形成了鲜明的对比,后者是将应用程序的所有部分紧密耦合在一起。
微服务架构的关键要点
- 独立部署:每个微服务都是一个独立的应用程序模块,可以单独进行部署和扩展,而不会影响到其他服务。这使得团队可以在不干扰整体系统的情况下进行快速迭代。
- 松耦合:微服务之间通过API或消息传递系统进行通信,保证了服务之间的低耦合性。这样,即便一个微服务出现故障,也不会影响到其他服务的运行,提高了系统的容错性。
- 业务独立性:每个微服务负责一个特定的业务功能,可以由不同的团队进行开发和管理。这不仅提高了开发效率,还让团队可以更快速地响应业务需求的变化,实现更好的业务与技术的对齐。
- 技术多样性:由于每个服务是独立的,团队可以根据具体的服务需求选择合适的技术栈和开发语言,而不必局限于单一的技术选择。
微服务的优势
- 更容易维护:由于服务被分解为小型模块,代码库变得更小,容易理解和维护。
- 生产力提升:团队可以并行地开发和部署不同的服务,减少了开发周期。
- 容错性更高:单个服务的故障不会导致整个系统瘫痪,系统具有更高的可靠性。
- 更贴近业务:团队可以为特定的业务需求创建和优化微服务,使得技术与业务更加贴合。
缺点:
- 微服务架构整个应用分散成多个服务,定位故障点非常困难。
- 稳定性下降。服务数量变多导致其中一个服务出现故障的概率增大,并且一个服务故障可能导致整个系统挂掉。事实上,在大访问量的生产场景下,故障总是会出现的。且如果GateWay崩溃了那么就有些困难,什么事情都做不了了;
- 服务数量非常多,部署、管理的工作量很大。成本增加,运维的复杂度增加。
- 开发方面:如何保证各个服务在持续开发的情况下仍然保持协同合作。
- 测试方面:服务拆分后,几乎所有功能都会涉及多个服务。原本单个程序的测试变为服务间调用的测试。测试变得更加复杂。
微服务架构适合那些需要频繁更新和扩展的大型复杂系统,通过解耦和模块化,微服务可以大大提高系统的敏捷性和可维护性。然而,在实际应用中,企业需要平衡其优势和实施成本,确保技术架构与业务需求匹配。
GateWay
作为一个统一访问的网关。应用程序(前端)不需要知道具体的是哪个服务器处理对应 的服务,只需要对GateWay发送请求就可以。此外,GateWay还可以起到负载均衡的作用,比如登录服务有三个服务器,他们的key名字都是完全一样的,GateWay就会采用相关的负载均衡的策略,提高并发性能。
Gateway 做的事情是: 客户端发过来的请求,比如 8080/user/login ,或者 8080/book/buyBook , Gateway 会知道说根据 /user 或者 /book 来转发到对应的微服务上面去,至于那个微服务的ip和端口, Gateway 不需要知道,因为 Gateway 会去 Eureka 上面去找,然后转发过去。
Eureka
Eureka 是什么?
- Eureka 是一种基于 REST(表现层状态转换)的服务,主要用于 AWS 云中,帮助定位服务,实现中间层服务器的负载均衡和故障转移。
- 核心组件:Eureka Server。
Eureka 的用途
- 服务发现:帮助微服务架构中的服务找到彼此。
- 基础负载均衡:默认支持轮询负载均衡,适合简单的负载分配需求。
- 高级负载均衡(Netflix):支持根据多种条件进行加权分配,确保更平稳的服务响应。
Eureka 是微服务架构中用于服务发现和负载均衡的重要组件,通过 Eureka Server 注册服务、通过 Eureka Client 进行交互和分配。Netflix 的版本进一步增强了弹性和稳定性。
高层架构
每个 region(区域) 有一个独立的 Eureka 集群,这个集群只知道自己区域内的实例信息。每个 zone(可用区) 至少有一个 Eureka 服务器,以应对区域内的故障。
在这个高层架构中,Eureka 作为服务注册中心,用于管理和发现服务实例。主要的设计原则包括:
Eureka 服务器的区域划分:
- Eureka 被设计为在不同的 region(区域) 中运行,每个区域有一个独立的 Eureka 集群。这意味着某一区域内的 Eureka 集群只关心并存储该区域内的实例信息,而不会涉及其他区域的服务实例。
Zone(可用区)内的故障处理:
- 为了确保服务的高可用性,每个 zone 至少有一个 Eureka 服务器。当某个 zone 发生故障时,其他 zone 的 Eureka 服务器仍然可以正常提供服务。这种设计分散了单点故障的风险,提高了系统的可靠性。
服务注册与发现过程:
- Application Service(应用服务) 作为 Eureka 客户端,通过注册和续约的操作将自身实例信息发送到 Eureka 服务器。
- Application Client(应用客户端) 从 Eureka 服务器获取服务实例列表,以便进行远程调用(比如访问其他服务)。应用客户端定期从 Eureka 服务器获取最新的注册信息,以便始终拥有最新的服务实例列表。
Eureka 服务器间的复制:
- 不同的 Eureka 服务器之间存在数据复制(replicate)机制。例如,在
us-east-1c
、us-east-1d
和us-east-1e
这三个 zone 的 Eureka 服务器之间,注册信息会相互复制。这样确保了即使某一台 Eureka 服务器出现问题,其他 Eureka 服务器仍然可以提供最新的服务注册信息。
这种架构设计的关键在于分区和高可用性,确保每个区域内的 Eureka 集群相互隔离,减少故障的传播。同时,每个区域内有多个可用区的冗余,以保证服务的持续性。
在 Spring Boot 中集成和使用 Eureka 作为服务注册与发现组件,通常使用 Spring Cloud Netflix 提供的支持。以下是实现步骤:
Serverless
什么是无服务器(Serverless)?
无服务器是一种云计算模式,开发者专注于编写应用逻辑代码,而不需要关心底层的服务器基础设施管理。通过无服务器架构,像函数即服务(Function as a Service, FaaS)这样的平台会自动处理扩展、运行时、资源分配、安全性等服务器细节。
在无服务器环境中,应用的执行是事件驱动的,即根据特定的触发条件(如HTTP请求、数据库更新等)来启动代码,而不是需要持续运行的服务器。此外,无服务器工作负载可以理解为“不关注通常由服务器基础设施处理的方面的事件驱动工作负载”,比如“要运行多少实例”和“使用什么操作系统”等问题都由FaaS平台管理,开发者只需专注于业务逻辑的实现。
无服务器应用的特点
- 事件驱动的代码执行:代码执行依赖特定的触发条件(如请求、数据库变化、定时任务等),只有在事件发生时才会运行,这种模式提高了资源利用率。
- 平台自动管理操作:云平台会负责应用的启动、停止和扩展工作,从而减少开发者在服务器管理上的负担,允许他们专注于代码编写。
- 可缩放至零:无服务器应用在空闲时可以缩放到零实例,这意味着当应用不使用时几乎不会产生费用。这种特性使得无服务器架构对负载不规则或需求不可预测的应用非常经济高效。
- 无状态性:每次函数调用都是独立的,不会在不同调用之间保留状态。无状态的特性简化了扩展,使平台可以并行执行多个函数实例而不会产生冲突。
缺点:第一次唤醒并返回响应需要一点时间。因为一些无服务器功能会按需运行,调试集成比较困难。
函数式服务:类比 y = f(x),给定一个输入,得到一个输出结果。此外两次或者多次的函数计算之间没有关 系,上次请求计算的结果计算完返回就结束,跟后面的没有任何关系,体现出一个无状态。
图示解释无服务器与传统基础设施的区别
图中的左侧显示了无服务器(FaaS),右侧是传统基础设施。传统的基础设施需要开发者管理许多系统方面,比如:
- 服务器:需要管理和扩展的物理或虚拟机。
- 安全性:网络安全、防火墙等保护机制。
- 事务管理:保证事务的完整性和一致性。
- 调度器:任务调度或队列管理。
而在FaaS的无服务器模型中,开发者只需专注于函数和应用的开发,底层的基础设施由云提供商自动管理,包括扩展和安全性。这种方式减少了管理的复杂度,使开发者可以快速部署和扩展应用。这种模式非常适合负载不可预测或事件驱动的应用,能够迅速扩展,并且仅在实际使用时产生费用,从而显著降低了成本。
Spring Cloud Function
Spring Cloud Function 是一个帮助 Spring 开发者使用无服务器架构或 FaaS(Function as a Service)平台的框架。通过 Spring Cloud Function,开发者可以将业务逻辑以函数的形式实现,并能够在常见的 FaaS 服务(如 Amazon Lambda、Apache OpenWhisk、Microsoft Azure、Project Riff 等)上运行这些函数。
Spring Cloud Function 的主要目标
- 推广通过函数实现业务逻辑:将业务逻辑封装在函数中,简化开发,增强灵活性。
- 解耦业务逻辑的开发和特定运行时环境:相同的代码可以作为 Web 端点、流处理器或任务运行,独立于具体的执行环境。
- 提供统一的编程模型:Spring Cloud Function 支持多种无服务器提供商,并且可以在本地(Standalone)或 PaaS 环境中运行,实现了跨平台的一致性。
- 启用 Spring Boot 特性:在无服务器环境中仍然可以使用 Spring Boot 的自动配置、依赖注入和指标监控等功能。
具体部署
- 启动注册表:其他的服务器启动的时候都要到服务注册这里来注册一下,所以必须要最先启动Eureka的这个服务。
- 启动微服务具体的服务器:在这个服务器启动的时候会自动到注册表服务器里面注册一下,便于后面的GateWay能够找得到。
- 启动GateWay,因为具体的业务服务器已经启动了,启动GateWay之后,用户的请求就可以转发到对应的微服务,如果先启动GateWay而没有启动具体的业务服务器,如果有请求发来就会导致GateWay找不到对应的微服务的服务器,然后出现报错。
微服务状态
肯定不能用session,比如微服务集群里面一个服务器用来登录,如果用这个服务器存储session,那么他下次下单的时候,可能是另外一个服务器处理的,而另外的服务器不知道用户的登录状态,因此解决方案就是jwt。
用户使用一个jwt token传入,服务器的GateWay对于需要登录授权的请求加一个过滤器,然后解析和这个jwt,再把解析获取到的用户信息放入请求的header里面,如果解析失败就直接拦截不转发给后面的,这样也增加了安全性。
第十一章
MYSQL优化
- 表结构是否合理,范式化?数据类型?
- 拆表?对于一张表中的数据,我们更希望是整张表中的数据全部被访问或者全部不被访问;若不然,就需要考虑拆表。
- varchar(可变长,便于压缩)和char还有text(存储一个指针,执行外部存储的原文);char(N)占 用的字节得看字符集,占用的是字符集中最长的字节数;
- 读取记录的时候,考虑到page的大小有限,如果一条记录的大小就占用了好几个page显然是不合理的,所以特别长的文本需要使用text类型,这样读取出来的text部分就是一个指针,性能更好。
- 什么样的索引科学? 什么样的存储引擎好?(注:MySQL其实分为两部分:Server层和存储引擎层,Server层的作用是 跟客户端交互,解析SQL,进行查询优化,存储引擎层负责数据的存储和提取)
- 锁、隔离机制?所有的内存用到缓存合适吗?
- 当硬件赶不上业务的时候,该怎么调整服务器性能?
- 列最好直接设置为not null;否则,在存储的时候,在这一列上多占用一个bit,来表示是否为null, 这样会浪费空间;同时,SQL在执行的时候还会多执行一句来判断你这一个字段到底是不是空的, 浪费时间。解决方案:即使是空的,我也写一个特殊值进去,就不为空了。
- 能选数字就选数字,数字类型少很多字节,而不是选择字符类型,这样性能会更好。
- join的时候要注意:两个表里面的那两个列,首先数据类型最好一致;其次两个列的名称最好也是 一致的,不要一个是“student-id”,一个是“student-number”。
硬件层级的优化
随着数据库负载的增加,任何数据库应用最终都会遇到硬件瓶颈。数据库管理员(DBA)需要评估是否可以通过调优应用程序或重新配置服务器来避免这些瓶颈,或者是否需要增加硬件资源。
常见的系统瓶颈来源包括:
- 磁盘寻道:磁盘定位数据的速度受限时,会导致性能下降。
- 磁盘读写:频繁的数据读写操作会导致磁盘的负载过高,影响数据访问速度。
- CPU 周期:复杂查询和计算密集型任务会耗费大量 CPU 资源。
- 内存带宽:内存带宽不足会影响数据从内存中读取和写入的效率。
优化硬件资源使用能够帮助缓解这些瓶颈,提升数据库应用的整体性能。
索引
提高select性能最好的办法就是在经常查询的列上面建立索引。索引条目充当指向表行的指针,允许查询快速确定哪些行符合WHERE子句中的条件,并检索这些行的 其他列值。
索引尽可能建立在取值为不为空(否则请用一个特殊的值填充空值),而且值唯一(或者很少有重复的)列上面。
并不是索引建立的越多越好,不必要的索引建立会浪费时间来决定用什么索引搜索;同时索引本身占据空间!(我每建立一个索引,就相当于用这个key来建立了一棵B+树)
- 如果建立了一堆索引,对于数据更新的时候,索引改变需要调整,那么由于大量的索引建立,导致调整b+ tree需要消耗大量的时间(所以需要管理者权衡考虑!)
- 系统会自动在自增的主键建立索引(是聚簇索引,也即索引在叶子节点的指针指向的位置在磁盘上是连续的),这样最大的好处就是在join操作的时候,可以快速定位到目标。
- 当表格比较小的时候,建立索引很浪费,不如全表扫描。
- 获取要find all的时候,索引也很浪费,本来就是要把整个表数据读取出来,索引失去意义。
选择自增主键做索引的好处:
- 如果表内的某几列都很重要,但是拿不准哪几列做索引的时候,不妨直接使用自增主键
- 自增主键的唯一性由数据库保证,更加可靠
- 同时这个主键作为外键也比较方便
- 因为索引是要把这列复制出来的,因此结构简单的话,生成的索引比较小,同时生成的B+树的效率 也比较高
1. 索引的作用
- 快速查找特定列值的行:如果没有索引,MySQL 需要从头读取整个表,导致表越大,读取成本越高。有了索引后,MySQL 可以直接定位到数据文件中的位置,而不必读取每一行。
- 优化 WHERE 子句查询:索引可以帮助快速找到符合
WHERE
条件的行。 - 排除无效行:通过索引,可以在早期筛选掉不符合条件的行,加速查询。
- 多列索引和前缀优化:对于多列索引,优化器可以利用索引的左侧前缀(即左边的一个或多个列),直接查找匹配的行。
- 连接表时快速查找行:索引加速了表联接过程中其他表的行检索。
- 查询最小值或最大值:对指定索引列(例如
MIN(key_col)
或MAX(key_col)
)的值检索时,索引可以提供快速访问。 - 排序和分组:如果排序或分组列是索引的左侧前缀,MySQL 可以直接使用索引,从而优化
ORDER BY
和GROUP BY
操作。 - 避免访问数据行:在某些情况下,查询可以直接从索引中获取结果,而不必访问数据行,这提高了效率。
2. 索引类型
- B-Tree 索引:MySQL 大多数索引类型(如
PRIMARY KEY
、UNIQUE
、INDEX
、FULLTEXT
)基于 B 树存储。 - R-Tree 索引:用于空间数据类型。
- Hash 索引:
MEMORY
表支持哈希索引。 - 倒排索引:InnoDB 的
FULLTEXT
索引使用倒排列表来加速文本查询。
3. 索引的适用性
- 小表或大表的报表查询:如果查询需要访问大部分或全部行,顺序读取会更快。顺序读取能减少磁盘寻道次数,甚至在不完全使用所有行时也有助于提升性能。
- 主键列:主键列(或列组合)上自动创建索引,这些索引通过
NOT NULL
限制优化查询性能。使用InnoDB
存储引擎时,表数据会按照主键列顺序进行物理存储,从而加速基于主键的查询。
4. 没有显著主键时的解决方案
- 自动递增主键:当一个表没有自然主键时,可以创建一个自动递增的列作为主键。这样生成的唯一 ID 可在表联接时作为外键指针指向其他表中的对应行。
索引是优化 MySQL 查询性能的重要工具,但在小表或需大规模读取的大表中意义有限。选择合适的索引类型和布局能显著提升数据库的查找、排序和联接性能。
MySQL 索引优化
聚簇索引和数据在磁盘上存储的顺序一致,所以会读一次就是一整页,一个表只能有一个,默认主键上就是聚簇索引
如果允许索引为空:浪费1bit;运行时一直做检查;树退化成链表
1. SPATIAL 索引优化
- SPATIAL 索引要求:MySQL 支持在
NOT NULL
的几何值列上创建SPATIAL
索引。为了确保比较操作正常,每个SPATIAL
索引的列必须具有限定的 SRID(空间参考系统标识符)。 - SRID 限制的作用:优化器仅在 SRID 限制的列上考虑
SPATIAL
索引。- Cartesian SRID 索引:支持笛卡尔边界框计算。
- Geographic SRID 索引:支持地理边界框计算。
2. 外键优化
- 分表策略:对于拥有大量列的表,如果查询时涉及多种列组合,可以将不常用的数据拆分到少列的独立表中,同时在主表中复制该表的主键 ID 列来关联这些表。
- 性能提升:小表拥有主键,便于快速查找数据。通过联接操作可以查询所需的列组合,从而减少 I/O 并优化缓存利用率。小表在每个数据块中能存储更多行,这使查询更高效。
3. 列索引优化
- 索引前缀:使用
col_name(N)
语法,可以对字符串列的前N
个字符创建索引。对较长的字符串列进行前缀索引,能显著减少索引文件大小。- 示例:
CREATE TABLE test (blob_col BLOB, INDEX(blob_col(10)));
- 前缀长度限制:
InnoDB
表的REDUNDANT
或COMPACT
行格式:最多 767 字节。InnoDB
表的DYNAMIC
或COMPRESSED
行格式:最多 3072 字节。MyISAM
表:最多 1000 字节。
- 超出前缀长度的搜索:若查询条件超出索引前缀长度,索引仍可用于排除不匹配的行,余下行会进一步检查匹配性。
- 示例:
4. FULLTEXT 索引
- 适用范围:
FULLTEXT
索引用于全文搜索,支持InnoDB
和MyISAM
存储引擎,适用于CHAR
、VARCHAR
和TEXT
列。FULLTEXT
索引作用于整个列,不支持列前缀索引。 - 优化查询:
- 查询文档 ID 或文档 ID 和排序:仅返回文档 ID 或带搜索评分的 ID 会更高效。
- 限定结果的排序:
FULLTEXT
查询按得分降序排序,且使用LIMIT
限制返回行数。 - 仅计数查询:如只需返回匹配项的
COUNT(*)
,WHERE
子句格式应为WHERE MATCH(text) AGAINST ('search_text')
。
5. 空间索引
- 支持的引擎与结构:
MyISAM
和InnoDB
支持空间数据类型的R-Tree
索引。- 其他存储引擎(除
ARCHIVE
外)使用B-Tree
索引空间类型,ARCHIVE
不支持空间类型索引。
6. MEMORY 存储引擎中的索引
- 默认索引类型:
MEMORY
存储引擎默认使用HASH
索引,也支持BTREE
索引。
7. 多列复合索引
根据多个列建立索引,会有不同列的优先级,比如姓名相同按照年龄排序,年龄相同安装学号排序。因此,索引的顺序非常重要!
- 复合索引的创建:MySQL 支持多达 16 列的复合索引。
- 适用查询类型:MySQL 可以对包含所有复合索引列、前几列、或前几个连续列的查询使用复合索引。
- 列顺序优化:按正确顺序指定索引列时,单一复合索引可以加速多个不同查询。
8. 多列复合索引的特性
- 本质:多列复合索引可以视作一个有序数组,其中每行是通过将索引列的值串联创建的。例如,下表通过在
last_name
和first_name
上建立复合索引:sql CREATE TABLE test ( id INT NOT NULL, last_name CHAR(30) NOT NULL, first_name CHAR(30) NOT NULL, PRIMARY KEY (id), INDEX name (last_name, first_name) );
- 索引使用场景:对于使用
name
索引的查询,包括但不限于以下几种情况:- 仅按
last_name
查询:SELECT * FROM test WHERE last_name='Jones';
- 同时按
last_name
和first_name
查询:SELECT * FROM test WHERE last_name='Jones' AND first_name='John';
- 使用多个
first_name
值查询:SELECT * FROM test WHERE last_name='Jones' AND (first_name='John' OR first_name='Jon');
- 范围查询:
SELECT * FROM test WHERE last_name='Jones' AND first_name >= 'M' AND first_name < 'N';
- 仅按
- 未使用索引场景:若查询条件不符合索引的左侧前缀,则无法使用索引。例如:
- 仅按
first_name
查询:SELECT * FROM test WHERE first_name='John';
- 使用 OR 条件(无法利用索引):
SELECT * FROM test WHERE last_name='Jones' OR first_name='John';
- 仅按
9. 左前缀原则
- 若存在复合索引
(col1, col2, col3)
,则 MySQL 仅在查询条件中包含索引的左侧前缀时使用该索引。 - 示例查询:
SELECT * FROM tbl_name WHERE col1=val1; -- 使用 (col1, col2, col3) 索引
SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2; -- 使用 (col1, col2, col3) 索引
SELECT * FROM tbl_name WHERE col2=val2; -- 不使用 (col1, col2, col3) 索引
SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3; -- 不使用 (col1, col2, col3) 索引
- 由于
(col2)
和(col2, col3)
不是(col1, col2, col3)
的左前缀,因此后两种查询不能利用该索引。
10. B-Tree 索引的特性
- 支持的比较操作:B-Tree 索引支持
=
、>
、>=
、<
、<=
、BETWEEN
等运算符的列比较。 - LIKE 运算支持:B-Tree 索引在
LIKE
运算中仅在LIKE
模式为常量字符串且不以通配符开头时生效(本质仍然是leftmost)。- 示例(使用索引):
sql SELECT * FROM tbl_name WHERE key_col LIKE 'Patrick%'; SELECT * FROM tbl_name WHERE key_col LIKE 'Pat%_ck%';
- 非常量字符串或以通配符开头的
LIKE
模式无法使用索引:sql SELECT * FROM tbl_name WHERE key_col LIKE '%Patrick%'; -- 不使用索引 SELECT * FROM tbl_name WHERE key_col LIKE other_col; -- 不使用索引
- 示例(使用索引):
优化建议
- 使用左前缀规则:创建复合索引时,按常用查询条件的左前缀顺序排列列,以提高索引利用率。
- 避免 OR 条件:在复合索引查询中,尽量避免 OR 条件,因其无法有效利用索引。
- 当where语句中使用or操作符并且or两边的条件涉及到至少两个字段时,MySQL无法使用索引,会转向全表扫描。因此,应尽量避免使用or操作符。
- 原因:因为MySQL中的索引是根据某个字段进行排序建立的。当使用or操作符,说明有两个条件其中某个条件成立即可,而我们使用某个索引时只能判断出对应字段的条件是否成立,即使不成立,另一个条件成立时该记录也符合我们要查询的结果。所以使用索引无法做出判断。
- 合理使用 LIKE:对字符串匹配查询,确保 LIKE 模式常量不以通配符开头以充分利用 B-Tree 索引。
- 如果使用了like且以%开头,则索引会失效。(右模糊失效)
- 如果使用了like且以%结束,则索引生效。(左模糊生效)
B-树Tree引特点
- 可以利用索引的WHERE子句:
WHERE index_part1=1 AND index_part2=2 AND other_column=3
WHERE index_part1='hello' AND index_part3=5
- 复合索引从最左部分开始时最有效。
OR
语句可能会导致部分或完全不使用索引。- 不利用索引的WHERE子句:
- 非最左前缀,如
index_part2=1 AND index_part3=2
OR
条件不在最左前缀的索引上。
哈希索引特点
- 仅支持等值查询,适用于
=
或<=>
操作符,不支持范围查询。 - 无法优化ORDER BY,只能用于完整键值的查找,典型于“键值存储”场景。
MySQL降序索引
- 支持DESC定义,提升按降序扫描的效率。
- 多列优化:支持混合升序和降序的索引扫描,提高检索效率。
表设计优化策略
表列设计
- 数据类型最小化:优选更小的数据类型,例如用
MEDIUMINT
替代INT
。 - 尽量避免NULL值,可以提升索引效率并节省存储空间。
行格式
- 使用紧凑行格式(如
DYNAMIC
、COMPACT
),减少CHAR列的存储占用。VARCHAR(可压缩,对于变长及可为空数据更好) - 压缩存储:可以使用
ROW_FORMAT=COMPRESSED
来减少存储空间。
索引设计
- 主键设计:主键越短越好,节省次级索引的空间。
- 复合索引:根据查询频率设计复合索引,最常用的列放在前面。
- 部分前缀索引:对于长字符串列,可使用前缀索引,提高索引命中率。
连接与规范化
- 拆表:将不常用的数据分表存储,提升查询效率。
- 规范化:减少数据冗余,保持第三范式;但在特定场景可反规范化提高性能。
数值优化
- 优先使用数字ID,减少存储和内存占用,提升对比速度。
字符串优化
- 使用二进制排序加快字符串比较。
- 分离长字符串列到独立表中,减少主表的行大小,提高常用查询性能。
大数据类型优化
- BLOB压缩:考虑对大块文本数据压缩存储。
- 分离BLOB列到单独表中,通过连接查询时才读取,减少内存和I/O负担。
在插入大量数据时,可以关闭唯一键检查(但是要人为保证),也可以设置唯一键不严格按顺序递增,这样mysql会为每个线程分配一块自增id;所以就不能通过最后一个id判断数据库的大小
默认每条语句都在一个事务中执行,所以可以设置是否立刻提交;也可以设置多条语句使用一个事务,但是要避免大事务(回滚耗时长);缓存刷盘设置、选择性刷盘、大事务拆分;事务隔离级别设置;只读操作不上锁
MySQL 如何打开和关闭表
运行 mysqladmin status
示例
执行 mysqladmin status
命令时,会显示类似如下内容:
Uptime: 426 Running threads: 1 Questions: 11082 Reloads: 1 Open tables: 12
Open tables 数值可能会大于实际表的数量,因为 MySQL 是多线程的。多个客户端可以同时查询同一个表,每个并发会话会独立打开表,以便保持各个会话状态一致。
这种方式消耗额外内存,但通常提高了性能。在 MyISAM 表中,每个客户端需要一个额外的文件描述符来打开数据文件。
打开和关闭表的机制
table_open_cache
和max_connections
:控制服务器可以保持打开文件的最大数量。- 若增加其中任何一项,可能会受到操作系统对单进程文件描述符数量的限制。
- 计算缓存大小:对 200 个并发连接,推荐设置
table_open_cache
至少为200 * N
,其中N
是查询中每个连接所用的最大表数量。 - 文件描述符限制:操作系统必须支持所需的文件描述符数量,否则 MySQL 可能出现拒绝连接或查询失败的情况。
表缓存管理机制
- MySQL 关闭未使用的表并将其移出缓存的情况:
- 当缓存已满且线程尝试打开不在缓存中的表。
- 当缓存中的表数量超过
table_open_cache
且某些表未被任何线程使用。 - 当执行
FLUSH TABLES
、mysqladmin flush-tables
或mysqladmin refresh
时发生表刷新。
缓存管理流程
- 缓存满时:MySQL 优先释放当前未使用的表,从最久未使用的表开始。
- 如果需要打开新的表且缓存满且无可释放表时,缓存将暂时扩展。
- 缓存扩展期间:若某表从“已用”转为“未用”状态,该表将被关闭并从缓存中移除。
检测缓存大小是否不足
- 通过
Opened_tables
状态变量,查看自服务器启动以来的表打开次数:
mysql> SHOW GLOBAL STATUS LIKE 'Opened_tables';
如果该值非常大或增加迅速,即使未执行过多的 FLUSH TABLES
操作,建议在服务器启动时增加 table_open_cache
。
同一数据库中创建多个表的缺点
- 在同一数据库目录中包含大量 MyISAM 表会导致 打开、关闭和创建操作变慢。
- 执行多个表的
SELECT
语句时,若表缓存已满,则会有额外的开销,因为每次打开一个新表都可能导致关闭另一个表。 - 优化方法:增加表缓存中的条目数以减少频繁开关表的开销。
Limits
MySQL 数据库和表数量的限制
数据库数量
MySQL 本身没有数据库数量的限制,但底层的文件系统可能会对目录数量有限制。
表数量
- MySQL 本身也没有表数量的限制,但文件系统可能会限制用于表示表的文件数量。
- 不同的存储引擎可能会有不同的限制,例如:
- InnoDB 支持最多 40 亿张表。
表大小限制
- 表的最大有效大小通常由操作系统的文件大小限制决定,而不是 MySQL 的内部限制。
- 操作系统文件大小限制的最新信息,参考操作系统的官方文档。
- Windows 用户注意:FAT 和 VFAT (FAT32) 不适合生产环境,建议使用 NTFS 文件系统。
遇到“表已满”错误的原因
- 磁盘已满。
- 使用 InnoDB 表,且 InnoDB 表空间文件已满。表空间的最大大小即是单表的最大大小,推荐将大于 1TB 的表划分为多个表空间文件。
- 操作系统文件大小限制。例如,使用 MyISAM 表,操作系统支持的最大文件大小为 2GB,那么当数据文件或索引文件达到此限制时就会出错。
- 使用 MyISAM 表,且表空间大小超过了内部指针所允许的最大空间。
- 默认情况下,MyISAM 支持数据文件和索引文件的最大尺寸为 256TB,可以增大到最大 65,536TB(256\^7 − 1 字节)。
列数限制
每表的最大列数
- MySQL 对单表的最大列数限制为 4096 列,但具体限制因不同因素而异:
- 行的最大大小限制了列的总数和大小。所有列的长度总和不能超过该限制。
- 单列的存储需求影响了给定最大行大小内可以容纳的列数。
- 存储引擎限制:例如,InnoDB 表每表最大列数为 1017。
- 功能键部分作为虚拟生成的隐藏列实现,因此表中每个功能键部分都计算在列总数限制内。
行大小限制
行大小的限制因素
- 内部表示:MySQL 表的内部表示最大行大小限制为 65,535 字节,即使存储引擎能支持更大行数。
- BLOB 和 TEXT 列只占用 9 到 12 字节,因为其内容存储在行外。
- InnoDB 行大小:InnoDB 表的数据在本地数据库页中存储,对于 4KB、8KB、16KB 和 32KB 页大小,行大小限制略小于一页的一半。
- 不同存储格式占用不同的页头和页尾空间,影响可用于行存储的空间。
CHAR不可压缩,建表时超过page的一半就报错;VARCHAR可压缩,所以VARCHAR还会出现建表不报错但插入报错的情况,我们可以通过VARCHAR建一个大小接近64KB的表,但是插入数据到达page大小的一半时就会报错
行大小限制示例
示例 1: 由于行大小超过限制,需将某些列更改为 TEXT
或 BLOB
类型。
mysql> CREATE TABLE t (
a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000),
e VARCHAR(10000), f VARCHAR(10000),
g VARCHAR(6000)
) ENGINE=InnoDB CHARACTER SET latin1;
- 错误: 超出 InnoDB 表类型的最大行大小限制(65535 字节),建议将部分列更改为
TEXT
或BLOB
类型。
示例 2: 更改 g
列为 TEXT
,解决了行大小超限的问题。
mysql> CREATE TABLE t (
a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000),
e VARCHAR(10000), f VARCHAR(10000),
g TEXT(6000)
) ENGINE=InnoDB CHARACTER SET latin1;
示例 3: 行大小限制的进一步测试。
mysql> CREATE TABLE t1 (
c1 VARCHAR(32765) NOT NULL,
c2 VARCHAR(32766) NOT NULL
) ENGINE=InnoDB CHARACTER SET latin1;
- 成功: 没有超过 InnoDB 的行大小限制,刚好剩两个字节用于存储开销。
示例 4: 当把非空改为空
mysql> CREATE TABLE t3 (
c1 VARCHAR(32765) NULL,
c2 VARCHAR(32766) NULL
) ENGINE = MyISAM CHARACTER SET latin1;
- 失败: 当可以为空时需要有额外的开销。
示例 5: 尝试定义单列的超大 VARCHAR
字符串。
mysql> CREATE TABLE t2 (
c1 VARCHAR(65535) NOT NULL
) ENGINE=InnoDB CHARACTER SET latin1;
- 错误: 行大小超限,因为包含了存储开销,建议改用
TEXT
或BLOB
类型。
示例 6: MyISAM 存储格式的行大小测试。
mysql> CREATE TABLE t4 (
c1 CHAR(255), c2 CHAR(255), ..., c33 CHAR(255)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1;
- 错误: 255 * 33 = 8415行大小超限 (>8126 字节),建议将部分列改为
TEXT
或BLOB
类型。CHAR是对齐的,VARCHAR是压缩的
优化 InnoDB 表
存储布局优化
- 当数据量稳定或增长至一定规模时,可使用
OPTIMIZE TABLE
语句重新组织表和压缩空间,减少索引的碎片化,从而提升性能。 - 如果主键过长(如组合多个列组成复杂主键),将浪费大量磁盘空间。推荐使用自增列 (
AUTO_INCREMENT
) 作为主键,或仅索引VARCHAR
列的前缀以节省空间。 CHAR
固定长度会占用更多存储空间,而VARCHAR
适合存储可变长度字符串,尤其是包含大量NULL
值的列。- 对于包含大量重复文本或数字的表,可考虑使用
COMPRESSED
行格式来压缩数据。例如学号5220319可以压缩。
事务管理优化
AUTOCOMMIT=1
是 MySQL 默认设置,但在繁忙服务器上可能造成性能瓶颈。建议将多个相关的数据操作放入单个事务,并在操作结束时执行COMMIT
。- 对于仅包含
SELECT
查询的事务,可以开启AUTOCOMMIT
以识别为只读事务,从而提高性能。 - 避免在插入、更新或删除大量数据后执行回滚,这样会严重影响性能,并可能导致比原始操作更长的回滚时间。即使在回滚过程中终止数据库进程,服务器重启时也会重新开始回滚。
读事务优化
- 对于只读事务,可使用
START TRANSACTION READ ONLY
语句,减少事务处理的开销。 - 如果事务只包含非锁定的
SELECT
语句,可以开启AUTOCOMMIT
设置,以简化事务管理并提升查询效率。
批量数据导入
- 在导入数据时关闭
autocommit
模式,因为每次插入都会刷新日志。
SET autocommit=0;
-- 批量导入 SQL 语句
COMMIT;
- 对于具有
UNIQUE
约束的二级索引,导入前可暂时关闭唯一性检查,避免大量磁盘 I/O。
SET unique_checks=0;
-- 批量导入 SQL 语句
SET unique_checks=1;
- 如果有外键约束,导入时可关闭外键检查,节省磁盘 I/O。
SET foreign_key_checks=0;
-- 批量导入 SQL 语句
SET foreign_key_checks=1;
InnoDB 查询优化
- 每个
InnoDB
表都需要主键,不应包含过多或过长的列,以免重复值在二级索引中占用过多空间。 - 不要为每列创建单独的二级索引,因为查询只能使用一个索引。对于常用组合查询,可以使用多列组合索引,而非单列索引。
- 若列不可包含
NULL
值,建议在创建表时声明为NOT NULL
(占用空间)。
在 MySQL 中,如果要一次插入多行数据,可以使用多行 INSERT
语法,这种方式不仅能减少与服务器的通信开销,还可以提高插入效率。语法如下:
INSERT INTO yourtable VALUES (1,2), (5,5), ...;
多行 INSERT
语法的工作原理
在一次 INSERT
操作中插入多行,客户端与数据库服务器之间的通信会减少,只需要一次命令即可插入多行数据。这种方法能减少 SQL 语句的处理开销,节省通信时间,提升插入速度。这种优化对于 任何存储引擎(不仅限于 InnoDB) 都适用。
innodb_autoinc_lock_mode
设置
对于包含自增列(auto-increment)的表,在执行批量插入时,innodb_autoinc_lock_mode
设置为 2(interleaved 模式)比默认的 1(consecutive 模式)更高效,尤其在并发环境下。
各个模式的解释:
- 模式 1(consecutive):在默认模式下,InnoDB 为每次自增值分配一个全局锁。这意味着即便是在批量插入中,服务器也会以递增的顺序分配每一个自增 ID,确保顺序连续。这种锁的方式减少了插入时的冲突,适用于单线程或不太频繁的自增操作场景。
- 模式 2(interleaved):在 interleaved 模式下,InnoDB 只会在获取自增值时申请一次锁,然后立即释放。这种方式更适合并发插入操作,因为不需要严格按照顺序分配自增值,只要 ID 是唯一的即可。这种方式允许批量插入时同时分配多个自增值,减少锁竞争。
为什么选择模式 2(interleaved)
在批量插入中,如果使用默认的模式 1(consecutive),系统会逐个锁定每个自增值,即便是批量插入,仍然会顺序分配,这会引入额外的锁竞争。模式 2(interleaved)则允许批量插入的自增值并行分配,不要求顺序一致,因此插入速度更快、效率更高。
InnoDB 的磁盘 I/O 优化
缓冲池的使用
- 增加缓冲池大小将数据缓存,避免每次查询都访问磁盘。
- 使用
innodb_buffer_pool_size
选项指定缓冲池大小。
刷新方法的调整:若数据库写入性能不足,调整 innodb_flush_method
以优化写入性能。
配置 fsync 阈值:innodb_fsync_threshold
可设置触发阈值,以便分散刷新写入,避免同一时刻大量 I/O 操作。
考虑非旋转存储
- 对于随机 I/O 操作,建议使用非旋转存储(如 SSD),而旋转存储更适合顺序 I/O 操作。不用机械硬盘
- 不同文件类型可分布在不同的存储设备上,如
file-per-table
表空间文件适合随机 I/O,而系统表空间、双写缓冲和重做日志文件适合顺序 I/O。
增加 I/O 容量:若由于检查点操作导致吞吐量定期下降,增大 innodb_io_capacity
的值,可避免积压造成的性能下降。
InnoDB 的 DDL 操作优化
- 使用
TRUNCATE TABLE
而非DELETE FROM
清空表,但若存在外键约束,TRUNCATE
会退化为DELETE
。 - 主键在 InnoDB 中是表结构的核心,因此应在创建表时确定主键,尽量避免后续的主键修改。
优化 MEMORY 表
- MEMORY 表适用于存储非关键性数据:可以为频繁访问但不需要持久化的临时数据提供高速存储。由于数据存储在内存中,因此在数据库重启或断电时将会丢失。
MyISAM 表优化
MyISAM 引擎虽然逐渐被 InnoDB 替代,但仍然在一些特定场景下应用(如全文检索和只读表),因此优化策略依然有用:
表碎片整理
- 使用
OPTIMIZE TABLE
语句整理 MyISAM 表以减少碎片,特别是频繁删除和更新的表会产生碎片。
延迟写入
- 设置
delay_key_write
为ON
可减少键缓存的磁盘 I/O,但在数据库崩溃时有丢失数据的风险,因此仅适合非关键数据。
索引缓存
- 使用
key_buffer_size
参数增加键缓冲区大小,可加速索引操作,特别适用于以 MyISAM 为主的读密集型应用场景。
读写锁机制
- MyISAM 不支持行级锁,只支持表级锁,容易在高并发下发生锁争用。若表不再更新,可以在应用层通过合适的分区策略来降低锁竞争。
内存优化 (MEMORY) 表的优化
索引选择
- MEMORY 表支持哈希索引和 BTree 索引。对于等值查询(例如
=
),哈希索引较 BTree 索引更快,但在范围查询时效率低于 BTree 索引。 - 若 MEMORY 表频繁进行范围查询(例如
<
、>
),建议使用 BTree 索引。
数据类型的选择:MEMORY 表的每行数据会保存在内存中,因此要合理设计数据类型以节省空间。例如,使用 VARCHAR
而非 CHAR
存储可变长度字符串。
定期刷新和清空:由于内存表不会持久化,建议在非关键数据使用完毕后定期清空或刷新表,以释放内存。
分区技术:当数据量庞大时,可以通过分区来管理多个 MEMORY 表,将不同分区数据放在不同的服务器内存中,减轻单一服务器的负载。
预读:读取少的时候多读一些放在内存里
落盘线程数(小于实例数量),落盘阈值
暖启动:即使关机,也把部分缓存落盘,然后恢复缓存
缓冲与缓存优化
缓存设置
设置Buffer size时,默认Chunksize是128。指令需要指定Buffer size大小和实例数量,如果设8G,16个实例,实际buffer是8G;但如果改成9G,实际会分配10G,因为9G/16不是128M的整数倍。,实例最多是64个,Buffer size最少是1G
缓冲池大小调整
- 对于 InnoDB 表,通过调整
innodb_buffer_pool_size
可以缓存更多数据,减少磁盘 I/O 操作,提高性能。
查询缓存
- 对于频繁重复的相同查询,开启
query_cache
可显著提升查询性能。但是,在高并发写操作场景下不推荐启用查询缓存,因为数据更新会导致缓存失效,增加系统负担。
索引缓存
key_buffer_size
参数用于控制 MyISAM 表的索引缓存大小。对于主要使用 MyISAM 表的应用,可以适当增加此参数以优化索引查询。
临时表和连接缓存
tmp_table_size
和max_heap_table_size
参数控制临时表的大小。在需要大数据集连接操作的场景下,可以增加这些参数以防止临时表写入磁盘。table_open_cache
参数控制表缓存数量。对频繁访问的表增大此参数可减少表打开和关闭的操作次数,提升性能。
InnoDB的缓冲池管理并不完全遵循传统的最近最少使用(LRU)算法,而是采用了一种改进的LRU策略,以减少那些仅访问一次的数据在缓冲池中的占用。这种方法帮助频繁访问的“热”数据留在内存中,同时让不常访问的数据更快地被替换掉。
InnoDB 会在后台执行一些任务,包括将缓冲池中的脏页(即已修改但尚未写入磁盘的数据页)刷新到数据文件。在 MySQL 8.0 中,缓冲池的刷新操作由页面清理线程负责,其数量由 innodb_page_cleaners
参数控制,默认值为 4。然而,如果页面清理线程数超过了缓冲池实例数,则 innodb_page_cleaners
会自动调整为与 innodb_buffer_pool_instances
相同的值。
当脏页的比例达到 innodb_max_dirty_pages_pct_lwm
参数设置的低水位值时,就会触发缓冲池刷新。默认低水位值为缓冲池页总数的 10%。如果将 innodb_max_dirty_pages_pct_lwm
设置为 0,则会禁用此早期刷新行为。
工作机制
- 中间插入点:新读入的页面不会直接被插入到LRU列表的头部(即最近使用的部分),而是被插入到距尾部3/8位置。这种插入方式能够防止那些仅访问一次的页面(通常由预读或全表扫描引入)挤占缓冲池中的频繁数据。
- LRU列表的双段结构:
- 新页面:新读入的页面首先进入缓冲池的3/8插入点位置,处于“冷”状态。
- 热页面:如果这些页面再次被访问,它们会被提升到LRU列表的头部(最近使用端),表明它们的重要性,从而在内存中保留更长时间。
- 淘汰策略:LRU列表尾部靠近末端的位置(在插入点之后)的页面被认为是“旧的”,在需要空间时优先被淘汰。这样,不频繁访问的数据被更快地淘汰,优化了缓冲池中频繁数据的保留。
这种改进的LRU设计能有效防止缓冲池被可能只使用一次的数据填满,从而保留足够的空间给真正的“热”页面,提升缓存效率。这种方法对于高吞吐量系统尤为重要,因为在这些系统中,避免缓冲池污染对于维持性能至关重要。
日志缓冲
innodb_log_buffer_size
控制事务日志缓冲区大小,适合有大量事务的场景。较大的日志缓冲区可以减少磁盘写入频率,但也会增加崩溃时的恢复时间。
总体优化建议
- 适配不同数据引擎:根据应用场景选择适合的引擎,例如事务型应用推荐使用 InnoDB,而读密集型和全文检索可选用 MyISAM。
- 索引策略:合理设计主键和索引,避免过长或冗余的主键。对高查询性能要求的应用,采用组合索引或覆盖索引。
- 分区和分表策略:对于大表,通过水平分区和分表策略来减少锁争用和提高查询效率。
- 事务管理:合理使用事务控制语句,减少长事务,尽量避免大批量数据的回滚操作。
- 监控与调整:定期使用性能分析工具(如
EXPLAIN
)分析查询性能,并持续调整数据库参数,以适应业务需求的变化。
第十三章
MySQL 备份与恢复概述
为什么需要备份与恢复?
保护数据免受:系统崩溃。硬件故障,断电。用户误操作(如误删数据)。典型应用场景:安全升级。数据迁移或复制环境搭建。
备份与恢复类型
1. 物理备份 vs. 逻辑备份
- 物理备份:复制数据库文件及目录。适合需要快速恢复的大型数据库,相对安全,但必须是同一种数据库。
- 逻辑备份:使用
CREATE DATABASE
和INSERT
等逻辑结构保存数据。适合数据量小或需要跨平台迁移的场景。执行慢,安全差、
2. 在线备份 vs. 离线备份
- 在线备份(热备份):
- 在数据库服务器运行期间备份。
- 对客户端影响较小,但需确保适当的锁定机制。管理很复杂,涉及到各种加锁。
- 离线备份(冷备份):
- 在数据库服务器停止时备份。
- 操作简单,快,但会中断服务。
- 温备份:
- 写操作中止,读操作正常
- 服务器运行但锁定数据,防止修改。
3. 本地备份 vs. 远程备份
- 本地备份:在 MySQL 服务器所在主机上执行。
- 远程备份:从其他主机进行备份。
4. 快照备份
- 借助第三方工具(如 Veritas、LVM 或 ZFS)生成文件系统快照。
- 特点:
- 快速记录文件系统或存储状态的方法,通过写时复制技术捕获某一时间点的数据,不复制实际数据,创建速度快、性能影响小,适合高频备份和快速恢复。
- 但依赖特定存储系统,并需保障数据一致性。
- 快照创建后,任何对文件系统的修改,都会将原始数据保留在快照中,新的数据写入另一个位置。实际上,由于copy-on-write技术,假如在某个时间我们做了一个全量备份,那么之后由于数据库的增删改,然后出现了文件数据的变化,文件相对于旧有的文件会有一部分变化,这部分变化就叫做快照!
- 本身就是增量式备份
- 注意:MySQL 本身不支持快照功能。
5. 完整备份 vs. 增量备份
- 完整备份:包含某一时刻的所有数据。
- 增量备份:保存自上次备份以来的变更数据(依赖二进制日志)。
数据库备份方法
背景知识:MySQL的结构其实是分两层的,一层是上面的Server,这是面向用户的,用户发过来的SQL请求,你来做处理;第二层是引擎,InnoDB or MyISAM,默认现在用的是InnoDB。引擎在这里负责的是和文件系统交互。有一个很大的buffer缓存。有很多log:undo log,redo log,整个这一层操作的时候它看不到SQL,只看到文件。
上面的SQL是在处理SQL的,它对应的log就是bin-log,它记住你执行的所有的SQL操作。这两层之间是可以解耦的。解耦的好处就是,我在linux或者windows上面安装MySQL,我只需要改engine这一层就可以了。
Mysqldump
mysqldump
是 MySQL 提供的一个备份工具,用于导出数据库的结构和数据,常用于数据迁移或备份。
核心功能
- 数据库备份:支持将一个或多个数据库导出为 SQL 文件,包含表结构和数据。
- 表结构导出:仅导出表的创建语句而不包含数据。
- 数据恢复:生成的 SQL 文件可以通过
mysql
命令导入,完成恢复。 - 灵活的过滤:支持按表、按条件导出特定数据。
恢复方法
- 1. 恢复完整备份:直接从最近的完整备份恢复数据。
- 2. 时间点恢复:结合完整备份和二进制日志(bin-log),恢复到特定时间点。(先从全量备份获取,接着bin log里面恢复到特点时间)
假设我们的备份策略是每个周日下午一点进行一次全量备份,也就是把数据库里面的所有数据全备份一次。那么,在两次全量备份之间,数据库每次执行写入操作的时候,都会对bin-log文件进行操作,写入增量备份。也就是记录用户对数据库的所有的操作变更。现在假如在周三的中午,数据库主机突然崩了,那么我们首先要恢复到最近一次做的全量备份:好了,现在就已经恢复到最近一次全量备份的状态里,然后要从上次全量备份到崩溃期间的binlog文件全部读取出来,然后恢复。如果每次磁盘坏死或者磁盘错误,基本上就能恢复到原来的状态。但是如果有一个binlog文件崩溃了或者寄了,那就可能恢复不到周三崩溃的时候的状态了,可能只能恢复到周二的下午或者某个稍微更早的时间点。
备份策略
- 定期备份:根据数据重要性和访问频率设置备份频率。
- 验证备份:确保备份文件完整,随时可恢复。
- 备份存储安全:使用加密技术并存储在安全、冗余的环境中。
Binlog、Redo Log 和 Undo Log 对比表
特性 | Binlog(归档日志) | Redo Log(重做日志) | Undo Log(回滚日志) |
---|---|---|---|
主要用途 | 记录所有对数据库的更改,用于主从复制和数据恢复 | 确保已提交事务的持久性(崩溃恢复) | 支持事务回滚和多版本并发控制(MVCC) |
格式 | 二进制格式(不可读) | 二进制格式(不可读) | 二进制格式(不可读) |
存储位置 | 存储于磁盘,可轮换或归档 | 存储于 MySQL 数据目录 | 存储于 InnoDB 表空间中 |
使用范围 | 数据库全局事务级别更改 | 仅适用于 InnoDB 的数据页更改 | 仅适用于 InnoDB 的事务回滚操作 |
恢复角色 | 用于点时间恢复和主从复制 | 用于崩溃恢复时重做已提交的事务 | 用于崩溃恢复时回滚未提交的事务 |
应用场景 | 主从复制、故障后的数据恢复 | 数据库崩溃后的事务恢复,确保数据持久性 | 事务回滚、多版本并发控制下的一致性读取(MVCC) |
类型 | 逻辑日志 | 物理日志 | 逻辑日志 |
第十四章
MySQL 分区
一、简介
当一个表里面的数据太多了的时候,占用的空间太大,可能就接近磁盘的存储空间,这时候就需要分区,把表的数据拆开(分区),然后让不同分区的数据存储到不同的磁盘上面。我们要做的事情就是,让这些多个节点上存储的东西在逻辑上是一张表!
分区是将表中的数据划分到不同的存储区域,以优化存储和查询性能的技术。MySQL 从 8.0 开始支持分区功能,主要通过 InnoDB 和 NDB 存储引擎实现。
分区的基本概念
- 水平分区(Horizontal Partitioning): 将表的行数据分布到多个分区中。
- 垂直分区(Vertical Partitioning): 将表的列数据分布到多个分区中。(MySQL 不支持)
二、分区的类型
MySQL 支持以下 分区类型,根据场景选择适合的方案:
1. 范围RANGE 分区
- 指定了某一列或者某几列,根据他们值的范围,然后来分区。区域应该是连续而且不能重复的,通常都是用的LESS THEN(都是小于啊!没有等于),比如<0的分个区,<50的分个区,< MAX_VALUE(这玩意就是保证最后所有最大值都给你包容进去)分个区。
- 不允许用float浮点数。臧老师的ICS告诉我们,浮点数的问题是很多的!因为浮点数是近似表示。如果制定了多列,比如a、b、c,能不能把a的列做一个平方或者函数计算一下处理?不行!不接受表达式。
- 多列比较的是向量,比如要比较(a,b,c)(d,e,f),就比较a和d,比较出来小于的话就小于,比较不 出来就是比较b和e,然后同理比较c和f
举例: 定义一个员工表,根据员工ID分区,1~10号员工一个分区,11~20号员工一个分区,依次类推,共建立4个分区。
2. 列表LIST 分区
基于离散值列表的匹配。列表分区和范围分区类似,主要区别是list partition的分区范围是预先定义好的一系列值,而不是连续的范围。比如交大有闵行、七宝校区等等就可以根据这些校区的值来确定分区。
CREATE TABLE members (
firstname VARCHAR(25) NOT NULL,
lastname VARCHAR(25) NOT NULL,
username VARCHAR(16) NOT NULL,
email VARCHAR(35),
joined DATE NOT NULL
)
PARTITION BY LIST (YEAR(joined)) (
PARTITION p0 VALUES IN (2000, 2001, 2002),
PARTITION p1 VALUES IN (2003, 2004, 2005),
PARTITION p2 VALUES IN (2006, 2007, 2008)
);
- 定义某个列的取值范围,根据取值范围来分区。适用于列值在多个离散集合中的情况。适用于列值在多个离散集合中的情况。如果采用Ignore标识符,插入多个行,有的合法有的不合法,系统会插入合法的,丢弃不合法。
3. HASH 分区
基于用户定义的表达式返回的整数值,将数据分散到不同分区中,然后决定是哪一台服务器存储,一般在非负的值上面处理。
- 分区的数量通过
PARTITIONS
指定。
4. KEY 分区
类似于 HASH 分区,但使用 MySQL 内置的哈希函数,Mysql自动根据主键散列,且支持非整数列。
- 适合需要简单定义分区规则且无需自定义表达式的场景。
三、分区的优势
1. 提升查询性能
- WHERE 子句过滤: 查询只访问满足条件的分区,大幅减少无效扫描。
- 显式分区选择: 通过显式分区选择,用户可以指定查询访问特定的分区,从而显著提升查询速度。例如:
SELECT * FROM employees PARTITION (p0, p1) WHERE job_code < 1000;
在这个例子中,MySQL 会仅查询 p0
和 p1
分区的数据,而忽略其他分区,这在已知分区位置的情况下非常高效。
2. 方便的数据管理
- 删除分区: 当某些数据失去效用时,可以直接删除包含这些数据的分区。例如,删除包含历史数据的分区比删除单独的行更加高效。
- 添加新分区: 对于持续增长的数据,添加新的分区可以避免迁移大量数据,提高扩展性。例如,当需要为新的年份数据创建一个新分区时,只需添加一个分区而无需重建整个表。
3. 增强的数据维护
- 分区技术使得数据库维护变得更加灵活和高效。例如,可以通过定期维护分区来提升数据库性能,或者在需要时通过数据的生命周期管理对分区进行清理和归档。数据可以跨磁盘/文件系统存储,适合存储大量数据。
对于应用来说,表依然是一个逻辑整体,但数据库可以针对不同的数据分区独立执行管理操作,不影响其他分区的运行。而数据划分的规则即称为分区函数,数据写入表时,会根据运算结果决定写入哪个分区。
四、注意事项
- 分区键的选择:
选择分区键时要谨慎,应该选择查询时常用作过滤条件的列。分区键的选择会影响性能,因此需要根据具体的查询场景来决定分区策略。 - 查询优化:
分区虽然可以提升性能,但也需要谨慎设计。例如,WHERE
子句中涉及分区键的查询可以充分利用分区优化,但如果查询条件没有涉及分区键,则可能无法获得预期的性能提升。 - 性能的平衡:
分区可能带来额外的管理开销,特别是在分区数目较多时,MySQL 在处理大量分区时可能会出现性能瓶颈。因此,在设计分区时,要考虑合适的分区数量和数据的分布方式。 - 无法跨分区 JOIN:
分区表内的 JOIN 操作通常会受到限制,特别是在跨多个分区查询时。如果 JOIN 语句的表或条件不涉及分区键,可能会导致性能下降。 - 分区的维护:
虽然分区可以简化数据管理和查询优化,但也需要定期对分区进行维护。数据迁移、分区扩展、删除不再需要的分区等工作都需要定期进行。 - 存储引擎限制:
分区表通常依赖于 InnoDB 或 NDB 存储引擎,且某些操作(如全文索引、外键约束等)可能不完全支持分区表。因此,在使用分区功能时,需确认存储引擎的支持情况和业务需求。
- NULL处理:如果遇到了NULL数值,这种插入的东西会被放到最小的一个分区里面(范围最低的一个分区)而对于List分区,允许in Null专门创建一个分区;对于Hash分区,null都会被当做0然后送到hash函数里面
- 调整分区(重新组织分区):比较耗时间,调整涉及到整个表格
- 删除分区:如果用的分区规则全是小于,那么删除一个分区就会导致原来存储的数据删除,但是后面新的输入插入进来的时候就会进入正确的分区。
- 特别提示:不允许drop 分区(通过Hash分区或者key散列的)!一旦drop,那么整个分区的数量就发生变化,那么假如之前是取模运算,现在分区数量改变,所有的存储都要发生大变化!除非重新调整,而且不是用reorginaze,用的语法是coalesce!但是追加可以直接进行!具体原因没有讲,有自己的实现!
第十五章
NOSQL
1. 大数据背景
- 大数据现状:
- 好消息:大数据已成为现实。
- 坏消息:如何存储和分析大数据是巨大的挑战。
- 数据类型:
- 结构化数据:有明确格式(如 XML 文档、数据库表等),传统关系型数据库(RDBMS)擅长处理。
- 半结构化数据:有一定的结构,但可能会被忽略(如电子表格)。
- 非结构化数据:无内部结构(如纯文本、图像数据)。
2. NoSQL 数据库
- 定义(Not only SQL):
- 传统 RDBMS 无法高效存储和处理海量数据,非结构化数据,NoSQL 数据库作为补充技术应运而生。
- 只要数据分布式存储,需要增加可靠性,就需要副本replication
- 分布式存储,使用关系型行不行?我们在分区的时候讲到了,如果一张表数据量很大,我就要拆,然后这种操作可能又需要加锁。
- 示例:MapReduce 框架。
维度 | 传统 RDBMS | MapReduce |
---|---|---|
数据规模 | Gigabytes(千兆字节级别) | Petabytes(PB 级别,超大规模) |
访问方式 | 支持交互式和批量(Interactive and batch) | 仅支持批量(Batch) |
更新模式 | 读写多次(Read and write many times) | 写入一次,读取多次(Write once, read many times) |
数据结构 | 静态模式(Static schema) | 动态模式(Dynamic schema) |
数据完整性 | 高完整性(High) | 完整性较低(Low) |
扩展性 | 非线性扩展(Nonlinear) | 线性扩展(Linear) |
DAO和Repository差异
- 一般来说,一个Repository对应的一个类型的数据库里面的一个表。
- DAO的任务是需要屏蔽不同数据库差异,让服务层的眼里只有一个一个的对象。比如对于“用户”,用户的头像可能存在MongoDB,其他的用户名邮箱存在SQL里面,DAO就要完成组装。
MongoDB
- MongoDB 是什么?
- 一种文档型 NoSQL 数据库,开源,C++编写。
- 名字来源于“humongous”(庞大)。
特点:
- 文档式(Document-Oriented)存储
- 全索引支持(2D),支持地理空间索引
- 高可用性(通过复制和自动分片partition)
- MapReduce 支持
- 内嵌文件系统(GridFS)
- 支持Map Reduce
- collection是模式自由(schema free)的
- 权限控制(就不同的人可以访问不同的数据库),每个数据库存储在单独的文件
在一个集合(Collection)中可以放置不同的文档(Document),并且它是无模式(Schema-Free)的。从理论上来说,确实可以将所有数据都存储到一个集合中。然而,这么做是否合理需要具体问题具体分析。
关系型数据库和非关系型数据库的区别
在关系型数据库中,不同的表通常对应不同的数据结构。例如:
- 人员信息会存储在“人员表”中
- 订单信息会存储在“订单表”中
- 书籍信息会存储在“书籍表”中
原因是这些数据的结构不一样,而在关系型数据库中,表是严格受模式约束的,所以你需要将数据按结构拆分到不同的表中。而在像 MongoDB 这样的非关系型数据库中,文档本身是可以具有不同字段的,因此从技术上讲,你确实可以把所有类型的数据都放在一个集合里。
为什么仍然需要多个集合?
尽管 MongoDB 是无模式的,但将所有数据放到一个集合中在实际开发中会带来许多问题:
- 开发与维护的复杂性
- 在一个集合中存放不同类型的文档会让开发人员和管理员的工作变得复杂。因为需要额外的逻辑来区分和处理不同类型的数据,容易出错且难以维护。
- 效率问题
- 获取集合的列表要比从一个集合中提取所有数据类型的列表快得多。
- 将同类文档分组存储在同一个集合中,有助于实现数据的局部性(Data Locality),从而提高查询效率。
- 索引的作用
- 当为集合创建索引时,就开始对文档结构施加了一定的约束。
- 如果集合中包含多种类型的文档,索引的设计会变得复杂,性能也会受到影响。
虽然理论上可以将所有数据存储到一个集合中,但在实际应用中,根据文档类型拆分为不同的集合是一种更合理的设计方式,这样可以减少复杂性,提高性能和可维护性。
基本术语
- 文档 (Document):
- 数据的基本单元,相当于关系型数据库中的一行。
- 格式:
{"key": "value"}
,可嵌套。- 键不能包含空字符
\0
。 - 特殊字符如
.
和$
应避免使用。 - 键名区分大小写,且文档中键名不能重复。
- 键不能包含空字符
- 集合 (Collection):
- 文档的分组,相当于关系型数据库中的表。
- 无固定模式 (Schema-free),集合中的文档可以有不同的结构。
- 组文档而成,命名规则:
- 不能使用空字符串。
- 不建议以
system.
开头。 - 可以通过
.
表示子集合。
- 数据库 (Database):
- MongoDB 中的数据库由多个集合组成,每个数据库独立存储、具有独立权限。
- 特殊数据库:
admin
:用于权限管理。local
:不被复制,适用于本地存储。config
:用于存储分片配置。
MongoDB 的复制与分片
复制 (Replication):
- 提供高可用性,通过主从复制实现。
- 主节点接受写操作,从节点同步数据。
分片 (Sharding):
- 用于大规模数据分布式存储。
- 数据分区分布在多个节点(Shard)上。
Shard
自动分片
- Sharding指的是分片,MongoDB支持自动分片;假如一个collection比较大,他就会把collection切成几个shard,shard其实还有更小的单位就是chunk,把这些chunk分布到多台机器上面存储。
- 此外MongoDB会自动保证不同的机器之间,chunk的数量差异小于等于2
- 分区之后,还有一些配置的元数据需要记录,在config配置服务器存储,它相当于一个路由器,针对访问的请求结合元数据,把请求转发到对应的分片存储的数据服务器来获得结果
- 这样的好处就是存储的压力比较平衡,当然也支持人工的管理,因为有时候数据的冷热有差别,数据访问的频率有差别,这种情况就会手动分片,把一些经常访问的数据均衡放置,因此这种情况下,可能某一台机器存储的大小比较大,不同机器之间chunk数量的差距可能超过了2(为什么有时候也需要人工管理sharding分片)
shard的依据:需要选择一列,作为一个key,这个key就叫做shard key。例如A-F开头学生姓名作为一个分片,例如G-P开头学生姓名作为一个分片。
sharding时机:1.当前计算机上的磁盘空间已用完。2.希望写入数据的速度快于单个mongodb所能处理的速度。(比如大量的传感器往数据库1s可能要写入1GB的数据,如果来不及写,就需要多个MongoDB来写入)3.希望在内存中保留更大比例的数据以提高性能,分布式缓存
假设开始的时候分片,每个chunk代表的是某一个范围的数据,chunk就会被分布式的存储在shard服务器里面。当往一个chunk里面不断插入数据的时候,这个chunk可能就会分裂,分裂成多个chunk。考虑到前面所说的不同的shard服务器之间chunk的数量差异不能超过2个,那么一旦分裂之后数量违反了这个规定,MongoDB就会自动的调整chunk在服务器之间的位置,保证均衡。
保证了存储数据负载平衡,均匀分布(类似于B树,不同shard的chunk差值超过一定范围就分裂)
什么时候用MongoDB
为什么我们需要mongoDB?这个一定要从它是schema free的这一点来出发。
三星和苹果的手机,就它俩的属性不完全一样,也就是说大家是 schema free 的,就你有你的属性,我 有我的属性,在这种情况下,那我把它存到一起都在这个 mobile 这个 collection 里。
你如果用三星里面它特有的,比如说它价格比较便宜,用它特有的价格去搜索,尽管 apple 里面没有 price 这一个属性,你这个搜索的语句一定能搜出来符合要求的东西。所以你是在 schema free 的这样 一个数据库上去存了大家不一样的数据,然后我的搜索还可以按照不一样的属性去给它找出来,不是说 大家都要有相同的这个属性,所以这个场合才是mongoDB很有用的地方。
还有什么是 schema free 的?其实有一个东西是,就是如果我对书有一些评论。那是不是就是树形的? 就是说这本书还有一个评论,它还可以有跟帖,它的跟帖还可以有跟帖,它的跟帖可能有好几个,跟帖的跟帖也可以继续下去。然后另外一本书它也有,但是它到底有几层?然后每一层的跟帖有多少个?这是不一定了。
就大家这个是没有什么说,大家的跟帖最多到三层,每一层最多 5 个,然后我就做了个表,没有的,那 这时候你看到你要从这种书评的话,那它确实就是 schema free 的,然后你就可以把这些东西就存成一 本书的某一个属性往里面去追加。 那如果你要在 MySQL 里去存,那它最带来的最大问题就是,那你要去找这个书评的时候,你这么这么多层,你怎么去处理?你是把列转成行存呢?还是说你怎么用其他的 方法?反正就比较麻烦,所以这个是想给大家解释一下,就是你要用 mongoDB,你千万别觉得他只是拿来存图片用的。
第十六章
Neo4J 和图计算
一、图数据库简介
- 什么是图 (Graph)
图是由顶点(vertices/nodes)和边(edges/relationships)组成的数据结构。
- 节点表示实体(如人、物体)。
- 边表示实体之间的关系。 图的特性:
- 节点具有属性(key-value 对)。
- 边也可以包含属性(例如权重、时间戳等)。
- 边有方向性,且始终连接两个节点。
- 什么是图数据库 (Graph Database)
图数据库是一种支持图模型存储和操作的在线数据库,提供标准的CRUD操作:创建(Create)、读取(Read)、更新(Update)、删除(Delete)。
- 优化目标:事务性能(OLTP)和操作可用性。
- 两大核心组件:
- 底层存储(Underlying Storage):决定如何存储数据。
- 处理引擎(Processing Engine):决定如何查询和操作数据。
- 传统关系型数据库(RDBMS)和NoSQL数据库处理关系复杂性效率较低。
- 图数据库天然支持复杂关系建模和高效的关系查询。
二、图数据库与其他存储方式的比较
- 关系型数据库的缺点:
- 多表 JOIN 操作复杂且低效,随着关系深度增加性能下降。
- 示例:获取 Bob 的朋友的 SQL 查询:
sql SELECT p1.Person FROM Person p1 JOIN PersonFriend ON PersonFriend.FriendID = p1.ID JOIN Person p2 ON PersonFriend.PersonID = p2.ID WHERE p2.Person = 'Bob';
- NoSQL 数据库的缺点:
- 缺乏关系模型,主要适合非结构化或弱关系数据。在那种关系比较复杂的关系存储的时候,查询的代价也会非常大。
- 图数据库的优势:
- 直接表达实体与关系。
- 高效查询深度关系,例如朋友的朋友(Friends-of-Friends)。如果用mysql存储的话,在查找一个人好友的好友的好友需要经过多次 join连接操作(要先做笛卡尔积,然后根据某个条件进行进一步筛选),这种在复杂的查询的情况 下,mysql的查询的效率就非常低。
三、Neo4J 的图模型
- 标注属性图模型 (Labeled Property Graph Model)
- 节点 (Node):表示实体,存储属性。
- 属性是键值对(Key-Value)形式,支持字符串、数组等类型。
- 关系 (Relationship):连接节点,带有方向性、名称和属性。
- 例如:
(Alice)-[:KNOWS]->(Bob)
表示 Alice 知道 Bob。
- 例如:
- 标签 (Label):为节点分类,表示角色或群组。
- 查询语言 Cypher
Cypher 是 Neo4j 的查询语言,语法简洁直观,类似 ASCII 艺术。
- 基本语法:
MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c) WHERE a.name = 'Jim' RETURN b, c;
四、数据建模与图应用开发
- 数据建模
- 节点表示事物:使用节点代表感兴趣的实体,如用户、设备等。
- 关系表示结构:用方向性关系表达实体间的关联。
- 属性存储特性:节点和关系可带有属性,用于记录相关信息(如权重、时间戳等)。
- 语义清晰:通过关系方向明确实体间的语义。
- 应用示例:书籍推荐系统
- 需求:作为读者,我希望知道喜欢某书的其他读者还喜欢哪些书,以便寻找阅读建议。
- 实现查询:
cypher MATCH (:Reader {name: 'Alice'})-[:LIKES]->(:Book {title: 'Dune'}) <-[:LIKES]-(:Reader)-[:LIKES]->(books:Book) RETURN books.title;
- 事实建模为节点
示例:描述一位员工的职位信息。
CREATE (:Person {name: 'Ian'})
-[:EMPLOYMENT]->(:Job {start_date: '2011-01-05'})
-[:EMPLOYER]->(:Company {name: 'Neo'}),
(:Job)-[:ROLE]->(:Role {name: 'Engineer'});
五、Neo4J 部署与运行
- 运行 Neo4J:
- 控制台模式:
<NEO4J_HOME>/bin/neo4j console
- 后台运行:
<NEO4J_HOME>/bin/neo4j start
- 控制台模式:
- 嵌入式模式:与应用程序同进程运行。
- 服务器模式:独立部署,通过客户端访问。
六、Neo4J 内部存储机制
- 存储文件:数据分块存储到不同文件中(节点、关系、标签、属性分别存储)。
- 关系存储的双向链表:节点与关系间使用双向链表连接,快速遍历关系网络。
- 节点的存储占用15byte:第一个位是一个标志位,是否有关联的边是否正在使用,然后(偏移量 1)是存储下一个边的ID关系,然后存储下一个属性的id,然后存储labels,最后存储其他的内容
- 边的存储占用34byte,第一个位是一个标志位,存储是否正在使用;之后存储这个关系的起始节点 是谁,终止节点是谁、关系的类型;然后存储第一个节点(边的起始节点)的前一个关系是谁,后 一个关系是谁,第二个节点的前一个关系是谁,后一个关系是谁,后面就是保留字段
- 是通过链表连起来的
第十七章
日志结构数据库 (Log-Structured Database)
LSM-Tree(Log-Structured Merge Tree)
定义:LSM-Tree 是一种 分层、有序、面向磁盘 的数据结构。其核心思想是利用磁盘 顺序写性能远高于随机写性能 的特点来优化写入。
特点:
- 数据写入:以 Append 模式写入,避免删除和修改,提升写性能。
- 适用场景:写多读少 的场景,如日志系统和时序数据库。
优点:
- 显著提升 插入、修改、删除 性能。
- 适用于 实时、时序数据存储。
- 数据热度与 层级(level) 相关。显然读新数据会更快。
缺点:
- 牺牲部分 读取性能:一次读取可能需要访问多个层。
- 读放大 和 写放大 问题:数据多层管理可能导致多余的 I/O 操作。
- 写放大 :为了完成一项写操作,数据库系统需要在底层存储中写入的字节量大于实际的数据量。比如说LSMTree合并,Compaction就有。
- 读放大:为了完成一次读操作,系统需要额外读取大量的无关数据。比如说LSMTree读取某个值,实际要读取很多块。
SSTable (Sorted String Table)
定义:SSTable 是一种 <键, 值> 的存储格式,支持有序存储。
特点:数据顺序写入,提升磁盘性能。支持高效的随机读取。
限制:
- 写入磁盘后不可变,修改或删除需要大量 I/O 操作。
- 不适合随机写入。
写阻塞:写入的时候可能有个阻塞问题,为啥?因为它写入的时候它有可能需要去做这个合并,比如 L0 层满了 之后,它要往 L1 层去落,那么我刚才讲了一个极端情况是 L1 层你落下来之后也满了,又要往 L2 层 落,以此类推,每一层可能都会满。
碰到这种情况怎么办?他就说这样在做 compaction 的时候,把内存里的落下来这个动作直接做了以 后,如果触发了从 L 0 到 L1 层的这个动作,就转移到后台去做异步的写入硬盘就不要在前台,就是说 它只要落下来马上就给用,就可以去变成一个可写的一个内存表,马上就可以接收新的请求进去,后面 就放到这里面,这就所谓的写阻塞,就是说你在写的过程当中就很容易不断的往下落,然后就看到这个 放大就很严重,就这一次其操作其实执行了很多写的动作,然后还有可能成为这个瓶颈,这样就阻碍了 这个事物的处理的可用性。
读放大:那读呢?是存在这么一个问题,就是我在找一个数据的时候,我先到内存表里;找不到,我再到这个 L0 层找,找不到我也不能说这数据不存在,有可能它太老,它落到底下去了,于是我一层层找,极有 可能就是最后你在很找了很多层你才找到。而且你在不同的层里面可能还存着不同的版本。因为我们刚 才说了,它的改写是通过追加得到的,那也就是说你如果有老版本的话,它会在更低的层里面,如果没有经过compaction,这个数据还是在的,只有 compact 才有一次机会可能把它给删掉。限制了AP查询 的性能。
解决方案:
LevelDB 和 RocksDB
LevelDB:基于 LSM-Tree,支持层级压缩(Compaction)以优化磁盘空间使用。
RocksDB:
- 由 Facebook 开发,作为 LevelDB 的扩展。
- 支持更高性能,尤其适用于 混合事务分析处理(HTAP)。
- 提供嵌入式、持久化的键值存储,优化了日志结构数据库的性能。
RocksDB 的优化
写入流程:
- 数据先写入内存,再后台异步写入磁盘,降低写延迟。
- 问题:
- 内存写满时可能引发阻塞。
- 后台任务(Compaction)可能成为性能瓶颈。
读取流程:需要访问多层数据文件,导致 读放大问题。
解决方案:
- 写阻塞:通过中间缓存层分发写入任务,缓解写入压力。
- 读放大:引入 列式存储 优化分析性能。
混合存储 (HTAP)
OLTP 和 OLAP:
- OLTP:
- 在线事务处理,特点是低延迟、高并发。
- 适合 行式存储。
- OLAP:
- 在线分析处理,特点是高延迟、低并发、大数据量。
- 适合 列式存储。
混合存储策略:
- 行式存储适合事务数据,列式存储适合分析数据。
- 通过 动态格式转换 降低磁盘开销。
向量数据库 (Vector Database)
向量化:向量(embedding)由 AI 模型(如大型语言模型,LLM)生成,包含大量维度信息。向量的维度表示数据的特征,可用于捕获模式和关系。
用途:管理和存储高维特征数据,支持基于相似性的查询。
向量数据库的工作原理
核心:基于 近似最近邻(Approximate Nearest Neighbor, ANN) 搜索算法找到与查询向量最相似的向量。
主要算法:随机投影(Random Projection):将高维数据投影到低维空间,减少计算复杂度。
权衡:准确性 vs. 速度:向量数据库通常提供近似结果,需在性能和准确率之间做出取舍。
向量数据库的应用
典型工具:Pinecone 等。
场景:AI 推荐系统、图像/文本检索等。
核心算法
- 欧几里得距离(L2 距离)
定义:计算两点之间的直线距离,公式为 (d = \sqrt{\sum (x_i – y_i)^2})。
特点:对绝对数值敏感,适用于数值特征。
适用场景:在特征分布较为均匀,且对具体数值变化较为敏感的场景,例如推荐系统中的用户偏好。 - 余弦相似度
定义:计算两向量夹角的余弦值,公式为 ( \text{cosine}(A, B) = \frac{A \cdot B}{||A|| \cdot ||B||} )。
特点:只关注向量的方向,忽略了大小,适合文本数据。
适用场景:文本相似度计算,如信息检索、文档聚类,尤其在特征表示为词向量时。 - 曼哈顿距离(L1 距离)
定义:计算两点之间在各个维度上的绝对距离之和,公式为 (d = \sum |x_i – y_i|)。
特点:对异常值较为稳健。
适用场景:特征空间稀疏的情况,或者当特征之间有明确的绝对值意义时。 - 杰卡德相似度
定义:计算两个集合的交集与并集的比率,公式为 ( J(A, B) = \frac{|A \cap B|}{|A \cup B|} )。
特点:适用于集合数据,强调元素的存在与否。
适用场景:推荐系统中的用户行为相似度、文档去重等。 - 皮尔逊相关系数
定义:衡量两个变量之间的线性关系强度,计算公式为 ( r = \frac{cov(X, Y)}{\sigma_X \sigma_Y} )。
特点:对均值和标准差进行了归一化处理,适合衡量线性关系。
适用场景:用户评分数据的相关性分析,适用于推荐系统中的协同过滤。
第十八章
TSDB
什么是时序数据库(TSDB)?
时序数据库(TSDB)是专门为处理带有时间戳的数据(即时序数据)而优化的数据库。时序数据指的是随时间变化的测量或事件。例如,服务器的性能监控、网络数据、传感器数据、点击事件、市场交易等,都是时序数据的典型例子。时序数据库主要用于处理这些与时间相关的度量和事件数据。
特点:
- 时序数据的生命周期管理、数据总结和对大量记录的扫描,使得时序数据不同于其他数据类型。
- TSDB 优化了对时间相关数据的管理,能够高效处理随时间变化的数据。
具体而言:
- 有存活状态,设置了存活时间
- 格式简单
- 存储时根据差值,进一步压缩
- 所有数据都有时间戳
时序数据库的重要性
如今,几乎所有物理世界的表面都有传感器,城市的街道、工厂、卫星、衣物、手机等都在不断地产生时序数据。这些数据量大、来源多、监控需求强,传统数据库很难应对如此庞大的数据处理需求。为此,需要一种具有高性能、可扩展、专为时序数据设计的时序数据库。
时序数据:比如说一个地图,我这是一个,比如说一个花园,我在里面布了很多的传感器,这些传 感器就是它的温度和湿度,我来做这个自动浇灌,然后他们就会源源不断的产生数据,我要把它存 起来,那可以看到它们源源不断产生数据,于是我们看到这数据有几个特征。
- 第一个:这个数据只要你往上发,它一定带一个时间戳,我要知道你这个数据是什么时间发上 来?这是第一个问题。
- 第二个:你一定会有大量的这种传感器在传数据,所以它必须要支持这种高并发,就吞吐量要 大,这种吞吐量必须要大才能支持高并发。
- 第三个:你说传感器如果他在报数据,他偶尔丢掉一两个或者一两个不太准,它会不会影响你 的这个数据整体的效果? 就是他如果说每一秒钟都报一个数据,在某一秒他突然丢了一个数据,它会不会影响你做统计 的效果?就理论上来说,它里面的数据允许少量的不准确或者是缺失,也就是说它的数据和那 种 transaction 的数据就形成了鲜明的对比。
- 第四个:就是其实我们对单点的数据是不感兴趣的,你说我要知道某一个时间点,这里面每一 秒都要发数据,我要知道在某一秒的数据,这个肯定你不太关心,你关心的是一个时间区间 内,比如说在一小时内它的数据什么样的,我要去求和。所以在持续数据库里,他对单点数据 的访问不会特别多,他经常做的是对一块数据,尤其是时间片划分出来的一段数据,他要去做 一个访问。
- 整体的存放是用了类似lsm日志结构合并树的结构;时序数据库的生命周期一般比较短,比如一个 传感器,可能我们只关心最近一周的或者一个月的数据,超过的时间的数据就可以删除(比如当掉 到L0以下,我们就将其压缩,甚至删除)。(类比关系型数据库的订单数据,订单数据必须要持久 化保存,但是这种时序数据库可以通过摘要压缩一下,或者直接删掉)
- 不太可能会建立索引,例如记录温度随时间的变化,一般不会有必要建立索引(比如建个温度在时 间的索引,这没有意义)
- 对于时序数据库接受的数据的特点:更多的数据点,更多的数据源,更多的监控,更多的控制.
优化:1.可以不记录时间戳:如果传输来的数据比较稳定的话,例如每秒1个数据,我可以记录某一个特定 数据(比如开始的数据的时间戳)然后后面每秒记录一个数据就好。2.可以记录数据的增量:比如记录温度,变化比较小。我就记录相比上一个数据增加或者减少了多少 度。这样存储的数据的空间得以节省了,所以可以存更多的数据。
InfluxDB
InfluxDB 是一个专门为时序数据构建的平台,旨在处理由传感器、应用程序和基础设施生成的大量时间戳数据。它帮助开发者快速构建用于分析、物联网(IoT)和云原生服务的实时应用程序。
核心特性:
- 处理大规模、高频率的时序数据。
- 适用于需要高性能和可扩展性的应用场景,如 IoT、云计算等。
InfluxDB 的核心概念
InfluxDB 使用多个元素来存储和管理数据。主要包括:
- 时间戳(Timestamp):所有数据都包含时间戳,表示数据记录的时间。InfluxDB 使用 RFC3339 UTC 格式显示时间,并支持纳秒级别的精度。
- 测量(Measurement):测量是数据的容器,包含时间戳、标签、字段等信息。通过测量名称可以清晰地描述数据的内容(例如 “census” 记录的是关于蜜蜂和蚂蚁的数量)。
- 字段(Fields):字段包括字段键(field key)和值(field value)。字段值可以是字符串、浮动值、整数或布尔值。
- 字段键(Field Key):字段的名称。
- 字段值(Field Value):字段的值。
- 标签(Tags):标签是元数据的集合,包括标签键(tag key)和标签值(tag value)。标签被索引,因此在查询时速度较快,适合存储常查询的元数据。
- 标签键(Tag Key):表示标签的类别,如 “location” 和 “scientist”。
- 标签值(Tag Value):标签键的具体值,如 “klamath” 和 “portland”。
InfluxDB 的设计原则
InfluxDB 在设计时考虑了以下几个原则,旨在优化时序数据的处理:
- 时间顺序的数据写入:为了提高性能,InfluxDB 强制数据按时间升序写入。
- 严格的更新和删除权限:为提高查询和写入性能,InfluxDB 限制了对数据的更新和删除操作。时序数据通常是一次性写入,不需要频繁更新。
- 优先处理读写请求:InfluxDB 优先处理读写请求,而不是强一致性保证。这意味着查询可能不会包括最新的数据,但最终数据会达到一致性。
- 无模式设计(Schemaless Design):InfluxDB 采用无模式设计,更灵活地管理不连续的数据。
- 数据集优于单个点:由于时序数据的每个点在整体数据集中的作用较小,因此 InfluxDB 提供了强大的聚合工具来处理大量数据,所以没有ID这个概念,就是按照时间戳去拿;对于多个时间点重复数据,不会存两次(减少占用)
- 幂等性。应用场景里面就是大量的传感器,会把大量的数据通过不可靠的或者不那么可靠的网络传 递过来,那我就不能保证它没有被发送多次,但是它发送多次它也不能对我的数据库产生影响,我 只存一个(根据时间戳)。比如除了value之外的部分数据都是相同的,这样的数据提交了三次, 不会像mysql一样插入三行相同的数据,InluxDB会用最新的一个数据存储。
InfluxDB 的存储引擎
InfluxDB 的存储引擎确保数据安全、查询结果准确并且具有较好的性能。存储引擎的组成部分包括:
- 写前日志(WAL):用于确保数据的持久性。
- 缓存(Cache):数据首先写入内存缓存,并且可以立即进行查询。
- 时间结构合并树(TSM):用于存储和压缩数据,以提高查询效率。
- 存储的是差值,delta变化(针对于某一个点的初始值)
- 按照时序的key去找
- 时序索引(TSI):用于加速时序数据的索引和查询。
InfluxDB 数据写入过程
当数据通过 API 被写入 InfluxDB 时(用),数据首先被写入到 WAL 中,然后被压缩并写入磁盘。在内存缓存中,数据是立即可查询的,缓存定期写入磁盘。随着 TSM 文件的积累,它们会被合并和压缩成更高层次的 TSM 文件。
InfluxDB 的数据结构优化
InfluxDB 提供了针对数据模型优化的工具:
- Series:一个系列是具有相同测量、标签集和字段键的点集合。系列帮助分组具有相似属性的数据。
- Point:一个数据点包括系列键、字段值和时间戳。
数据存储与组织
- Bucket(桶):InfluxDB 的数据存储单位,结合了数据库和数据保留期的概念。每个桶都有一个组织和保留期设置。
- Organization(组织):组织是一个用户组的工作区,所有的仪表盘、任务、桶和用户都属于同一个组织。
Schema优化
location和scientist都是tag,_field和_value都是field。
- Fields不参与索引,必须扫描全表;如果经常被访问,不适合以这种形式存储,就应该以tag的形式 存储。
- Tags参与索引,当然查询起来就会更快。当然索引本身是有开销的,并且如果你进行写操作,索引 也会更新,带来更多开销。
influx的数据都是以append的方式往里加(类比lsm,但是实际上叫做时间序列合并树)。
Storage Engine 存储引擎
整体是TSM(Time Structured Merge Tree)结构,类似于LSM的结构。 落到比较下面的层就按照列压缩。为什么按照列存?我们关心的是给定时间段内列的value,按照列 存,这些讯息就可以连续存储了。
Shard
数据量大了,mongoDB中就是把数据压成很多shard,neo4j也是。时序数据库是最简单的线性表,因 为并不存在什么诡异的外键关联这种,直接切就完事了。
1 个 bucket 可以分成若干个 shard group,这个 shard group 里面又分成若干个shard。意义?首先是我们可以做分布式存储。还有,类比我们在关系型数据库里看到那个partition,就是我如果要查某一天的数据,你不用去做全表扫描,或者建一个全表的索引去做处理,我就定位到这个 shard group里去查找。如果跨越几天,我就查符合的两个三个shard group。
第十九章
云数据库的概念和特点
云数据库的一个最大的特点是什么?比如说在华为的机房里有很多很多的服务器拿出来去给你用了,然 后你也别管它有多少物理服务器,你就说我需要一台虚拟机,那理论上就是这个机房里所有的机器全部 可以给你拿来用(比如支持在线弹性伸缩),你只要有钱就可以——对你来说就是你不要管这个机房里 有多少机器,你眼睛里看到的是一台超级计算机。我把所有的硬盘合到一起,形成了一个网络文件系 统,我再把机器上运行的这些 CPU 通过网络合在一起,形成了一个超级CPU。
然后我所有的任务来了之后,就在这些 CPU 上去调度,利用底下所有的硬盘合起来构成的网络文件系 统去存储数据,这就是存储和计算的分离。
这就和大家看到的本地的计算机不一样,甚至跟你本地搞了 5 台计算机,大家建一个集群也不一样。集 群的目的是做主从备份——一主四从,做读写均负载均衡,但是你部署为一个云的话,在我眼里看到的 不是五台机器,是一台机器,所有的任务可以在这上面去进行调度,这跟你五台机器彼此之间隔离,然 后大家互相之间去做备份是不一样的。所以他这里就讲了我们是一个分布式的共享存储。
为什么是共享存储?就刚才讲它是一个超大的一个存储,一个网盘,然后在这个基础之上要做高可用的 容灾,就是我所有的服务器有可能会出错,一旦出错马上就有其他的顶上来,然后我任务和数据就做一 次迁移。
GaussDB
GaussDB是一个分布式的数据库。
- 华为的openGauss不是云数据库,搬上云GaussDB才是云数据库。
- openGauss是一个高性能、高安全、高可靠性的企业级开源关系型数据库
- openGauss是一个单进程多线程的架构,但是mySQL是一个多进程的架构,很可能一个数据库连 接就对应一个进程。使用单进程的话可以降低通信的开销。多进行需要使用到进程间通讯,而单进 程的话可以使用共享内存的方法。
- 单表大小32TB,单行数据量支持1GB
分布式的困难?一个关系型数据库,要想做分布式就很恶心了,就困难在你把数据怎么存储才能让这个 数据库它的性能会比较高,比如说要做一个查询,正好这个查询涉及到数据全部存在不同的节点上,那 你要把数据全部的汇聚到一个地方,这就很恶心。
GaussDB的分布式优化器
你写了一个 SQL 的查询,他就先做语法和解析,就看你要干啥,然后生成一棵查询计划树。它的执行 效率就可能不高,所以就根据我预先定义好的一些规则去优化。
优化的东西有包括什么呢?比如在分布式场景下,比如说我有两张表去连接,那你可以T1 连接T2, T2 可以连接T1。
所以经过这一系列的东西,最后你会发现你写的这个 SQL 语句,你可能压根都没想过数据是分布的, 然后还有什么将来能并行的。但是经过这一系列的处理,最后它产生的是在多个机器上,而且是并行去 执行的,这样的逻辑就跟你写的不一样了,中间经过了一系列的优化,就在防止你写的东西很烂。
GaussDB多租户
如果存的时候每一个数据库都是所有人各自独占,完全不一样,那么带来的一个问题是我这个很难去维 护。微信小程序它的一个基本的原理是:它要用到微信本身自己提供的那些服务去开发他自己的东西。 那么如果我要是看到所有的这些小程序,他们都有各自完全不同的数据结构去存储,然后还要我对他们 去进行支撑,我就一个数据库,我如何为所有的小程序服务?
换句话说,你有一个数据库服务器的实例,你的东西去给不同的租户,他们每一个人都在上面可以存他们的数据,但是他们彼此之间都认为自己是有一个独占的数据库。
设我现在有两个应用,一个应用说我这个数据库里面这个表,它就一张表,他说我的字段分别是a、b、 c、d、e。那有另外一个用户,他说我的字段是这样的:a、b、c、d、f。如何服务这两个用户?
- 第一种方法:single table,我只有一张表,没有个性化的东西,最后表有个表示oid(owner id), 用这个标识来区分数据是谁的,我们就可以直接隔离了。缺点:schema要求是一样的,不支持个 性化了,事务的管理也会比较复杂,并发量高,访问性能就会下降。
- 第二种方法:不是一张表。第一张表比如我存abcd+oid,第二张表存oid+主键(关联到第一张表) +扩展key+扩展value。这里其实就是把自己特有的列转为了行来存储。
- 第三种方法:算了,大家还是别存在一起了。第一个应用有自己的表,第二个应用有自己的表,但 是大家还是放在一个db里面。
- 第四种方法:干脆一个人一个db,彻底分开。(最不好,最浪费资源的方法)。
站在数据库的角度来看:
1. schema 如果有一些可以共享,那么我这个数据库就好管理,我既可以支持你有这个共同的东西, 也可以支持你个性化东西。 2. 硬件的资源要隔离开,某一个租户挂了不会影响其他人。
openGauss的查询优化
因为数据量巨大,所以优化是值得的,否则数据量小,比如别做优化,优化反而花时间。具体优化看上面的图。
- 基于规则的查询优化(RBO)
- 根据预定义的启发式规则对SQL语句进行优化
- 基于代价的查询优化(CBO)
- 利用统计信息,通过基础和代价估计对SQL语句对应的候选物理计划进行代价估算,并从候选计划中选择代价最低的物理计划作为最终的执行计划
- 基于人工智能的查询优化(ABO)
- 收集执行计划的特征统计信息,借助人工智能模型估计基数和代价,并选择高效的执行计划
- 智能基数估计和代价估计
- 智能计划选择和生成
行存储和列存储
- 行存储:适合业务数据的频繁更新,比如插入更新一条数据,类比双十一开卖的时候订单很多条目要插入进来
- 列存储:支持业务数据的追加和分析场景,类比双十一结束的的时候需要统计销售额【注意:在批量加载的时候为什么更适合列存储呢?因为列存储的时候压缩率高,比如某一列连续下来存的是5个连续的1,我就可以压缩一下,或者我时间戳的话发现可以记录一个增量,这样就大大的优化了存储的空间,使用列存储的话更加的高效】
- openGauss的引擎会根据执行的SQL语句判断是在行还是列存储上进行查找
- 同时做行存储和列存储的缺点是浪费了空间,但是好处是适合的SQL会发到对应的存储上,速度会明显提高。
- 内存表:数据整个在内存里面,大量的数据一次性加载到内存里面,以后的所有操作全部都在内存里面,可能只要我不关机,我可能数据一直都不用落硬盘,每天可能为了备份,把数据落一次硬盘。(那和我们之前说的mysql有什么区别呢?之前所说的buffer比较小,我们只能把正在操作或需要操作的数据加载到buffer里面,但是内存表要求很大的缓存,开机的一刻就把数据加载到内存中)
- 应用场景:比如存储一系列风力发电机的厂家、型号、风场信息用行存储,但是如果风力发电机不断的发时序数据的话,这些数据应该更适合列存储。同时需要行、列存储的场景就是这样
ORM效率
有人可能有疑问:ORM映射是通过把对象的操作通过JPA工具生成SQL语句,然后发送给数据库进行操 作的,那如果我自己直接写SQL语句的话不是避免了这个转换的过程,一定会有更高的效率吗?
回答:错误的。JPA工具里面有大量积累的优化的SQL的经验,会把对象的操作转换为基本上说是最优效率的SQL操作语句。但是如果你自己写SQL语句可能效率远远比不上。但是有人说数据库不是有查询优化吗?实际上数据库的查询优化很有限(课上的举例就是底板不行怎么打扮都不好看)所以说通过ORM映射产生的SQL基本就是最优解。此外还有一种情况,比如数据库里面有一些函数需要被调用,他 们产生的是一个结果返回我,数据的处理就是在数据库里面实现的,如果用ORM的话可能会写一个遍历,那这样的情况可能ORM就比较差。
GaussDB的分布式事务
如果你只涉及到一个数据节点,那这是一种情况,如果你涉及到两个数据节点,那就会有一个两阶段提 交协议。
优化是说在第二个阶段它是异步的;他发请求出去,然后他也没有去等着他们;回没有回来的这个线。 也就是说这个灰色的部分相当于在后台慢慢去处理,前端这端他马上就到 OK 了,就此处后面这端他会 想办法去让他一定能提交的,那么这是他做的一个优化。
为什么要有异步?举个例子: 比如说有一个数据采集器,它每隔一段时间就会去采集一下数据,然后把数据写到数据库里面去。 但可能数据发过来非常的快,第一个数据还没处理完,第二个数据就发过来了。同步的情况下,必 须是前端发过来一个数据,后端server去处理,处理完了和前端说哦我处理完了,前端再来下一 个。这就会出现问题。 什么意思?就是在现在这个场景之下,是我们不断的来事件,事件在没有被处理完之前会有新的事 件进来,我们要依次要去处理,它不能丢。但是我每一个处理之后不是同步在处理,不是说第一个 处理完了,第二个才能来,那这种东西叫啥呢?叫流式数据。
GaussDB版本更新
简单来说只有xmin和xmax之间的是可见可用的。
第二十章
Data Warehouse 数据仓库
- 多元的数据如何融合处理?数据仓库:把很多来源的数据加载之后,把他们安装同一个方式存储。
- 比如交大的数据、复旦、同济的数据,可能分别有10、11、12个字段的数据,需要把这些数据“抽取、转换、存储”,通常来说把不同的文件转存储到Parquet/RC File的文件格式
- RC File的文件:把原始的关系抽取某一列或者几列、甚至把整个表存储起来。同时文件最前面还有有一些元数据 表达的行号是哪几行、或者是否有压缩,压缩后key长度。然后这些文件被建成HDFS的块。
- Parquet:类似RC File。
- 存在的问题:需要ETL(抽取、转换、存储)然后写入到Parquet的中间文件格式,这个的代价是很大的。而这些文件在之后如果直接做SQL的时候效率很低。所以最终还是要转到SQL的数据库中(直接对数据库处理)那我们 就想:如果数据来了,我们可不可以先把数据按照原始的格式存储,当我真正需要的时候,再把数据导入进来。 所以这就是数据湖。
Data Lake 数据湖
- 以自然/原始格式存储数据的系统或存储库,通常是对象blob或文件
- 数据湖通常是一个单一的数据存储,包括源系统数据、传感器数据、社交数据等的原始副本,以及用于报告、可 视化、高级分析和机器学习等任务的转换数据。
- 数据湖可以包括来自关系数据库的结构化数据(行和列)、半结构化数据(CSV、日志、XML、JSON)、非结 构化数据(电子邮件、文档、pdf)和二进制数据(图像、音频、视频)。数据湖可以建立在“本地”(在组织的数 据中心内)或“云中”(使用亚马逊、微软、甲骨文云或谷歌等供应商的云服务)。
- 1. 在必要的时候会通过ETL的方法,把数据导入到数据仓库的里面
- 2. 数据湖要解决的问题就是:面临大量的数据要导入的时候怎么把原始数据快速接受下来
Data Lake vs Data Warehouse
Data Lake | Data Warehouse | |
Data Structure | Raw | Processed |
Purpose of Data | Not yet defined | Currently in use |
Users | Data Scientists: 针对原始数据进行处理 | Business Professionals: 主要针对商业金融的分析、统 |
Accessibility | Highly accessible and quick to update:因为不需要去做etl这样的动作 | More complicated and costly to make changes: 可以理解为主要是数据的一次导入, 以后只是做分析的 |
特点 | 数据仓库 | 数据湖 |
数据 | 关系数据 | 各种数据(因为它是直接存储的原始数据: 结构化半结构化等等都可以) |
schema 模式 | 比较严格的模式, 数据在读取的时候或者写入的时候做一个校验 (schema-on-write或者read) | schema-on-read(只有在数据分析的时候, 也就是从读走数据的时候校验数据的模式是否合法, 因为它是直接存储的原始数据,存储的时候不校验) |
性能 | 本地存储的时候非常快 | 用低成本的存储把大量的数据存储进来, 当搜索的时候可能比较慢 |
数据质量 | schema-on-write,过滤了不合规定的数据 | 因为所有数据直接接受,质量无保证 |
湖仓一体:不管是谁要来查数据,都到底层去抓数据,如果是关系型的就去关系型的找数据,非关系型的就到非关系型的去找就好了,不严格区分数据湖和数据仓库。
第二十一章
集群
- 有许多用户,可能在许多不同的地方(高性能)
- 系统是长时间运行的,不能终端服务(可靠性)
- 每秒处理大量事务
- 用户数量和系统负载可能会增加(比如open ai)
- 代表可观的商业价值(比如支付系统是一个很关键的系统,但是类比电子书店的浏览功能就是个很随 便的功能,因此为了保证支付的可靠性,很可能需要把常规的业务服务和支付服务分开,保证安全 和可靠性)
- 由多人操作和管理
Nginx
集群
- 集群(Clustering)是由一组松散耦合的服务器组成,向客户端提供统一的服务。
- 从客户端视角,集群表现为单一系统(单一系统视图或单一系统镜像)。
- 节点(Nodes): 集群中的计算机称为节点。
RAS 属性
- 可靠性(Reliability): 消除单点故障。
- 可用性(Availability): 确保总体可用性,计算公式为: 1−(1−𝑓%)^n。
- 可维护性(Serviceability):
- 复杂性比单一服务器更高,但支持热升级。
可扩展性(Scalability):
- 可在系统运行期间增加服务器,减少中断。
- 利用标准硬件构建集群比使用多处理器机器更便宜。
负载均衡与故障转移
负载均衡:
- 定义:将请求分配给集群中的不同节点,以优化整体性能。
- 方法:
- 系统化(顺序分配)或随机分配。
- 监控节点负载,选择负载较低的节点。
- 会话粘性(Session Stickiness):
- 问题:session会存在于某一台特定的服务器上,导致后续会话只能与特定服务器交互
- 解决:
- broadcast 将session广播给所有服务器。缺点浪费。
- 找专门的session服务器,处理session。好处是所有服务器都无状态了,坏处是这台服务器变成了单一状态节点,可能要对其备份。(这个bug在于单点故障,如果redis挂了麻烦就大了)
- 用ip-hash,保证对于同一个用户的访问,用同一台机器处理。
故障转移(Failover):
- 请求级故障转移: 某节点无法服务时,将请求重定向到另一节点。
- 会话级故障转移: 如果会话状态在客户端和服务器间共享,需在新节点上重建状态。
幂等性(Idempotence)
定义:幂等操作是指对同一请求重复执行,始终获得相同结果的操作。如:HTTP 的 GET 请求通常是幂等的。
失败可能点:
- 请求发出后,服务方法执行前失败。解决:换一台服务器。
- 服务方法执行中途失败。解决:重新换服务器,重新拿session执行。
- 服务方法执行完成后,响应传输失败。解决:保证系统严格幂等的情况下换服务器执行。
Nginx
一个高性能的开源Web服务器和反向代理服务器,同时还可以用作邮件代理和负载均衡器。WordPress使用此技术。
反向代理:(reverse proxy),指的是代理外网用户的请求到内部的指定的服务器,并将数据返回给用 户的一种方式;客户端不直接与后端服务器进行通信,而是与反向代理服务器进行通信,隐藏了后端服务器的 IP 地址。 反向代理的主要作用是提供负载均衡和高可用性。
- 负载均衡:Nginx可以将传入的请求分发给多个后端服务器,以平衡服务器的负载,提高系统性能 和可靠性。
- 缓存功能:Nginx可以缓存静态文件或动态页面,减轻服务器的负载,提高响应速度。
- 动静分离:将动态生成的内容(如 PHP、Python、Node.js 等)和静态资源(如 HTML、CSS、 JavaScript、图片、视频等)分别存放在不同的服务器或路径上。
- 多站点代理:Nginx可以代理多个域名或虚拟主机,将不同的请求转发到不同的后端服务器上,实 现多个站点的共享端口。
属性 | 代理(Proxy) | 反向代理(Reverse Proxy) |
---|---|---|
服务对象 | 客户端(用户) | 目标服务器(服务端) |
主要功能 | 帮助客户端访问目标服务器 | 帮助服务器处理来自客户端的请求,发送给处理请求服务器 |
隐藏信息 | 隐藏客户端的IP和身份信息 | 隐藏后端服务器的IP和结构信息 |
典型用途 | 上网代理、匿名浏览、内容过滤 | 负载均衡、安全防护、缓存加速 |
常见工具 | Squid、Shadowsocks、HTTP Proxy | Nginx、HAProxy、Apache Traffic Server |
负载均衡方式
- 轮询(Round-robin): 请求按顺序分配给服务器。(优点:负载 均衡的效果比较好,不同的机器轮流来处理,一般来说比方法三的效果好;缺点:session需要单 独维护,以及没有方法二的优点)
- 最少连接(Least-connected): 分配给当前活动连接数最少的服务器。
- IP 哈希(IP-hash): 使用哈希函数根据客户端 IP 地址选择服务器。优点:保证了会话的粘制性,只要用户的IP不变,集群中处理用户请求的机器 就不会变,缺点:这是三种方法里面负载均衡效果最差的一种,如果hash函数选择的不好的话,最终的结果很可能是导致集群里面的经常是一台机器在处理请求;当然还有一个缺点就是如果用户改变了IP,比如上网途中开启了或者关闭了VPN或者代理服务器,导致用户ip改变,这时候session就可能丢失)
Mysql集群
组成:一个类似GateWay的MySQL Router,负责转发请求。
数据库组成可能是一个主MySQL服务器,两个从MySQL服务器,主服务器负责读写操作,后面两个从服务器只能读,然后同步主服务器的内容。
MySQL Router,负责转发请求也会保证负载均衡,对于读的操作均衡分配(当然主服务器可能会少一点)对于写的操作分配给主服务器。
第二十二章
Cloud Computting
云计算是一种通过虚拟化技术,实现资源动态分配和管理的计算模型。用户按需获取资源,降低了硬件投资成本。把所有东西变成互联网上面能够访问的服务,暴露出来。
特点
- 弹性伸缩 (Elastic Scaling):
云计算服务可以根据需求无限扩展或缩减,满足客户弹性的需求。 - 按需付费 (Pay-as-you-go):
用户只需为实际使用的资源付费,无需预付大额费用。 - 快速部署 (Rapid Provisioning):
部署服务的时间从传统的数周或数月缩短至几秒或几分钟。 - 高级虚拟化技术 (Advanced Virtualization):
云计算通过虚拟化实现资源池化和高效分配。
核心技术
MapReduce
MapReduce 是一种典型的批处理框架,适合大规模数据的分布式计算任务,解决海量数据的并行处理和容错问题。
- 使用 Map 和 Reduce 的分阶段处理模型。
- 通过切分任务实现并行化计算。
- 通过任务重试和中间文件机制实现容错。
- 适合离线数据处理,但硬盘读写可能导致性能瓶颈。
两个阶段
Map阶段:将输入数据分片,映射为键值对(key-value),并生成中间结果。
Reduce阶段:对中间结果的键值对进行归并和聚合,生成最终的输出结果。
流程分解
- 用户程序(User Program)
- 用户编写 MapReduce 程序。
- 程序通过客户端提交到集群,由 Master 进行调度。
- Master 调度任务
- Master 负责:
- 将输入文件切分成多个逻辑分片(split)。
- 将 Map 和 Reduce 任务分配给空闲的 Worker 节点。
- 输入文件的分片大小通常等于分布式文件系统(如 HDFS)的 block 大小。
- Master 负责:
- Map 阶段
- 每个 Map Worker 处理一个或多个分片(split),从本地磁盘读取数据。
- Map Worker 执行用户定义的
map()
函数,将数据转化为键值对(key-value
)。- 例如,读取一次“am”,输出
{am: 1}
。
- 例如,读取一次“am”,输出
- 中间结果写入本地磁盘,生成中间文件(Intermediate Files)。
- Shuffle 阶段
- Reduce 阶段开始前,所有中间文件会通过网络传输(http)到 Reduce Worker。
- (sort)Master 按照键值对的键(
key
)对中间文件分区,并分配到不同的 Reduce Worker。- 例如,键值对
{A-N}
发送到 Reduce Worker 1,{O-Z}
发送到 Reduce Worker 2。
- 例如,键值对
- (Secondary Sort)如果需要对 key 的分组规则和排序规则进行区分,可以通过
Job.setSortComparatorClass(Class)
来指定自定义的排序规则,可以控制 key 如何分组,从而实现基于 value 的二次排序。
- Reduce 阶段
- Reduce Worker 读取分配的中间文件。
- 执行用户定义的
reduce()
函数,对键值对进行归并和聚合,生成最终结果。- 例如,将
{are: 1, are: 1}
归并为{are: 2}
。
- 例如,将
- 最终结果写入输出文件(Output Files)。注意:Reducer 的输出数据不是排序的。
- 输出结果
- Reduce Worker 的输出文件存储在分布式文件系统中,供用户使用。
后几个阶段都属于reduce,从Shuffle 算起。如果按照课件,reduce分为三个阶段suffle ,sort,reduce
容错机制
- 任务重试:如果某个 Worker 节点工作缓慢或宕机,Master 会将未完成的任务分配给其他空闲的 Worker。例如,当处理第 100 块数据的 Worker 出现问题,Master 会将任务分配给另一个空闲节点。
- 中间文件的冗余:中间文件存储在本地磁盘中,任务失败时可以被其他 Worker 重新读取和处理。
批处理特点
- 批处理:所有数据处理必须按照阶段(Map -> Shuffle -> Reduce)完成,数据以批次流动。
- Reduce 阶段开始前,所有 Map 阶段必须完成。
- 数据处理是离线的,适合需要全量数据分析的场景。
- 流式处理:与批处理不同,流式处理(如 Spark Streaming)可以边接收数据边处理。
性能优化
- 硬盘读写的瓶颈:MapReduce 中间结果存储在本地磁盘,对性能有一定影响。
- 内存优化:为提高性能,可以将中间结果存储在内存中(如 Spark 使用 RDD 数据结构实现),内存优化需要高效的数据结构支持。
Google分布式文件系统 (GFS):
利用现有的文件系统实现文件管理,在上面再加一层,实现分布式。
- 设计用于满足大规模数据处理需求。
- 使用日志记录提交的更新,并通过内存和持久化存储结合实现高效访问。
Bigtable:
- Google开发的分布式存储系统,用于管理结构化数据。
- 数据以行键、列键和时间戳的形式索引,适用于处理海量数据。
- 1. 首先和mongoDB一样,不存在什么乱七八糟的外键关联。
- 2. Column Family 列族:像二级分类一样,比如你有十个字段,我们分为3+3+4,3,3,4就分别成 为列族。后面结构也可以比较自由地变化,没有那么严格的schema。
- 3. TimeStamp:基于时间戳的数据存储。上图中展示出了一个立体的结构。
总结:数据量非常大、schema没那么严格、希望存储多个版本。
- Apache Hadoop:
- 一种开源框架,支持可靠、可扩展的分布式计算。
- 包括核心组件:
- HDFS:高吞吐量的分布式文件系统。
- MapReduce:大数据分布式处理框架。
Edge Computing
边缘计算是一种优化云计算的方式。
基本原理:
- 核心思想:将数据处理从云端“中心化”向“去中心化”转变,通过就近处理优化性能和效率。当无法处理时候向上卸载到计算能力更强节点。
- 网络边缘:是指靠近数据源(如传感器、物联网设备、智能终端等)的部分,而不是依赖于远程云数据中心。
特点
- 减少带宽需求:
数据无需全部上传到云端,节省网络传输成本。 - 本地处理能力:
在设备本地完成大部分计算任务,例如智能手机、传感器、平板电脑。 - 断网运行:
边缘设备可以在网络不稳定或断网时继续运行。
应用场景
- 视频分析:
- 传统云计算因延迟和隐私问题不适合处理视频数据。
- 边缘计算可以在本地设备分析视频,并仅上传结果到云端。
- 智能家居:
- 在家中本地处理数据,减少隐私泄露和带宽压力。
- 边缘网关(如EdgeOS)可以实现设备的高效管理和服务部署。
移动边缘计算 (MEC)
- 云卸载(Cloud Offloading )
- 边缘服务器有时无法处理过多的请求或计算,此时可以向上卸载到计算能力更强的节点处理。
- 计算卸载 (Computation Offloading):
- 将复杂计算任务从终端设备转移到边缘服务器,节省设备能耗,提高计算速度。
- 形式包括本地执行、完全卸载和部分卸载。
第二十三章
GraphQL
- API的查询语言:
- GraphQL是一种用于API的查询语言。
- 它允许客户端只请求所需的数据,如指定的Order_id而非全部Order,避免了传统REST API冗余数据的问题。
- 类型系统 (Type System):
- GraphQL基于一个用户定义的类型系统来运行,能兼容现有的数据和代码。
- 通过定义数据类型和字段,提供灵活且强大的数据查询方式。
功能
- 查询和变更 (Queries and Mutations):
- 查询 (Query):用于获取数据。
- 变更 (Mutation):用于更新或写入数据。
- 字段和参数 (Fields and Arguments):
- GraphQL支持为每个字段定义参数,实现精确查询。如查所有hero的name,指定数据转换为英尺。
- 别名 (Aliases):
- 允许给字段设置别名,解决数据冲突问题。如不同field里的id,起别名区分
- 片段 (Fragments):
- 支持在多个查询中复用字段集合,提高开发效率。
- 变量 (Variables):
- 动态值可以从查询中提取,通过字典方式传递。
GraphQL 和 RESTful API 对比
- GraphQL 优势:灵活的数据获取方式、避免多余或不足数据传输、支持实时数据推送、减少请求次数,适合复杂和现代化应用。
- RESTful API 优势:设计简单、生态成熟、适合资源结构清晰、接口稳定的场景。
对比维度 | GraphQL | RESTful API |
---|---|---|
端点数量 | 单一端点,所有查询通过一个 /graphql 端点完成。 | 多个端点,每个端点代表一个资源或一组相关资源。 |
查询语言 | 使用强大的查询语言,客户端可以精确请求所需的数据字段。 | 不支持查询语言,客户端只能获取服务器预定义的数据结构。 |
数据获取方式 | 客户端决定数据结构,可以按需获取需要的数据,避免过多或过少的数据传输。 | 服务器决定返回的数据结构,客户端需要根据服务器端的设计访问资源路径获取数据。 |
版本管理 | 不需要严格的版本管理。新增或修改字段不会影响现有客户端。 | 通常需要通过增加 API 版本(如 /v1/resource )来管理更新,确保兼容性。 |
实时数据支持 | 支持实时数据推送,通过订阅(Subscription)机制实现。 | 不直接支持实时数据,通常需要通过轮询或 WebSocket 等机制实现。 |
资源表示 | 允许客户端根据需求组合资源,客户端可以灵活决定如何获取和组合数据。 | 以资源为中心,每个资源有唯一的 URI,资源表示固定,缺乏灵活性。 |
过多数据传输 | 避免过多数据传输,客户端只会接收到明确请求的数据字段。 | 可能会返回客户端不需要的数据,造成数据浪费。 |
不足数据传输 | 避免不足数据传输,客户端可以在一次请求中获取所有需要的数据,避免多次请求。 | 如果一个资源不包含客户端需要的全部数据,可能需要多次请求不同的端点来获取完整数据。 |
复杂查询支持 | 支持复杂查询,可以在单个请求中获取多个相关资源的数据。 | 复杂查询需要多个端点请求,或者通过服务器端设计专门的查询接口。 |
实时性 | 原生支持订阅机制,适合需要实时更新数据的场景。 | 不支持原生实时更新,通常需要额外实现(如轮询或 WebSocket)。 |
学习曲线 | 需要学习 GraphQL 的查询语言和架构设计,初期学习曲线较陡。 | 使用 HTTP 协议的标准方法,学习曲线较平缓,开发者较为熟悉。 |
工具支持 | 有丰富的工具支持(如 Apollo、Relay 等),可以帮助快速开发和调试。 | 有广泛的工具和库支持(如 Postman、Swagger 等),生态成熟。 |
性能优化 | 可以减少客户端和服务器之间的请求次数,优化性能(特别是在需要复杂查询时)。 | 可能需要多次请求来完成复杂查询,增加网络开销。 |
错误处理 | 错误信息通常集中在响应的 errors 字段中,便于客户端处理。 | 错误信息通过 HTTP 状态码和响应体返回,需要客户端解析和处理。 |
生态成熟度 | 新兴技术,生态系统正在快速发展,适合现代化应用开发。 | 成熟的技术,广泛应用于各种项目,特别是传统系统和简单应用场景。 |
适用场景 | 适合需要灵活数据获取、实时性强、复杂查询的场景(如移动端、前端应用)。 | 适合资源简单、接口稳定、传统项目中,特别是 CRUD 操作较多的场景。 |
缓存支持 | 客户端需要自行实现缓存逻辑(如 Apollo Client 提供的缓存功能)。 | 基于 HTTP 的缓存机制(如 ETag、Last-Modified)支持原生缓存功能 |
第二十四章
Container
容器是一个沙盒化的进程,运行在主机系统中,与其他进程完全隔离。
容器利用了Linux内核中的以下功能:
- Namespaces(命名空间):
- 提供进程、网络、文件系统等隔离环境。
- cgroups(控制组):
- 控制资源分配,如CPU、内存等。
Docker
Docker 是一种容器化技术,通过操作系统级的虚拟化,将应用程序及其依赖打包到一个容器中。容器提供了一个独立的运行环境,可以在任何地方运行(开发、测试、生产环境一致)。与传统虚拟机相比,容器更加轻量级,因为它们直接与操作系统内核交互,无需虚拟化硬件。
Docker使这些技术变得更加易于使用。总结来说:
- 容器是一种可运行的镜像实例:可以通过Docker API或CLI创建、启动、停止、移动或删除。
- 跨平台:可以在本地、虚拟机或云端运行。
- 高度隔离:容器有自己的软件、二进制文件、配置等,彼此独立。容器彼此独立,互不干扰(通过 cgroup 和 namespace 实现资源隔离)。
- 可移植:可以运行在不同的操作系统上。
Image
镜像是构建容器的基础,每个镜像类似于操作系统快照,是一个只读模板,用来创建容器。容器启动时从镜像获取文件系统。
- 镜像提供了容器所需的隔离文件系统。
- 镜像包含:
- 应用运行所需的所有依赖、配置、脚本、二进制文件等。
- 容器的元数据(如环境变量、默认运行命令等)。
虚拟机需要一个完整的操作系统来运行,资源开销较大。而容器共享宿主机的操作系统内核,启动速度快、资源占用低。
特性 | 容器 | 虚拟机 |
---|---|---|
启动速度 | 极快(通常在几秒内) | 较慢(通常需要几分钟) |
资源占用 | 轻量(共享主机内核) | 重(每个VM有独立OS,资源消耗大) |
隔离性 | 中等(共享内核,但进程隔离) | 高(完全隔离,每个VM独立运行) |
移植性 | 高(能在不同环境中一致运行) | 中等(依赖于hypervisor及OS) |
管理工具 | Docker, Kubernetes等 | VMware, Hyper-V, KVM等 |
Docker构建
构建镜像:
- 创建一个名为
Dockerfile
的文件(无后缀)
FROM node:18-alpine #指定镜像的基础环境
WORKDIR /app #工作目录位置
COPY . . #将宿主机(即开发环境)当前目录的所有内容(.)复制到容器的工作目录(/app)。
RUN yarn install --production #在容器中运行 yarn install 命令
CMD ["node", "src/index.js"] #node启动
EXPOSE 3000 #3000端口暴露
- 执行命令构建镜像:
$ docker build -t getting-started .
启动容器:
- 使用以下命令启动容器:
$ docker run -dp 3000:3000 getting-started
- 参数说明:
-d
:后台运行容器。-p 3000:3000
:将主机的3000端口映射到容器的3000端口。
Mount
当需要对内容修改,我们可以将数据写到容器的可写入层,但当容器停止运行时,写入的数据会丢失。要么重新打包,过程十分繁琐。
这时候可以用数据挂载,指的是将主机文件系统中的某个部分(文件或目录)与容器内的某个路径关联起来,使容器可以访问主机的数据,或者主机可以方便的修改文件或数据,以下是两种挂载方式:
对比项 | Bind Mount | Volume |
---|---|---|
Source位置 | 用户指定 | 系统指定 |
Source为空 | 覆盖 dest 为空 | 保留 dest 内容 |
Source非空 | 覆盖 dest 内容 | 覆盖 dest 内容 |
Source种类 | 文件或目录 | 只能是目录 |
可移植性 | 一般(自行维护) | 强(Docker 托管)可以通过 docker run 或 docker-compose 文件轻松移植到不同的主机。 |
宿主直接访问 | 容易(仅需 chown ) | 受限(需登录 root 用户) |
通过 nicolaka/netshoot 和 dig 命令,我们可以快速检查 Docker 网络是否配置正确。如果发现问题,可以进一步使用其他工具(如 ping, tcpdump)进行深入排查。
docker compose
Docker Compose 是 Docker 官方提供的一个工具,主要用于管理和部署由多个容器组成的应用程序(pod)。
通过一个 YAML 格式的文件(通常命名为 docker-compose.yml
),可以定义所有需要的服务(容器)
第二十五章
Hadoop
Hadoop是一个由Apache基金会所开发的分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力,解决海量数据的存储及海量数据的分析计算问题。
广义上的Hadoop是指Hadoop的整个技术生态圈;狭义上的Hadoop指的是其核心三大组件,包括HDFS、YARN及MapReduce.
- Hadoop Common: 内核
- HDFS: 分布式文件系统
- YARN: 资源管理,MapReduce: 分布式计算;
- MapReduce并行处理的时候会出现一个问题,就是单一故障节点, 可能导致整个系统崩了,或者成为bottleneck,所以要有YARN。 针对大数据批处理请求,设计基于MapReduce/YARN的并行处理方案。
- Clinent node : 客户端,运行MapReduce程序的地方,由你编写的代码组成(MapReduce Program)。
- JobClient:是客户端上的一个组件,负责将用户的MapReduce作业提交到集群中运行。它是用户程序与Hadoop集群交互的接口。
- JobID:每个作业在Hadoop集群中都有一个唯一的标识符。JobClient会向JobTracker请求一个JobID,以标识这个作业。JobID类似于任务的“身份证”,用于在集群中跟踪任务的状态。
- JobTracker(负责任务的调度和跟踪,但它是一个单点组件,容易成为性能瓶颈(Yarn解决)):收到作业后,根据HDFS中输入目录找到需要处理的文件。HDFS中的文件通常会被切分成多个数据块(block),每个数据块可能被进一步划分成多个split(逻辑上的输入分片)。每个split对应一个Map任务。JobTracker会根据任务的split信息和集群中各节点的状态,决定将任务分配给哪些节点上的TaskTracker执行。同时检测集群故障和健康状态,如果任务失败要重新调度任务。
- TaskTracker(运行在集群上,负责接收任务分配请求,并报告状态):接收到任务后,会根据任务的需求(比如需要处理的split)从HDFS中读取相关数据。为了保证任务的隔离性和独立性,TaskTracker会在节点上创建一个新的JVM(Java虚拟机)子进程,在这个子进程中运行用户的MapReduce程序。
- 执行的过程中,TaskTracker不断发心跳给JobTracker,保证存活。
- MapReduce程序会在TaskTracker创建的JVM中运行。
为解决上面搜说的性能瓶颈,使用YARN通过将资源管理和任务跟踪分开,解决了JobTracker的性能瓶颈和单点故障问题,同时支持更多的计算框架,使Hadoop更加灵活和高效
Reducer数目
number.of.nodes ∗ number.of.max.containers.per.node ∗ 0.95or1.75,注意不是范围,只有两个值
- number.of.nodes:集群中机器的数量。
- number.of.max.containers.per.node:每台机器上可以运行的最大容器数。
- 0.95:保守估算,Reducer 数量略少于集群的最大并行能力。
- 特点:Reducer 的数量小于集群的最大并行能力,所有 Reducer 可以同时运行。
- 优点:
- 简单:所有 Reducer 一次性调度和运行,调度开销低。
- 没有任务等待:所有容器都可以同时工作,不需要切换任务。
- 缺点:
- 负载不均衡:如果某些 Reducer 处理的数据量特别大(数据倾斜),这些任务会拖慢整体进度,导致部分容器空闲而浪费资源。
- 适用场景:数据分布较均匀,Reducer 的负载差异不大。任务调度简单,减少调度开销。
- 1.75:激进估算,Reducer 数量超过集群的最大并行能力,任务分批执行。
- 特点:Reducer 的数量超过集群的最大并行能力,任务需要分批次运行。
- 优点:
- 负载均衡:任务被切得更细,所有 Reducer 的工作量更加均匀,减少数据倾斜带来的影响。
- 资源利用率高:即使某些 Reducer 处理速度慢,其他任务也可以快速完成并继续工作,避免资源闲置。
- 缺点:
- 调度开销高:需要频繁切换任务,存在额外的调度和数据传输开销。
有关Reduce内容往回找Mapreduce
第二十六章
Spark
Spark 简介
Spark 的优势:
- 相比 Hadoop MapReduce,Spark 性能提升可达上百倍。
- 以内存为核心进行计算,避免频繁的磁盘 I/O 操作。
- 支持 批处理 和 流处理,可以适应多种场景(如数据科学、机器学习等)。
- 可运行在 单机 或 集群 环境中。
批处理和流处理的区别:
- 批处理:数据以批次的形式一次性处理完。例如,先收集一批数据,处理后才能进入下一阶段。
- 流处理:数据源源不断地到来,来一点处理一点。例如,传感器持续发送数据,服务器需要根据数据实时处理。
流处理的特点:
- 数据是持续产生的(例如传感器传输的温度数据)。
- 流式处理并非 请求-响应模式(如 HTTP)。流数据可以通过消息中间件(如 Kafka)缓冲,避免处理速度跟不上数据产生速度。
Spark 的核心优势:内存计算
Hadoop 的缺点:
- Hadoop MapReduce 使用分布式文件系统(HDFS)进行中间结果的汇聚,频繁的磁盘 I/O 操作会导致性能瓶颈:
- 读取数据到内存(I/O 操作)。
- 中间结果写入磁盘(I/O 操作)。
- Reduce 阶段再次读取和写入(I/O 操作)。
Spark 的优化:
- Spark 读取数据后,利用 内存计算 来处理数据,减少磁盘 I/O 操作。
- 提供 缓存机制(cache):
- 当内存不足时,Spark 优先保留缓存的数据,尽量避免将数据写入磁盘。
- 注意:缓存是用户的“愿望”,具体是否缓存由 Spark 根据资源情况决定。
Spark 核心组件
术语 | 含义 |
---|---|
Application | Spark 应用程序,由一个 Driver 和多个 Executor 组成。 |
Driver | 应用程序的主节点,负责运行 main() 方法,创建 SparkContext ,划分任务并调度 Executor 执行。 |
Cluster Manager | 集群资源管理器,支持多种模式(如 Hadoop YARN、Mesos、Standalone)。 |
Worker Node | 执行计算任务的工作节点。 |
Executor | 工作节点上的进程,负责执行 Task,并将结果保存到内存或磁盘中,同时向 Driver 汇报状态。 |
Task | 发送到 Executor 的工作单元。 |
Job | 多个并行执行的 Task 组成一个 Job。 |
Spark 执行流程
SparkContext:
- Spark 应用程序的入口,允许用户通过 SparkContext 访问集群资源。
- 用户程序通过 SparkContext 连接到集群资源管理器,申请计算资源并启动 Executor。
Driver 的职责:
- 将计算任务划分为执行阶段(stage)和多个 Task。
- 将 Task 分发给 Executor 执行。
- 汇总 Executor 的执行状态。
Executor 的职责:
- 执行 Task。
- 将执行结果保存到内存或者磁盘。
- 定期向 Driver 和集群资源管理器汇报状态。
Spark RDD(Resilient Distributed Dataset)
是一个分布式、不可变的弹性数据集合,支持基于内存的并行计算,并通过容错机制自动重建丢失的分区数据。
RDD 的特点:
- Resilient(弹性):
- RDD 之间形成有向无环图(DAG)。
- 如果某个分区丢失,可以通过父 RDD 重新计算生成,即具备容错性。
- Distributed(分布式):
- RDD 的数据以逻辑分区的形式分布在集群的不同节点上,支持并行计算。
- Dataset(数据集):
- 存储的数据可以来自外部数据源,如 JSON 文件、CSV 文件、数据库等。
RDD 的操作类型:
- Transformation:
- 转换操作,返回一个新的 RDD。
- 惰性执行:只有遇到 Action 操作时,Transformation 才会真正执行。
- 示例:
map
、filter
。
- Action:
- 执行操作,返回结果或将结果保存到外部存储系统。
- 示例:
reduce
、collect
。
惰性执行的优势:
- Transformation 操作不会立即执行,而是构建一个有向无环图(DAG)。
- 只有当 Action 操作触发时,才会根据 DAG 图向前推导并执行 Transformation。
- 好处:节省内存空间,避免不必要的计算。
Spark 分区(Partition)
分区的意义:
- RDD 会被分割为若干个分区,分布在集群的不同节点上。
- 分区使得数据可以在不同节点上并行计算,提升计算效率。
分区的好处:
- 假设有一个用户信息表(userData)和一个点击事件表(events),需要对两者进行 Join 操作:
- 如果 userData 和 events 被分区存储在不同节点上,Join 操作只需在对应节点内完成,减少了网络传输开销。
- 如果没有分区,Join 操作会导致大量的网络通信和数据传输,严重影响性能。
Spark 的依赖(Dependency)
依赖的类型:
- 窄依赖(Narrow Dependency):
- 每个父 RDD 的分区最多被一个子 RDD 的分区使用。
- 无需 Shuffle,可以并行计算。
- 宽依赖(Wide Dependency):
- 每个父 RDD 的分区可能被多个子 RDD 的分区使用。
- 需要 Shuffle,计算复杂度较高。
窄依赖的优势:
- 只需重算丢失数据的父分区,支持并行计算,节点恢复效率高。
- 宽依赖需要重新计算多个父分区,可能导致不必要的冗余计算。
Spark 的 Stage 和 DAG
Stage 的划分
- 在Spark 中,任务会被划分为多个 阶段(Stage),每个阶段内部尽可能多地包含可以流水线化(Pipeline)的操作,以提升效率。所以每个 Stage 内部包含尽可能多的窄依赖操作。
- Stage 的边界:
- 宽依赖上的 Shuffle 操作。
- 已缓存的分区。
DAG(有向无环图)
- Spark 基于 DAG 构建计算任务,所有的 Transformation 操作会先形成 DAG。
- 当 Action 操作触发时,DAG 被划分为多个 Stage 并执行。
Stage 的执行
- 在一个 Stage 内,所有窄依赖的 Transformation 操作会被流水线化(Pipeline)执行。
- 好处:减少内存占用和中间结果存储,提高执行效率。
Stage 1:原始数据 A 通过 groupBy 转换为 B,并缓存了 B。因为已经缓存,可以看到没有执行。
Stage 2:从缓存的 B 开始,执行 map 和 union,生成中间结果 F。
Stage 3:执行 join 操作(宽依赖),将 F 和缓存的 B 结合,生成最终结果 G。
第二十七章
Storm
Apache Storm 是一种分布式实时流处理框架,由推特发明,用于处理源源不断的流式数据。它擅长对海量数据进行实时计算,适合处理需要快速响应的场景,例如推特实时处理用户上传的图片、日志监控、实时统计等。比如说用户上传一堆很大照片,你就可以先传回个糊的,再慢慢变清晰(JMoments也用了)
赛博斗蛐蛐,Hadoop 和 Storm 都是用于大数据处理的框架,但它们的侧重点和应用场景不同,都是分布式和荣错的。
属性 | Storm | Hadoop |
---|---|---|
处理方式 | 实时流处理 | 批处理 |
状态管理 | 无状态(处理任务彼此独立) | 有状态(任务结果依赖于持久化的中间状态) |
架构 | 基于 ZooKeeper 的主从架构:主节点:Nimbus从节点:Supervisors | 主从架构:主节点:Job Tracker从节点:Task Tracker |
数据处理能力 | 每秒处理数万条消息(低延迟) | 处理海量数据,但需要耗费数分钟或数小时(高吞吐) |
运行模式 | Topology 持续运行,直到人为关闭或发生不可恢复的故障 | MapReduce 作业按顺序执行,完成后退出 |
容错性 | 主节点(Nimbus)或从节点(Supervisor)宕机后可恢复 | 主节点(JobTracker)宕机后,所有运行中的作业丢失 |
存储 | 不负责数据持久化 | 使用 HDFS(分布式文件系统)进行数据存储 |
- Storm 是流处理,实时性强,任务无状态。
- 适合实时计算(如实时日志分析、实时推荐、在线监控等),强调低延迟。
- Hadoop 是批处理,吞吐量大,任务有状态。
- 适合离线批处理(如大规模数据统计、机器学习训练等),强调高吞吐。
- 结合使用:可以用 Hadoop 进行离线数据存储和批处理,用 Storm 进行实时数据流处理,满足不同业务需求。
核心概念
- Tuple:Storm 的基本数据结构,是一个有序的元素列表,类似于一行数据。
- Stream:一个无序、无限的 Tuple 序列,是 Storm 中数据流动的核心。
- Spout:数据的来源,用于从外部数据源(如 Kafka、Twitter API)读取数据并生成 Stream。
- Bolt:数据处理的逻辑单元,接收 Spout 或其他 Bolt 的数据,执行计算、过滤、聚合等操作并输出结果。
- Topology:一个有向图,由 Spout 和 Bolt 组成。
- Spout 是起点,负责读取数据。
- Bolt 是节点,负责处理数据。
- Topology 持续运行,直到被手动终止。
- 任务(Task):Spout 和 Bolt 的具体执行实例,一个任务对应一个线程。多个实例可以并行运行,提升性能。
Storm 的工作机制
- 数据流动:数据从 Spout 开始流入,经过多个 Bolt 的处理后生成最终结果。
- 实时性:微观上是批处理(处理一条条 Tuple),宏观上是流处理(连续处理源源不断的数据流)。
- 容错性:如果某个节点(如 Nimbus 或 Supervisor)宕机,Storm 会自动恢复并从中断的位置继续运行。
Zookeeper
Zookeeper 是 Storm 集群的协调服务,主要功能包括:
- 管理集群中的节点(如 Nimbus 和 Supervisor)。
- 实现一致性:确保集群中所有节点对任务的状态达成一致。
- 故障恢复:当主节点(Nimbus)宕机时,Zookeeper 会选举新的主节点。
第二十八章
HDFS
HDFS 使用场景
适合的场景:
- 大文件存储:
- 适合存储非常大的文件(如 GB、TB 级别)。
- 小于 100MB 的小文件存储效率较低。
- 流式数据访问:
- 数据通常是“一次写入,多次读取”。
- 不适合频繁修改的数据。例如:时序数据(如 InfluxDB 数据)非常适合。
- 廉价硬件(Commodity Hardware):
- 可以用大量低成本的硬件组成集群,不需要高性能硬件。
- 使用“副本机制”(默认 3 个副本)保证可靠性。
不适合的场景:
- 低延迟的数据访问:
- HDFS 主要追求高吞吐量,而非低延迟。
- 例如:一个巨大的文件分为 1000 个 block,同时读取效率高;但小文件单独读取效率低。
- 大量小文件:
- 每个文件需要记录元数据(metadata),小文件过多会导致元数据膨胀,难以维护。
- 频繁修改、多写者场景:
- HDFS 只支持“单写者、追加写”。
- 不支持任意位置的修改,多个写操作需要串行化,影响效率。
HDFS 架构
HDFS 采用 Master-Slave(主从)架构,分为 NameNode 和 DataNode。
- NameNode(主节点):负责存储 元数据(metadata),如文件名、文件分块信息、副本位置等。不存放实际数据。
- DataNode(从节点):存储实际数据块(block)。周期性向 NameNode 发送 心跳(heartbeat) 和 BlockReport,报告自己存活状态及数据块信息。
读数据流程:
- 客户端向 NameNode 请求文件的元数据(如文件分块信息,每个块的位置)。
- NameNode 返回数据块的位置信息。
- 客户端直接从 DataNode 读取数据,NameNode 不参与数据传输。
写数据流程:
- 客户端向 NameNode 请求写权限。
- NameNode 分配数据块位置。
- 客户端直接将数据写入 DataNode,并根据副本策略同步到其他 DataNode。
注意:NameNode 是 HDFS 的单点故障点,若 NameNode 挂掉,整个系统不可用。因此需要对 NameNode 的元数据进行备份。需要使用log。
HDFS 的关键设计
1. 副本机制(Replica):
- 每个数据块有多个副本(默认 3 个),以提高可靠性和容错性。
- 过多意味着执行写操作时间会长,失败的概率变大,且阻塞。
- 副本存放策略(以 3 副本为例):
- 第一个副本:优先存放在指定的 DataNode 本地,否则随机在集群中选择 一个DataNode .
- 第二个副本:存放在不同机架的 DataNode(跨机架存储)。
- 第三个副本:存放在第二个副本所在机架的其他节点。
2. 机架感知(Rack Awareness):
- HDFS 通过识别网络拓扑结构,将节点划分为不同的机架。
- 优化副本存放策略,提升数据可靠性和网络带宽利用率。
- 数据传输的优先级:
- 同节点。
- 同机架内的不同节点。
- 不同机架的节点。
3. 心跳机制(Heartbeat):
- DataNode 定期向 NameNode 发送心跳,报告存活状态。
- 心跳中包含状态数据(如数据块信息,BlockReport),以填满 TCP 数据包,提高网络利用率。心跳不能是专门的一个线程,真正的心跳应该是在你的主线程里面。
4. 安全模式(Safemode):
- NameNode 启动时进入“安全模式”,不允许写操作。
- 检查所有数据块是否满足副本数量要求,若不足则触发副本复制。
- 确保系统一致性后退出安全模式。
HDFS 的优势和局限
优势:
- 高吞吐量:适合批量处理大数据。
- 高可靠性:通过副本机制实现容错。
- 可扩展性:支持集群动态扩展。
局限:
- 不适合小文件:大量小文件会导致元数据存储和管理的压力。
- 不支持频繁修改:只能追加写,且只能单写者。
- 单点故障:NameNode 是单点故障节点,需做好备份。
HDFS 的实际应用场景
- 日志存储:大量日志数据(如 Web 访问日志、服务器日志)可以存储在 HDFS 中,用于后续分析。
- 数据仓库:HDFS 是大数据生态系统的核心存储组件,常用于存储离线分析数据。
- 备份数据存储:由于 HDFS 的高可靠性,可以用于存储重要数据的备份。
- 不可变数据:订单、交易记录等生成后不再修改的数据非常适合存储在 HDFS 中。
注意:像库存(Stock)等频繁变动的数据不适合直接存储在 HDFS 中,建议将变动部分存储在内存或数据库中。
第二十九章
HBase
HBase 是一个高效的分布式数据库,适合存储和处理海量的半结构化和非结构化数据,非常适合需要快速读写、动态扩展的应用场景,如日志分析、时间序列数据存储、物联网数据等,但不适合复杂的多表关联查询场景。
如果我们把 Hadoop 比喻为一个云上的操作系统:
- HDFS: 是文件系统,负责存储数据。
- MapReduce: 是作业调度系统,负责计算任务的分发和执行。
- HBase: 是数据库,类似 Google 的 BigTable,是一个专门处理大规模数据的分布式数据库。
HBase 的特点是:
- 处理半结构化和非结构化数据: 传统关系型数据库(RDBMS)主要处理结构化数据,而 HBase 更适合处理大规模的半结构化和非结构化数据。
- 分布式存储与线性扩展: 数据可以水平切分到多个节点上,系统的性能和容量可以通过增加节点线性扩展。
- 近实时读写: 尽管是大数据量,HBase 依然可以支持近乎实时的随机读写。
数据模型
HBase 的数据模型与传统关系型数据库有很大不同
2.1 表结构与存储方式
- 按列存储:
- HBase 是基于列存储的数据库,不同列的数据是分开存储的。
- 列存储的好处是访问效率高,特别是当只需要读取少量列时,不需要扫描整行数据。
- 分层存储:
- HBase 表会被水平切分成多个 Region,每个 Region 是表的一部分(类似于表的分片)。
- 每个 Region 会进一步切分成 Block,并存储在 HDFS 上。
- 这种分层存储方式加速了数据查询:
- 通过 Key 的范围,快速定位数据在哪个 Region。
- 然后在 Region 中找到具体的 Block,最后读取对应的数据。
- 主键:
- 每行数据都有一个唯一的 Row Key(主键)。
- 所有行根据 Row Key 的字节顺序排序存储。
- 查询时必须通过主键访问,主键的设计要有意义(如 URL 倒序存储)。
- 带版本的多维存储:
- HBase 支持对同一个字段存储多个版本的数据,每个版本以时间戳区分。
- 这使得 HBase 可以查看某个时间点的数据快照,或者在不同版本之间切换。
2.2 列族(Column Family)
- 概念:
- HBase 的列是通过 列族(Column Family) 组织的。
- 每个列族包含若干列(Column Keys)。列族是 HBase 的基本存储单位。
- 特点:
- 列族中的列可以动态增加,且不需要为已有数据填充默认值。
- 关系型数据库中,增加列需要修改整个表的结构,可能会导致已有数据不一致;而在 HBase 中,新增列不影响已有数据。
- 列键格式:
- 列键的格式为
family:qualifier
,其中family
是列族名,qualifier
是列名。 - 例如:
- 列族
language
中只有一个列键,用于记录网页的语言。 - 列族
anchor
中,每个列键都是一个锚点(anchor 名称),对应的值是链接的文本。
- 列族
- 列键的格式为
- 连续存储:
- 同一列族中的列会连续存储,这样可以提高访问效率。
- 例如,查询某个列族时,不需要扫描整个表,只需扫描该列族的数据。
2.3 时间戳与多版本数据
- 多版本数据:
- HBase 的每个单元格(Cell)可以存储多个版本的数据。
- 版本通过时间戳标记,可以保存最新的 N 个版本(N 是用户配置的值)。
- 时间戳机制:
- 每次写入数据时,HBase 会自动为数据添加时间戳。
- 查询时可以指定时间戳,获取某个时间点的数据,或者查看某个字段的历史值。
- 灵活性:
- 不同单元格可以有不同数量的版本。例如,一个单元格可以存 5 个版本,而另一个单元格只有 2 个版本。
- 这种机制增强了数据的表达能力,是传统关系型数据库无法实现的。
2.4 Web Table 示例
URL 倒序存储:
- 在 Web Table 中,URL 通常存储为倒序形式。例如:
原始 URL: www.example.com/page1 倒序 URL: com.example.www/page1
- 这样可以将同一域名的网页存储在一起,提高查询效率。
列族设计:
- 假设 Web Table 有两个列族:
- 语言(language): 存储网页的语言 ID。
- 锚点(anchor): 每个列键表示一个锚点,值是链接的文本。
- 这种设计可以灵活扩展,例如新增列键时,不需要修改表结构。
3. HBase 的操作与特点
3.1 操作方式
- 主键访问:
- 所有操作必须通过主键(Row Key)进行,主键的设计非常重要。
- 主键的排序方式(按字节码排序)会影响数据的存储和查询效率。
- 写操作:
- HBase 的写操作是追加式的,不会覆盖原有数据。
- 写入数据时会自动生成时间戳,从而支持多版本存储。
- 读操作:
- 查询时可以指定主键范围,或者通过时间戳获取某个版本的数据。
3.2 HBase 的优势
- 动态列:
- 列族中的列可以动态增加,不需要修改表结构,且不会影响已有数据。
- 这对数据模型变化频繁的场景非常友好。
- 高效的随机读写:
- HBase 支持近实时的随机读写操作,适合需要频繁更新和查询的场景。
- 线性扩展:
- 数据可以水平切分到多个节点,系统性能可以通过增加节点线性扩展。
- 适合半结构化和非结构化数据:
- HBase 可以存储灵活的数据模型,支持复杂的查询和分析。
3.3 HBase 的局限性
- 不支持复杂查询:
- HBase 不支持 SQL 查询,无法直接执行复杂的 Join 和聚合操作。
- 适合单表操作,不适合多表关联查询。
- 依赖主键:
- 查询必须依赖主键,主键设计不合理可能导致查询效率低下。
- 数据模型复杂:
- 列族和多版本机制虽然灵活,但也增加了数据模型的复杂性。
第三十章
Hive
Hive 是一个基于 Hadoop 的数据仓库工具,它可以将结构化的数据文件映射为数据库表,并提供类似 SQL 的查询功能。Hive 的核心作用是将 SQL 查询翻译成底层的 MapReduce 或 Spark 任务进行计算,而数据存储则由 HDFS(Hadoop 分布式文件系统)负责。主要用于大规模数据的离线分析,通过类 SQL 的接口降低了 MapReduce 的学习成本,并支持多种文件格式和扩展功能,Hive 的设计理念是 Schema on Read,在查询时才进行数据解析和转换,适合处理海量数据的分析场景。
\Hive 是一个将 SQL 转换为 MapReduce/Spark 任务的工具,甚至可以看作是 MapReduce/Spark 的 SQL 客户端。
注意: Hive 并不适合用作事务型数据库(如电子商务系统的订单管理),它的核心目标是高效处理大规模数据的批量分析任务,适合数据仓库和数据湖的场景。
Hive 的特点
- 可扩展性Hive 支持动态扩展集群规模,通常不需要重启服务。
- 延展性用户可以根据需要定义自己的函数(UDF、UDAF、UDTF)来扩展 Hive 的功能。
- 容错性Hive 具有良好的容错性,即使某些节点出现问题,SQL 查询任务仍然能够完成。
Hive 中的基本概念
1. ETL
ETL 是数据仓库和大数据处理中常见的流程,代表数据的抽取(Extract)、转换(Transform)和加载(Load)。在 Hive 中,ETL 过程通常包括以下步骤:
- 数据抽取(Extract)
从不同的数据源(如关系型数据库、日志文件等)中获取数据,可以使用工具如 Sqoop 或直接读取文件。 - 数据转换(Transform)
使用 HiveQL 对原始数据进行清洗、筛选、聚合等操作,使其符合目标数据仓库的格式。 - 数据加载(Load)
将转换后的数据加载到 Hive 表中,可通过LOAD DATA
命令或分区表机制完成。
2. Metadata Store(元数据存储)
Hive 的表由两部分构成:数据 和 元数据。
元数据描述了表中数据的结构和布局,例如:
- 文件包含的列数和列的数据类型
- 数据文件的存储路径
- 文件的格式(如 CSV、Parquet 等)
元数据通常存储在 Hive 的元数据存储组件中(如 MySQL 或 Derby 数据库)。
3. 数据加载
通过命令 LOAD DATA
可以快速将文件加载到 Hive 表中。例如:
LOAD DATA LOCAL INPATH '/home/hadoop/data/emp.txt'
OVERWRITE INTO TABLE emp;
注意:
- 该命令不会直接执行数据的解析、转换或加载(即不进行 ETL 操作)。
- 它只是告诉 Hive 表的数据来源于指定文件,真正的解析和转换会在执行 HQL 查询时进行。
这种策略的优点是加载速度快,但在查询时可能会稍慢。
Hive 支持的文件格式
Hive 支持多种文件格式和压缩方式:
- TextFile:默认的文本文件格式。
- SequenceFile:Hadoop 的二进制文件格式,支持分块压缩。
- RCFile(Row Columnar File):行列混合存储格式,适合于某些查询场景。
- ORCFile(Optimized Row Columnar File):优化行列存储格式,支持高效压缩和查询。
- Avro File:一种支持模式演化的数据序列化格式。
- Parquet:一种列式存储格式,现已成为 Apache 的独立项目,广泛用于跨系统数据交换和高效查询。
RCFile 是一种早期的行列混合存储格式,虽然在一定程度上结合了行存储和列存储的优点,但它的压缩方式仍然以行存储为主,导致压缩效率有限。比如上图ABCD如果代表不同的数据类型,则可能导致这种算法并不高效。
Hive 与传统数据库的对比
1. Schema on Read vs Schema on Write
- Hive(Schema on Read)
Hive 在加载数据时不会直接对数据进行解析或校验,而是在执行 HQL 查询时才进行数据的抽取、转换和加载。这种方式适合处理海量数据,因为它避免了初始加载的高延迟。 - 传统数据库(Schema on Write)
传统关系型数据库(如 MySQL)在插入或加载数据时就会进行数据校验和转换,确保数据的一致性和完整性。这种方式更适合事务处理场景。
2. 面向事务 vs 面向分析
- 传统数据库(如 MySQL):适合事务处理,要求数据一致性、实时性较高。
- Hive:适合离线分析,通常一次写入多次读取,强调大规模数据的批量处理。
3. 数据仓库 vs 数据湖
- 数据仓库:数据经过清洗和建模,适合高级分析操作。
- 数据湖:存储原始数据,支持多种格式和工具访问。
Hive 在某种程度上具备了数据湖的能力,因为它允许直接存储原始数据文件,并在查询时统一解析。
数据湖与数据仓库的关系
- 数据湖作为数据仓库的数据源
原始数据存储在数据湖中,数据仓库从数据湖中提取数据用于高级分析。 - 数据湖即数据仓库
数据湖可以直接通过 SQL 查询访问,成为一个统一的存储和分析平台。 - 湖仓一体(Lakehouse)
热数据存储在数据仓库中以提高查询效率,冷数据存储在数据湖中以节省成本。这种模式被称为湖仓一体,Hive 可以看作是早期实现湖仓一体的工具。
第三十一章
FLINK
Flink 是一个框架和分布式处理引擎,专门用于处理 有状态(stateful)计算 的 无界(unbounded) 和 有界(bounded) 数据流。
- 流式数据(Streams):
- 无界流(Unbounded Streams): 数据源源不断地产生,数据流没有固定的结束点。比如实时日志、传感器数据等。
- 有界流(Bounded Streams): 数据流是有限的,类似批处理。比如一个固定大小的文件。
- 实时流(Real-time Streams): 数据随着事件的发生实时到达。
- 录制流(Recorded Streams): 数据不是实时到达,而是从 Kafka 等消息队列中读取历史数据(Flink 处理速度可能跟不上实时数据时的解决方案)。
Flink 要解决的核心问题
- 数据分区(Partitioning):
- 有多个处理器(算子)时,如何将数据分区到不同节点上?(比如按 key 或规则将某些事件分配到特定的节点)
- 乱序问题(Out-of-order Events):
- 数据可能不是按事件发生时间的顺序进入系统,比如九点钟处理八点到九点的数据,但九点十五可能还会收到八点到九点的数据。如何保证正确性?
- 有状态的处理(Stateful Computation):
- 事件之间需要关联处理。例如:
- 用户点击行为跟踪:需要知道用户上一次点击的位置。
- 购物车:需要保存每个用户的购物车状态。
- 事件之间需要关联处理。例如:
- 这种状态需要被保存和管理,特别是当内存不足时,状态需要写入硬盘(如 RocksDB),并在需要时恢复。
核心概念
- Flink 的核心概念:
- 分区: 数据按 key 分区到不同节点,保证状态一致性。
- 状态: 保存事件的中间结果,支持存储与恢复。
- Watermarks: 解决乱序问题,定义延迟时间。
- 窗口机制: 将流式数据分批处理。
- Flink 的优势:
- 支持有状态的流式计算,能够关联前后事件。
- 高效的状态管理(内存+硬盘+快照)。
- 灵活处理乱序数据,保证结果的准确性。
什么是状态(State)?
- 状态的定义:
- 状态是程序在处理事件时需要记住的信息。例如:
- 用户的购物车(键值对存储)。
- 用户点击的访问路径。
- Flink 中的状态:
- Flink 的状态是每个事件处理逻辑的中间结果。例如:
- 当前事件处理到哪,前一个事件是什么。
- 这些状态需要在事件处理的前后进行关联。
- 状态管理的特点:
- 存储与恢复: 当内存不足时,状态会写到硬盘(如 RocksDB),并在需要时从硬盘恢复。
- Checkpoint(检查点): 为了容错,Flink 定期对状态进行快照(snapshot),确保即使系统崩溃,也可以从最近的检查点恢复。
- 异步快照: 前端继续处理事件,后端异步写入快照,避免前端停顿。
- 增量快照: 每次只记录变化的状态,避免全量快照带来的性能开销。
- 分区和状态维护:
- Flink 会根据 key 将相同 key 的事件发送到同一个节点处理,确保状态得以维护(类似 Nginx 的 IP 哈希逻辑)。
Watermarks(水位线)
- Watermarks 的作用:
- 解决少量数据延迟到达的问题。
- 例如:我们希望处理 8 点到 9 点的数据,但 9 点 2 分还有 8 点的数据到达,如何处理?
- Watermarks 的逻辑:
- 定义一个可以接受的延迟时间(如 5 分钟)。
- 如果延迟时间内(如 9 点 5 分前)数据到达,仍然可以处理为当前窗口的数据。
- 如果超过延迟时间(如 9 点 6 分到达),可以选择:
- 将数据放到下一个窗口处理。
- 丢弃这条数据。
- Watermarks 的实现:
- 每个数据源会生成一个水位线,表示它已经处理到的时间点。
- 下游算子会根据多个数据源的最小水位线,决定当前时间窗口的处理逻辑。
窗口机制(Windowing)
- 窗口的作用:
- 将流式数据分成小批次,分批处理。每个批次的数据叫做一个窗口。
- 两种窗口类型:
- 时间窗口(Time-based Window):
- 按固定时间间隔处理数据。例如每 30 秒处理一次。
- 计数窗口(Count-based Window):
- 按固定事件数量处理数据。例如每 3 个事件处理一次。
- 总结:
- 窗口机制将流式数据划分为小的批次处理,从宏观上看,流式数据就像是实时批处理。
2. Flink 的架构
- 有状态计算:
- 每次处理事件时,逻辑程序需要读取状态,决定如何处理当前事件。
- 状态存储在内存或硬盘上,频繁读写。
- 状态管理的关键点:
- 状态存储与恢复:
- 状态可能很大,内存不足时需要存储到硬盘(如 RocksDB)。
- 系统崩溃后,从快照中恢复状态。
- 异步与增量快照:
- 异步快照:前端继续处理事件,后端异步写入状态。
- 增量快照:只记录状态的变化,减少性能开销。
AI
人工神经网络
激活函数:sigmod
σ(z)= 1/(1+e^−z)
为什么sigmoid?任意一个函数都可以被拆成是若干个“阶跃函数”(分段函数)的线性组合,类似傅里叶 变换。但是分段函数表示起来比较麻烦。sigmoid的性质:
1. 取值范围在0~1之间,非常适合概率
2. 中心对称,在x=0处梯度较大
3. 平滑且求导方便,σ′(z) = σ(z)(1−σ(z))
4.非线性,可以帮助神经网络捕捉数据中的复杂模式
单个神经元只能产生线性决策边界,适用于二分类问题。实际碰到的问题复杂得多。
全连接神经网络
神经网络的输出使用的是“独热编码(One Hot Encoding)”,输出是一个向量,长度等于分类的数量。每 个分类都由向量中特定的位置来表示 (一个位置是1,其他位置为0,好处:相互正交,彼此独立,互不干扰)
概念:输入层,隐藏层,输出层。净输入(输入的加权和),激活 (到下一层神经元的输出)
- 输入层:这是网络的第一层,接受输入数据。图像分类任务中的每个像素值会作为输入层的一个节点。
- 隐藏层:这层位于输入层和输出层之间,负责处理数据并提取特征。神经网络的能力主要依赖于隐藏层的层数和节点数。
- 输出层:这是网络的最后一层,输出网络的最终结果。对于分类任务,输出层通常会给出每个类别的预测值。
- 权重(矩阵)节点之间的连接线条代表权重,用来表示输入和输出之间的关系。权重矩阵 𝑊(1),𝑊(2),𝑊(3)是神经网络的核心参数,训练的目标就是找到合适的权重来最小化误差。
对于多分类问题,最后一层是一个长度等于类别数量的向量
Sigmoid对多类别的扩展就是 softmax 函数:
它会产生一个向量,其中的各个项都介于 0 到 1 之间,并且它们的总和等于 1
softmax好处(之一):最后的结果会接近于独热编码
对于这个,参数量 100480 = (784 +1)*128,多出来的一个是偏置项
Dense:全连接层,两层间任意两个神经元间有连接
model = Sequential([ Input(shape=(28, 28, ), batch_size=32), Flatten(), Dense(128, activation=’relu’), Dropout(0.2), Dense(10, activation=’softmax’) ])
训练神经网络
设置批量batch的原因:并行计算一个和多个速度一样;原理是执行完每个batch进行一些修正(拿多个的损失值的平均),执行完多个再修正效果更好
如何训练神经网络:梯度下降!
1. 做出预测 2. 计算损失 3. 计算损失函数关于权重参数的梯度 4. 按照相反的方向更新一步权重参数 5. 迭代上述过程
σ = σ(z)(1−σ(z)) ≤ 0.25 当层数增多时,在靠前的层中,梯度就变得非常小了。这就是“梯度消失”问 题。正因为这个原因,其他的激活函数(例如ReLU) 变得更常见了
卷积神经网络(CNN)
目标:通过卷积提取局部的特征,例如提取图像边缘
全连接网络缺点:
1. 输入数据的各个分量是可互换的。也就是说,输入分量彼此之间没有任何特定的关系。但是对于图 像数据,模型应该充分利用其内在的属性。
2. 全连接网络会产生数量庞大的参数:彩色图像通常会有至少(200 × 200)像素 × 3颜色通道(RGB) = 120,000个参数。单个全连接层就需要 (200x200x3)^2 = 14,400,000,000 个权重!
动机:需要构建的特征:边缘-> 形状 -> 形状之间的关系;纹理
卷积核
- 卷积核:卷积核是一个权重网络,通常用于图像处理中。它像一个小网格一样覆盖在图像上,每次与图像中一个像素进行计算,通常以这个像素为中心进行操作。
- 卷积核尺寸:卷积核一次可以“看到”的像素数量;典型情况下为奇数,因为有一个居中像素;卷积核不一定非要是正方形的。
- 边缘效应:当卷积核滑动到图像边缘时,它会遇到问题,因为图像的边缘没有足够的像素来支持完整的卷积操作。例如,当卷积核是 5×5 时,图像边缘部分周围就没有 5×5 的像素可以参与计算。使用填充(padding)。通过在图像边缘添加额外像素(通常为零),使得卷积核能够在图像边缘也进行完整的卷积操作,从而避免边缘效应。
- 步幅:卷积核移动的步长。例如,步幅为 1 时,卷积核每次滑动 1 个像素;步幅为 2 时,卷积核每次跳过 1 个像素,直接滑动 2 个像素。
- 步幅越大,输出图像的尺寸越小。因为卷积核每次跳得更远,计算的区域减少了,导致输出的维度减小。
- 通道和深度:颜色和特征维度。每个像素通常包含多个数值(例如,RGB 图像中的红色、绿色、蓝色通道)。这些数值叫做“通道”(channels)。
- RGB图像 – 3通道, CMYK图像 – 4通道。
- 深度:图像的深度即为其通道数。对于 RGB 图像,深度为 3;对于灰度图像,深度为 1。
- 卷积核的深度:通道的数量,卷积核的深度通常和输入图像的通道数相同。例如:对于一 个在RGB图像上的 5 × 5 的卷积核总共有 5 × 5 × 3 = 75 个权重。
- 每一层的输出深度:在卷积神经网络中,每一层通常会训练多个卷积核。每个卷积核会生成一张特征图(feature map),而每个卷积核的输出深度是 1
- 如果某一层使用了 10 个卷积核,那么该层的输出深度为 10,因为每个卷积核都会生成一张特征图。
卷积核的工作原理:
卷积核本质上是一个权重网络,用于图像处理。它像一个小网格一样覆盖在图像上,每次与图像中一个像素进行计算,通常以该像素为中心。
- 卷积核位置与图像交互:
- 每个卷积核都作用于它所覆盖的图像区域,这个区域通常是矩形或正方形,卷积核的“中心”会覆盖图像中的某个像素。
- 在卷积操作中,卷积核通过加权求和来获取该位置的特征。公式为:下面
其中,Wp 为卷积核的权重,𝑝𝑖𝑥𝑒𝑙𝑝 是图像中对应位置的像素值,𝑃 为卷积核的像素总数。
卷积核的应用:
卷积核不仅应用于深度学习中的卷积神经网络(CNN),也广泛用于传统的图像处理技术中,具体包括:
- 模糊化(Blur):通过加权平均像素值来模糊图像。
- 锐化(Sharpening):通过特定的权重,让图像的边缘更加清晰。
- 边缘检测(Edge Detection):通过检测图像中的显著变化,识别物体的边缘。
- 浮雕化(Embossing):通过增加图像的深度感或3D效果,达到浮雕效果。
CNN
池化:通过将一块像映射为单个像素来降低图像的尺寸,可以缩减图像的维度。有最大值池化和平均值池化。
卷积神经网络的主要思想:1.让神经网络学习哪些卷积最有用 2.使用相同的卷积核集合作用于整幅图像(平移不变性) 3.减少参数的数量,减小“方差”(从“偏差-方差”平衡的角度)
- 输入图像为32×32的灰度图。
- C1、C3和C5是卷积层,用来提取特征并生成特征图。
- S2和S4是池化层,用于降维并减少计算量。
- F6是全连接层,负责将提取到的特征转换为最终的分类输出。
- 输出层给出10个类别的概率,进行数字分类。
总权重
- Conv1:5×5(卷积核尺寸)×6(卷积核个数)+6(偏置项)=6×(5×5+1)=6×26=156
- Conv3:5×5×6(卷积核尺寸×输入通道数)×16(卷积核个数)+16(偏置项)=16×(5×5×6+1)=16×151=2,416
- FC1:5×5×16(卷积核尺寸×输入通道数)×120(卷积核个数)+120(偏置项)= 120×(5×5×16+1)=120×401=48,120
- FC2:120(输入神经元数)×84(输出神经元数)=120×84=10,080
- FC3:84(输入神经元数)×10(输出神经元数)=84×10=840
- ALL:156+0+2,416+0+48,120+10,080+840=61,612
这笔单个全连接层的权重都少。
重点:卷积层具有相对较少的权重。
自然语言处理
文档相似性处理
处理文档相似性问题的常见步骤如下,主要涉及文本的预处理、特征表示和相似性计算。
1. 分词与预处理
- 语料库:也称为文本数据,包含了我们要处理的所有文档。
- 预处理:指对文本进行清洗和规范化,包括去除停用词、标点符号、数字等无关信息,常用的工具包括正则表达式等。
- 分词:将文本切分成单个词汇的过程,常见的分词工具有jieba、NLTK等。
2. 构建字典
从语料库中提取出所有出现的词汇,生成词汇表(字典)。每个词汇会被赋予一个唯一的ID(词ID),用于后续的向量化表示。
3. 构建词袋模型 (BOW)
- BOW模型:该模型忽略词汇顺序,仅统计每个词在文本中出现的频率。
格式:(词ID, 词频)
,例如对于文本“我 爱 学习”,词袋模型可能变为[(我, 1), (爱, 1), (学习, 1)]
。
4. 构建TF-IDF词权重
- TF-IDF:全称“词频-逆文档频率”(Term Frequency-Inverse Document Frequency)。它衡量一个词语对于一篇文档的重要性。
- TF (Term Frequency):词语在文档中出现的频率。
- IDF (Inverse Document Frequency):词语在整个语料库中的重要性,越少出现在其他文档中的词越重要。
- 通过TF-IDF计算得到每个词的权重,常用于改进BOW模型,使常见词语的影响力降低。
5. 构建N-Gram与Phrases模型
- N-Gram:是一种基于窗口大小的序列模型,表示文本中连续的N个词。比如,2-gram表示文本中的连续两个词:
"我 爱"
、"爱 学习"
。 - Phrases模型:通过N-Gram模型发现有意义的短语(如“夏季 联赛”),并将这些短语视为一个词。通过下划线连接(例如“夏季_联赛”),增强模型对常见词组的识别。
6. 使用Gensim构建LDA模型
- LDA (Latent Dirichlet Allocation):一种主题模型,通过它可以为每篇文档生成主题概率分布,并通过指定主题数来找到每篇文档的潜在主题。
- LSI (Latent Semantic Indexing):另一种常用的主题建模方法,通过SVD分解对文档进行降维,再通过余弦相似度来计算文档相似性。
7. 计算文档相似性
- 基于TF-IDF或LDA等模型的输出,计算不同文档之间的相似度。常见的计算方法有余弦相似度,它衡量两个向量夹角的余弦值,即文档之间的相关度。
文本转换为数值:BOW与词向量表示
将文本转化为数值形式是自然语言处理中关键的一步。常见的方法有:
- 词袋模型 (BOW):简单地记录每个词在文本中出现的次数,忽略顺序。适合于结构简单的文本分析。
- 词向量 (Word2Vec, GloVe):每个词通过训练生成一个高维向量,表示词语的语义信息。适用于复杂语境下的文本分析。
应用:通过一维卷积提取特征
- 一维卷积:在文本分类中,通常用卷积神经网络(CNN)来处理文本。通过卷积层,可以提取多个单词之间的关系,从而获得文本的特征表示,进行分类。
TextCNN与SimpleCNN的区别
- SimpleCNN:通常指的是基础的卷积神经网络,其卷积核大小(filter size)通常是固定的,所有卷积操作使用相同的窗口大小。
- TextCNN:为文本处理设计的卷积神经网络,使用了多种不同大小的卷积核(例如,1xN、2xN、3xN等)。通过将不同大小卷积核的结果进行拼接,TextCNN能够捕捉到文本中的多尺度特征,更加适合文本数据。
循环神经网络(RNN)
RNN(循环神经网络)是一种用于处理序列数据的神经网络结构,它通过时间步逐步处理输入数据,并将当前的输入与上一时刻的隐藏状态结合,来生成当前时刻的输出。其核心思想是循环连接,使得模型能够记住之前的信息,从而有效处理时间序列数据(如文本、语音等)。
SimpleRNN:基本想法:使用前面得到的结果,产生后面的输出。坏处是不能并行,长序列效果差。
LSTM:LSTM通过引入“遗忘门”“输入门”和“输出门”来控制信息流动,这使得它能够有效地解决梯度消失和梯度爆炸问题,捕捉长期依赖信息。LSTM能够选择性地“忘记”或“记住”信息,因此对于较长序列的处理效果更好。
RNN的缺点:不能并行,总是依赖前一次输入的结果作为这一次输入的特征之一(于是有了 transformer)
遗忘门,长序列效果差。
假设我们通过训练,得到了下一个词的不同可能性,以及对应的概率,我们如何保证每人得到的值不同?
——设置一个温度,温度越高,越有可能不选择概率最高的词语。
ChatGPT
一次只生成一个词。
假设我们通过训练,得到了下一个词的不同可能性,以及对应的概率,我们如何保证每人得到的值不同?
——设置一个温度,温度越高,越有可能不选择概率最高的词语。
概率是如何得到的?
假设我们得到了关于“cat”的语料库,首先我们统计每个字母出现的次数,然后让a模型产生一些“单词”,显然这个单词并不存在于字典里,所以我们还需要关注字母与字母间的关系,比如q后面经常出现u,再进行训练,就可以产生一些正确的单词。(n-gram)
产生单词后,我们在词和词之间做同样的训练,但是词语的个数远大于字符数,所以需要进行降维处理。(transformer)
Transformer
transformer的产生:
在CNN,中我们用小卷积核提取小范围特征,用多层卷积提取长范围特征,问题是不能看出词的顺序,而且很难反应前面词对后面词的影响;于是产生RNN及LSTM,通过将上一次输入变成下一次的输出,这样可以保持词与词之间的关系,缺点是不能并行执行。于是产生Transformer
Transformer架构:
左侧为编码器(输入一句话,输出一串特征),右侧为解码器(用特征产生结果),另外需要多个编码器嵌套
为什么需要多头记忆力:自然语言有歧义。注意力需要通过查询矩阵Q、键矩阵K、值矩阵V计算,得到一个注意力矩阵
为什么掩码:后面的东西还没生成,把后半边遮住
前馈网络层的作用:引入非线性,不然f1(f2(f3(f4(x)))),这么多个线性函数实际上就相当于一个线性函数
注意力:从大量信息中有选择地筛选出少量重要信息并聚焦到这些重要信息上,忽略大多不重要的信 息。
为了反应词语顺序对句子的影响,可以使用位置编码
相较于LSTM的优点:编码器可以并行计算