首页 数据库 【大数据高并发核心场景实战】 数据持久化层 - 查询分离

【大数据高并发核心场景实战】 数据持久化层 - 查询分离

1. 业务场景
适用场景:

数据查询缓慢(数据量大导致、数据聚合时调用外部系统过多导致等)
写数据效率尚可
所有数据都可能修改(若存在冷数据,可使用上一章的冷热分离方案)
基本思路:将更新的数据放在主数据库里,而查询的数据放在另外一个专门针对搜索的存储系统里。主库单表查询,无关联无外键,所以写数据无压力。数据查询通过一个专门处理大数据量的查询引擎来解决。

这里有同学可能会提到数据库读写分离,这种情况下在千万级别数据量下的速度提升并不大,并且只能解决数据库查询慢的问题,不能解决其他如查询详情时调用外部系统耗时长导致的查询慢问题。

核心问题:

如何触发查询分离?
如何实现查询分离?
查询数据如何存储?
查询数据如何使用?
历史数据如何迁移?
2. 查询分离
2.1 如何触发查询分离
1)修改业务代码,写入同时同步更新查询数据

同步更新示意图
图2-1: 同步更新查询数据示意图
2)修改业务代码,在写入常规数据后,异步更新查询数据

异步更新示意图
图2-2: 异步更新查询数据示意图
3)监控数据库日志,如有数据变更,则更新查询数据

优点是不会影响业务代码。

监控日志更新示意图
图2-3: 监控数据库日志更新查询数据示意图
优缺点对比

优缺点对比表
图2-4: 三种触发方式优缺点对比表
针对优缺点总结适用场景

适用场景总结
图2-5: 三种方法适用场景总结
2.2 如何实现查询分离
这里以方法二,业务代码异步更新查询数据的方式为例讲解实现方式,这个方法需要考虑以下几个问题:

写操作较多且线程太多时,需要加以控制,否则太多线程最终会拖垮JVM
创建查询数据的线程出错时,如何自动重试?如何标识更新失败的数据?
多线程并发时,需要解决很多并发场景
针对以上问题,可以考虑使用MQ来解决:在短时间线程过多时,将任务暂存到MQ中间件进行削峰处理;业务失败时可自动重新发送消息重试。

MQ解决方案示意图
图2-6: MQ解决方案架构示意图
具体方案

写操作时,主数据表添加标识 NeedUpdateQueryData=true,MQ消息简单,只是一个信号来告知更新数据,不包含更新的数据ID(如果包含业务信息,就需要考虑更多的幂等和消息丢失等问题)
消费者获取信号后,先批量查询待更新的主数据,然后批量更新查询数据,更新完成后将查询数据的主数据标识 NeedUpdateQueryData 更新为 false
若存在多个消费者同时有迁移动作的情况,就涉及并发性问题,这与前一场景冷热分离中的并发性处理逻辑类似,这里不再赘述
消息的时序性问题:

生产者1 将数据A修改为A1,发送消息Q1
生产者2 将数据A1修改为A2,发送消息Q2
消费者1 收到Q1,查询数据为A1(此时消费者2收到Q2,将数据A2迁移到缓存),A1迁移到缓存
即消费者查询数据库数据后,在未迁移数据时被后触发的消费者线程更新了迁移了更新的数据,而后先消费的消费者会将后消费消费者的迁移更新掉,导致缓存本该后迁移记录丢失。

解决方法:消费者查询 NeedUpdateQueryData=true 数据的同时查询 lastUpdateTime 作为乐观锁字段进行更新。

2.3 查询数据如何存储
常用的两个中间件是 MongoDB 和 ES,选择取决于团队成员的技术结构。我们团队选择的是 ES。

