ES+MySQL实时搜索架构实战解析
一、ES概述
ElasticSearch(简称 ES)是一个分布式、高扩展、高实时的全文搜索引擎,同时也是一个开源的 NoSQL 数据库,主要用于存储、检索和分析海量数据。它基于 Lucene 搜索引擎库构建,提供了简单易用的 RESTful API,能够快速处理结构化、半结构化和非结构化数据。
ElasticSearch 的核心特性
- 全文搜索能力支持复杂的全文检索,能对文本进行分词、同义词处理、模糊匹配等,比传统数据库的LIKE查询更高效、更精准(例如电商的商品搜索、日志检索)。
- 分布式架构数据自动分片存储在多个节点,支持水平扩展(增加节点即可提升容量和性能),并通过副本机制保证高可用(某节点故障时,副本节点可无缝接管)。
- 实时性数据写入后几乎可以立即被检索到(延迟通常在毫秒级),适合对实时性要求高的场景(如实时监控、即时推荐)。
- 强大的聚合分析内置丰富的聚合功能,可对数据进行统计、分组、计算(如计算某商品类别的销量排名、分析用户行为趋势),常用于数据可视化和业务分析。
- 多数据类型支持不仅支持文本,还能处理数字、日期、地理位置等数据类型,甚至可以存储 JSON 格式的复杂结构。
二、MySQL主从架构场景下Canal+ES的架构
在 MySQL 主从架构中,Canal 和 ElasticSearch(ES)的联合使用,主要是为了实现MySQL 数据实时同步到 ES,从而利用 ES 强大的全文检索和聚合分析能力。这种架构常被用于构建 “MySQL 存储核心业务数据 + ES 提供高效查询” 的系统(如电商商品搜索、日志检索等)。
架构方案:MySQL 主从 + Canal + Elasticsearch
写入路径(强一致)
- 订单/库存写 → 直写 MySQL 主库。
- 从库 负责 SQL 读请求(保障读写分离)。
同步路径(最终一致)
- MySQL 主库产生 Row-Based Binlog。
- Canal 模拟从库订阅 Binlog,解析出 数据变更事件(Insert/Update/Delete)。
- Canal Adapter / 自研消费者 将变更数据同步到 Elasticsearch(Upsert 到索引)。
- ES 中的数据即为库存/商品的近实时视图,延迟通常在毫秒级。
查询路径(低延迟)
- 复杂查询/聚合/搜索 → 直接走 Elasticsearch,单次查询延迟压缩至 100ms 内。
- 简单读(详情、库存校验) → 走从库 MySQL 或 Redis 缓存。
2.1 核心架构原理
MySQL 主从架构提供了数据备份和读写分离能力,而 Canal 可监听 MySQL 的 binlog 日志,将数据变更实时同步到 ES,形成 “MySQL ← Canal → ES” 的数据流闭环。整体架构如下:
- Canal 的角色:模拟 MySQL 从库的 “slave” 角色,伪装成从库连接主库,订阅并解析 binlog 日志,获取数据变更(增删改)。
- ES 的角色:存储同步过来的数据,提供全文检索、复杂聚合等查询能力,减轻 MySQL 的查询压力。
2.2、具体实现步骤
1. 准备基础环境
- MySQL 主库配置:需开启 binlog(log_bin = ON),并设置binlog_format = ROW(Canal 仅支持 ROW 模式,可获取行级变更详情),同时为 Canal 创建具有REPLICATION SLAVE权限的账号(同主从复制的授权逻辑)。
- Canal 部署:下载 Canal(官方地址),配置监听 MySQL 主库的地址、端口、账号密码,以及 binlog 的起始位置(如canal.instance.master.address = 主库IP:3306)。
- ElasticSearch 部署:搭建 ES 集群(单节点也可测试),创建索引(Index)和映射(Mapping),定义需要同步的字段类型(如文本、数字、日期等)。
2. 核心数据同步流程
Canal 监听 MySQL binlog 后,通过以下步骤将数据同步到 ES:
(1)Canal 解析 binlog 获取数据变更
Canal 启动后,会像 MySQL 从库一样连接主库,获取 binlog 日志并解析为结构化的变更数据(包含操作类型:INSERT/UPDATE/DELETE,以及变更前后的字段值)。
例如,MySQL 执行UPDATE user SET name='张三' WHERE id=1,Canal 会解析出:
{"table": "user","type": "UPDATE","data": [{"id": 1, "name": "张三", "age": 20}], // 变更后的数据"old": [{"name": "李四"}] // 变更前的旧数据(仅UPDATE有)
}
(2)数据转换与过滤(关键步骤)
Canal 本身不直接写入 ES,需通过客户端程序(如 Java、Python 脚本)处理数据:
- 过滤无关数据:只同步需要索引到 ES 的表或字段(如过滤掉日志表、临时表)。
- 格式转换:将 MySQL 的字段类型适配为 ES 的映射类型(如 MySQL 的datetime转为 ES 的date)。
- 处理复杂逻辑:如关联多表数据(需查询 MySQL 从库补充关联字段)、计算衍生字段等。
(3)写入 ElasticSearch
客户端程序将转换后的数据通过 ES 的 REST API 写入 ES:
- INSERT:调用POST /索引名/_doc/文档ID新增文档。
- UPDATE:调用PUT /索引名/_doc/文档ID更新文档(或使用_updateAPI 做部分更新)。
- DELETE:调用DELETE /索引名/_doc/文档ID删除文档
2.3、注意事项
数据一致性:
- Canal 同步是异步的,存在毫秒级延迟,需业务容忍 “MySQL 与 ES 数据短暂不一致”。
- 可通过 “双写”(写入 MySQL 的同时调用 ES API)保证强一致,但会增加业务复杂度。
异常处理:
- 需实现失败重试机制(如 Canal 的ack机制确保未处理的变更不会丢失)。
- 定期全量同步校验(如每天凌晨对比 MySQL 与 ES 数据,修复不一致)。
性能优化:
- Canal 客户端批量处理变更(减少 ES 写入次数)。
- ES 开启批量写入 API(_bulk),提升写入效率。
- 对大表同步,可先全量导出 MySQL 数据到 ES,再通过 Canal 同步增量变更。
与 MySQL 从库的关系:
- Canal 监听主库 binlog,与从库同步不冲突(从库仍负责备份和读业务)。
- 若担心主库压力,Canal 也可监听从库的 binlog(需从库开启log_bin)。
三、倒排索引
倒排索引是基于MySQL正排索引的概念来说的
3.1 正向索引
如果把图书馆的每本书看作 “数据载体”,“书的编号” 就是主键,“书名、作者、目录、内容摘要” 就是值 —— 正向索引就像 “书号→书的信息” 的对照表,通过书号能直接查到这本书的所有关键信息。
如果是根据id查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
1)用户搜索数据,条件是title符合"%手机%"2)逐行获取数据,比如id为1的数据3)判断数据中的title是否符合用户搜索条件4)如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
3.2倒排索引
倒排索引中有两个非常重要的概念:
- 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引
3.3正向索引和倒排索引对比
概念区别:
- 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
- 而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
3.4优缺点:
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
四、ES数据库概念
4.1 mysql与elasticsearch
各自长处:
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
我们统一的把mysql与elasticsearch的概念做一下对比:
MySQL | Elasticsearch | 说明 |
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性
五、ES的深分页问题
1. 深分页为什么会慢?
- Elasticsearch 底层是 Lucene,分页使用 from + size。
- 深分页时,比如 from=100000, size=10:
- Lucene 需要先取出前 100010 条数据,再丢掉前 100000 条,只返回最后 10 条。
- 这会导致:
-
- 内存占用大(要维护大量候选结果)。
- CPU 消耗高(排序、打分都要计算)。
- 延迟急剧上升(越靠后页数,延迟越明显)。
2. 常见延迟表现
- 前几页(比如 1~100 页,每页 10 条),延迟在 100ms 级别可接受。
- 深分页(10w 级以上偏移),延迟可能达到秒级,甚至 OOM。
3. 解决策略
(1)Search After(官方推荐)
- 原理:基于上一页的 sort 值继续往后拉,不允许随机跳页。
- 使用场景:游标式翻页,用户只需要“下一页/加载更多”。
- 优点:性能稳定,延迟和页数无关。
- 缺点:不能直接跳到第 N 页。
GET sku_index_v1/_search { "size": 10, "sort": [{ "updatedAt": "asc" }, { "id": "asc" }], "search_after": ["2024-09-09T12:00:00", 12345] }
(2)Scroll API
- 原理:游标方式,快照化查询结果,适合全量扫描。
- 优点:适合日志导出、大批量处理。
- 缺点:占用内存,不能长时间维持;不适合用户交互翻页。
GET /sku_index_v1/_search?scroll=1m { "size": 1000, "query": { "match_all": {} } }
(3)分段查询(数据分片 + 范围查询)
- 原理:通过业务字段(时间/ID 范围)做条件分段,而不是简单的 from+size。
- 例如:按 id > lastId 来查询下一批。
- 优点:轻量,避免深分页;性能高。
- 缺点:需要业务方改造查询逻辑。
(4)预计算与缓存
- 热门分页:可以放到 Redis 缓存,直接返回。
- 大分页导出:走 离线批处理(Hive/Spark → ES),不要依赖实时分页。
六、ES和MySQL的一致性问题
要保证Elasticsearch(ES)和MySQL之间的数据一致性,可以考虑以下几种方法:
- 双写:在应用程序中同时对MySQL和ES进行写操作。这确保了数据同时写入两个系统,从而保持一致性。这种方法简单直接,但会增加写入的延迟和复杂性。
- 使用消息队列:将写操作发送到消息队列,然后由消费者分别将数据写入MySQL和ES。这样可以确保写操作被顺序处理,从而保持数据的一致性。常用的消息队列包括Kafka、RabbitMQ等。
- 使用数据库的binlog:MySQL的binlog记录了数据库的修改操作,可以通过解析binlog来将数据同步到ES。可以使用工具如Debezium、Maxwell等来实时监听和解析binlog,并将数据同步到ES。
- 定期全量同步:定期将MySQL中的数据全量同步到ES。可以使用ETL工具或自定义脚本来实现全量同步。这种方法适用于数据量较小且对实时性要求不高的场景。