特性维度 MongoDB Elasticsearch
数据模型 文档型数据库,类似JSON,结构灵活 搜索引擎,擅长处理非结构化文本数据
核心优势 高性能读写、灵活的数据模型、横向扩展 强大的全文检索、复杂查询和数据分析
查询场景 适合精确查询、范围查询、事务和聚合操作 适合模糊匹配、全文搜索、多条件复杂检索
写入性能 写入速度较快,支持高并发写入 写入吞吐量通常低于MongoDB,但近实时搜索(秒级)
读取性能 精确查询和聚合操作性能优秀 复杂搜索和全文检索性能卓越
事务支持 支持多文档ACID事务 不支持事务,保证最终一致性
资源消耗 磁盘占用通常更小(高压缩存储引擎) 磁盘和内存消耗相对较高
扩展性 支持分片集群,需手动配置 天生分布式,开箱即用,自动分片
管理维护 集群配置相对复杂,需要专业知识 管理相对简单,有完善的监控工具
适用场景 Web应用后端、用户画像、设备监控 搜索引擎、日志分析、实时监控
不适用场景 复杂的全文搜索需求 需要强事务一致性的场景
学习成本 中等,查询语法相对简单 较高,查询DSL较复杂
社区生态 成熟稳定,社区活跃 生态丰富,插件众多
成本考量 通常存储成本更低 资源消耗大,总体成本可能更高
2.4 查询数据如何使用
ES自带查询API,在业务代码中直接调用ES即可。这里涉及到一个场景:缓存和数据库数据不一致的问题。

两种解决思路:

在查询数据更新到最新前,不允许用户查询(在数据同步完成前,强制查询走主数据源如MySQL,而不是ES)
给用户提示"当前数据为2s前的数据,如发现数据不准确可尝试刷新",通常用户都能接受
2.5 历史数据迁移
当前方案中,只需要把所有历史数据加上标识 NeedUpdateQueryData=true,程序就会自动处理。

2.6 MQ+ES 整体方案
业务数据修改后,触发异步线程数据同步
触发异步方式使用MQ(解耦、削峰)
查询数据到ES(适合大数据量的复杂查询)
查询数据同步到ES会有一定延时,用户可能查询到旧数据,需给用户提示
历史数据迁移,只需把所有历史数据的标识改成true,系统会自动批量同步到ES
整体方案示意图
图2-7: MQ+ES整体架构方案示意图
这个整体方案看似简单,但有一些陷阱必须注意。下面着重介绍使用Elasticsearch时的注意事项。

3. ElasticSearch注意事项
Elasticsearch的使用要点:

如何使用Elasticsearch设计表结构?
Elasticsearch的存储结构
Elasticsearch如何修改表结构?
Elasticsearch的准实时性
Elasticsearch可能丢数据
Elasticsearch分页
3.1 如何使用Elasticsearch设计表结构
Elasticsearch基于索引设计,无法像MySQL那样使用join查询,所以查询数据时需要把每条主数据及关联子表的数据全部整合在一条记录中。

下面以常见的订单业务类讲解如何设计ES表结构:

订单数据结构
图3-1: 订单业务数据结构示意图
虽然订单数据在关系型数据库中涉及多表,但使用Elasticsearch存储数据时不会设计多个表,而是将所有表的相关字段数据汇集在一个Document中,即一个完整的文档结构:

{
"order_ID": "o2020103115214521",
"order_invoice": {},
"user": {
"user_ID": "U1099",
"user_name": "YiHuiComeOn"
},
"order_product_item": [
{
"product_name": "乒乓球拍",
"product_count": 1,
"product_price": 149
},
{
"product_name": "纸巾",
"product_count": 2,
"product_price": 1.4
}
],
"total_amount": 20
}
习惯关系型数据库的同学可能会有疑惑:为什么汇聚到同一document中?为什么ES不需要关联查询?这就涉及到ES特殊的存储结构。

3.2 Elasticsearch的存储结构
3.2.1 Lucene和MySQL的概念对照
Lucene是一个索引系统,此处把Lucene与MySQL的一些概念做简单对照:

Lucene与MySQL概念对照
图3-2: Lucene与MySQL概念对照表
3.2.2 无结构文档的倒排索引
假设有一些无结构文档数据:

无结构文档
图3-3: 无结构文档示例
简单倒排索引后的结果:

简单倒排索引结果
图3-4: 无结构文档倒排索引结果
无结构的文档经过简单的倒排索引后,字典表主要存放关键字,而倒排表存放该关键字所在的文档ID。业务数据通常不是无结构的文档内容,而是有结构的数据,此时如何倒排索引呢?

3.2.3 有结构文档的倒排索引
更复杂的例子:每个Doc都有多个Field,Field有不同的值(包含不同的Term,Term是经过文本分析处理后不可再分割的最小单位)。

有结构文档示例
图3-5: 有结构文档示例
倒排表:

性别倒排索引
性别倒排索引
图3-6: 性别字段倒排索引示例
年龄倒排索引
年龄倒排索引
图3-7: 年龄字段倒排索引示例
武功倒排索引
武功倒排索引
图3-8: 武功字段倒排索引示例
由此可见,有结构的文档经过倒排索引后,字段中的每个值都是一个关键字,存放在Term Dictionary中,且每个关键字都有对应地址指向所在文档。

3.2.4 ES的Document如何定义结构和字段格式
设计ES的Document结构时,不需要像MySQL那样关联表,而是把所有相关数据汇集在一个Document中。直接将3.1节中订单的JSON文档转成一个ES文档(SQL中的子表数据在Elasticsearch中以嵌入式对象格式存储):

{
"mappings": {
"doc": {
"properties": {
"order_ID": {
"type": "text"
},
"order_invoice": {
"type": "nested"
},
"order_product_item": {
"type": "nested",
"properties": {
"product_name": {
"type": "text"
}
}
},
"total_amount": {
"type": "long"
},
"user": {
"properties": {
"user_ID": {
"type": "text"
},
"user_name": {
"type": "text"
}
}
}
}
}
}
}
至此,大家已经了解了Elasticsearch表结构的设计。在实际业务中,主数据修改表结构时,ES也要求修改文档结构,这时该怎么办?

3.3 Elasticsearch如何修改表结构
ES支持直接添加新字段
因为修改字段的类型会导致索引失效,所以ES不支持修改原字段类型
Elasticsearch底层基于Lucene,Lucene的倒排索引一旦创建就是不可变的。就像印刷好的书籍,你不能直接修改某一页的排版,只能重新印刷一本。

如果想修改字段的映射(表结构),需要新建一个索引,然后使用Elasticsearch的reindex功能将旧索引复制到新索引中
POST /_reindex
{
"source": {"index": "products_old"},
"dest": {"index": "products_new"}
}
reindex功能会使旧索引失效,直接重命名字段时可以使用alias索引功能。

注意:通常不会直接删除旧字段,常用做法是新版本项目代码兼容旧数据,在项目稳定运行后,再考虑清理旧字段。

3.4 陷阱一:Elasticsearch是准实时的吗
当更新数据至Elasticsearch且返回成功提示时,通过Elasticsearch查询返回的数据可能不是最新的。

这个过程涉及Elasticsearch的Shard(分片),以及Lucene Index、Segment、Document三者之间的关系。

Elasticsearch的一个Shard就是一个Lucene Index,每一个Lucene Index由多个Segment构成。

分片(Shard)结构图

分片结构图
图3-9: Elasticsearch分片结构示意图
Index、Segment、Document三者之间的关系

三者关系图
图3-10: Index、Segment、Document关系图
数据索引的过程详解:

当新的Document被创建时,数据首先会存放到新的Segment中,同时旧Document会被删除,并在原来的Segment上标记删除标识。当Document被更新时,旧版Document会被标识为删除,并将新版Document存放在新的Segment中

Shard收到写请求时,请求会被写入Translog中,然后Document被存放在Memory Buffer中

写请求处理
图3-11: Elasticsearch写请求处理流程
注意:Memory Buffer 不会被查询到

每隔1秒(默认设置),Refresh操作被执行一次,Memory Buffer中的数据会被写入一个Segment,并存放在File System Cache中,这时新数据就可以被搜索到了
Refresh操作示意图
图3-12: Refresh操作数据刷新流程
通俗理解整个过程:

名词解释:

Document:ES中的基本数据单元,相当于一条记录
Segment:Lucene索引的基本单元,是不可变的
Memory Buffer:临时存储新文档的内存区域
Translog:记录所有写操作的日志文件
Refresh:将内存中的数据写入新Segment并使其可搜索的操作
File System Cache:操作系统级别的磁盘缓存
流程解释:

新数据到达:先登记到Translog,再放到Memory Buffer
定期刷新:每1秒将Memory Buffer中的数据写入Segment,放到File System Cache
此时数据可被搜索
通过以上数据索引过程的说明,可以发现Elasticsearch并不是实时的,而是有1秒延时。解决方案是提示用户查询的数据会有一定延时。

3.5 陷阱二:Elasticsearch宕机恢复后,数据丢失
上一小节中提及每隔1秒Memory Buffer中数据会被刷到Segment中,此时数据可被用户搜索到,但没有持久化,一旦系统宕机,数据就会丢失。

如何防止数据丢失?使用Lucene中的Commit操作解决这个问题。

Commit操作方法:先将多个Segment合并保存到磁盘中,再进行持久化标记。

但commit有两个问题:

会占用IO资源,使得commit期间数据查询变慢
无法解决数据保存时,在translog写完还未写入文件系统缓存情况的数据丢失
translog持久化到磁盘需要执行fsync操作,具体实现方法有两种:

将index.translog.durability设置成request,缺点是耗费资源,性能差一些
将index.translog.durability设置为async,每隔index.translog.sync_interval时间执行一次fsync
配置建议
# 方案A:金融级安全(不能丢任何数据)
PUT /my_index/_settings
{
"index.translog.durability": "request"
}

# 方案B:普通业务(可容忍少量数据丢失)
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
实践总结
根据业务需求选择策略

业务类型 推荐配置 解释
金融交易 durability: request 数据绝对不能丢失
电商订单 durability: async, sync_interval: 1s 可容忍极短时间延迟
日志分析 durability: async, sync_interval: 5s 丢几条日志没关系
记住:没有完美的方案,只有适合你业务需求的方案!

3.6 陷阱三:分页越深,查询效率越低
Elasticsearch的读操作流程主要分为两个阶段:Query Phase、Fetch Phase。

Query Phase:协调节点先把请求分发到所有分片,每个分片在本地查询后建一个结果集队列,将Document ID以及搜索分数存放在队列中,再返回给协调节点,协调节点建全局队列,归并所有结果集并进行全局排序
Tips:在Elasticsearch查询过程中,如果search方法带有from和size参数,Elasticsearch集群需要给协调节点返回分片数×(from+size)条数据,然后在单机上进行排序,最后给客户端返回size大小的数据。比如客户端请求10条数据,有3个分片,那么每个分片会返回10条数据,协调节点最后会归并30条数据,但最终只返回10条数据给客户端。

Elasticsearch读操作示意图
图3-13: Elasticsearch读操作两阶段流程
Fetch Phase:协调节点先根据结果集里的Document ID向所有分片获取完整的Document,然后所有分片返回完整的Document给协调节点,最后协调节点将结果返回给客户端
比如有5个分片,需要查询排序序号从10000到10010(from=10000,size=10)的结果,每个分片返回给协调节点计算的数据量是10010条。这是为了防止其他分片中没有数据,考虑最坏情况10010条数据都在自己分片上,进而把10010条数据全部给协调节点去聚合计算。

也就是说,协调节点需要在内存中计算10010×5=50050条记录,所以用户分页越深查询速度会越慢,分页并不是越多越好。

那如何更好地解决Elasticsearch分页问题呢?为了控制性能,可以使用Elasticsearch中的max_result_window进行配置,这个数据默认为10000,当from+size > max_result_window时,Elasticsearch将返回错误。

如果用户确实有深度翻页的需求,使用Elasticsearch中search_after的功能也能解决,只是无法实现跳页(这样分片可以利用游标条件过滤部分数据,从而减少数据计算的数量提升查询速度)。

举例,查询结果按照订单总金额分页,上一页最后一个订单的总金额total_amount是10,那么下一页的查询示例代码如下:

{
"query": {
"bool": {
"must": [
{
"term": {
"user.user_name.keyword": "YiHuiComeOn"
}
}
],
"must_not": [],
"should": []
}
},
"from": 0,
"size": 2,
"search_after": ["10"],
"sort": [
{
"total_amount": "asc"
}
],
"aggs": {}
}
至此,Elasticsearch的一些要点就介绍完了。MQ也有一些要点,比如确保时序、确保重试、确保消息重复消费不会影响业务,以及确保消息不丢失等,后续各章节会有相应的场景描述,这里就不再展开了。

4. 小结
查询分离这个解决方案虽然能解决一些问题,但也要认识到它的不足:

使用Elasticsearch存储查询数据时,要接受一些局限性:有一定延时,深度分页不能自由跳页,有丢数据的可能性
主数据量越来越大后,写操作还是慢,到时还是会出问题。比如工单数据,虽然已经去掉所有外键,但当数据量上亿时,插入还是会有问题
主数据和查询数据不一致时,如果业务逻辑需要查询数据保持一致性呢?查询数据同步到最新数据会有约2秒延时,某些业务场景下用户可能无法接受
架构"没有银弹",不能期望一个解决方案既能覆盖所有的问题,还能实现最小的成本损耗。

如果碰到一个场景不能接受上面某个或某些不足时,该怎么解决?接着看后面的章节。

站星网

1. 业务场景适用场景:数据查询缓慢(数据量大导致、数据聚合时调用外部系统过多导致等)写数据效率尚可所..

为您推荐

Coze智能体实战:零基础学会搭建热点监控AI Agent,选题效率提升10倍

大家好,我是汤师爷,专注 AI 智能体分享,致力于帮助 100W 人用智能体创富~短视频内容创作小白经常会遇到这样的困扰。每天花大量时间刷视频,想要找到你所在赛道的爆款内容,却总是难以系统地整理和分析?想要批量..

大数据领域面临的挑战与未来的演进趋势

序有群友提了一个关于大数据行业当前发展状况的问题。暂时先用GPT帮我们对此问题,搜罗和总结一二。其一,此问题较为宏大,需要结合个人实践、对行业的综合认知做深度思考,较为耗时。短时间内,懒于再一一总结和思..

如果单表数据量大,只能考虑分库分表吗?

程序员最怕啥?不是需求改八遍,也不是半夜报警电话,而是数据库突然卡成PPT!尤其是当单表数据冲到几千万行,查询慢得像老牛拉车,这时候团队第一反应往往是:“赶紧分库分表!”但兄弟,分库分表可不是什么温柔小..

实时数据的处理一致性如何保证?

实时数据一致性的定义以及面临的挑战数据一致性通常指的是数据在整个系统或多个系统中保持准确、可靠和同步的状态。在实时数据处理中,一致性包括但不限于数据的准确性、完整性、时效性和顺序性。下图是典型的实时/..

关于大数据的一些真知灼见

大数据很强大,但还是有很多人仍然不知道它到底是什么。让我们来学习大数据的真实表现,以及如何更好地促进企业转型。或许我们经常听到有人讲大数据,但仍然有很多人不知道它到底是什么。因为我确信它很强大,所以我..

程序员最核心的竞争力是什么?

进行社招面试时,有一个问题几乎是必问的:你为什么要离开上一家公司?其实这个问题主要是想试探一下求职者的核心诉求,并借此预估一下他在本公司工作的稳定性。常见的答案也无非就是这么几种:对薪酬不满意、干得不..

.net环境下跨进程、高频率读写数据

一、需求背景1、最近项目要求高频次地读写数据,数据量也不是很大,多表总共加起来在百万条上下。单表最大的也在25万左右,历史数据表因为不涉及所以不用考虑,难点在于这个规模的热点数据,变化非常频繁。数据来源..

程序员持续成长,需要持久而痛苦的学习

前言每个程序员心里多多少少都有一个成为技术大牛的梦。毕竟梦想总是要有的,万一实现了呢?奇葩说第五季里有一期辩题是《高薪不喜欢的还是低薪喜欢的工作,你选哪一个?》在所有人的发言中,蔡康永的一句话最让人触..

工作中人们常提到的数据预处理,说的到底是什么?

数据预处理一方面是为了提高数据的质量,另一方面也是为了适应所做数据分析的软件或者方法。在做数据分析时,我想许多数据分析师会像《R语言实战第二版》的作者卡巴科弗那样发出感叹:“数据分析师在数据预处理上花..

CSS砌体布局示例和使用场景

CSS砌体布局(Masonry Layout)CSS砌体布局是一种网页布局技术,它的灵感来源于砖石墙的排列方式,类似于“拼图”或“拼砖”的效果。在砌体布局中,元素的排列并不完全遵循传统的网格布局规则,..

mysql随机获取一条或者多条数据

语句一:select * from users order by rand() LIMIT 1MYSQL手册里面针对RAND()的提示大概意思就是,在 ORDER BY从句里面不能使用RAND()函数,因为这样会导致数据列被多次扫描,导致效率相当相当的低,效率不行,切..

多语言网站数据库文章表设计

设计一个支持多语言的网站数据库时,应该确保内容能够方便地扩展和管理。以下是多语言数据库表设计的关键原则和示例:设计原则分离内容与语言:将与语言相关的内容独立存储,不直接硬编码到主要表中。每个支持多语言..

ASP.NET 使用Entity Framework (EF) 创建迁移修改SQLite数据库表结构

在 ASP.NET 中,使用 Entity Framework (EF) 创建并连接 SQLite 数据库是一种轻量级、高效的数据库管理方式。以下是详细步骤:安装必要的 NuGet 包安装EntityFrameworkCore.Sqlite包:Install-Package Microsoft.Ent..

Mysql查询一段时间内的数据

select * from wap_content where week(created_at) = week(now)如果你要严格要求是某一年的,那可以这样查询一天:select * from table where to_days(column_time) = to_days(now());select * from table where da..

SQLite性能支持多少数据量?

SQLite是一种轻量级的关系型数据库管理系统,广泛应用于移动应用、嵌入式系统和小型桌面应用程序中。由于其零配置、自给自足的特性,SQLite在很多场景下非常受欢迎。然而,对于许多开发者来说,一个常见的问题是:SQ..

Sylvan.Data.Excel 性能优异的开源.NET Excel数据读取库

Sylvan.Data.Excel是一个开源、免费、跨平台的.NET库,专注于读取和写入Excel数据文件。支持多种文件格式,并提供高效的数据访问和数据绑定功能。该库在.NET生态系统中是读取Excel数据文件的最快且内存分配最低的库..

HTQL 提取和查询HTML和XML数据的轻量级查询语言

HTQL(Hyper-Text Query Language)是一种用于提取和查询HTML和XML数据的轻量级查询语言。HTQL提供类似SQL的语法,可以方便地从网页或其他基于标签的文档中提取结构化数据,而无需解析整个文档。这使得它在爬虫、数..

使用ADO.NET连接到南大通用GBase 8s数据库

南大通用GBase 8s数据库广泛应用于各种企业级应用中,对于开发者而言,掌握如何使用ADO.NET连接到GBase 8s数据库非常重要。本文将详细阐述如何通过ADO.NET方式连接到南大通用GBase 8s数据库,并进行基本的数据库操作..

MySQL 5.x和MySQL 8.x数据库的区别

MySQL 是开源关系型数据库的代表,广泛应用于不同规模的 Web 和企业应用中。从 MySQL 5.x 到 MySQL 8.x 的升级带来了大量功能改进和性能提升。为了帮助大家更直观地理解两者的区别,本文将通过详细介绍并结合实际的 ..

Redis 同步、击穿、穿透及雪崩简述

对Redis最常见的几个问题,简要的说下我的理解与解决方法。数据同步指Redis做为缓存,在数据变化时,怎么保持与数据库数据同步的。一般解决方案为:缓存双删(同步方案大都采用删除缓存,而不会更新新缓存。缓存击穿..

发表回复

返回顶部

微信分享

微信分享二维码

扫描二维码分享到微信或朋友圈

链接已复制
蜂鸟影院2048影视资源论坛熊猫影视河马影视星辰影视萝卜影院八哥电影网人人看电影无忧影视网橙子影视网叮当影视网天天影视网青青影视网电影天堂开心追剧网西瓜影院麻花影视网70影视网年钻网茶小舍电影藏影堂新神州影域煮酒观影体积影视爱看影院星光电影至尊影院极影公社超清视界