搜索标签系统专题01:基本搜索系统搭建 —— CQRS 架构
需求背景
从最简单的实现开始,构建一个图书信息聚合和搜索系统。从用户交互角度,前端是一个图书信息聚合和展示的页面,支持多条件的复杂搜索功能。从后端角度来说,需要支持名称模糊搜索、作者名模糊搜索、出版年份搜索、出版社、图书类型、语言、国家、地区等搜索。
问题现状
这是一个图书信息聚合搜索系统,需要支持复杂的搜索功能。传统的数据库查询无法满足全文检索、模糊搜索、多条件组合等需求。
方案
为了支持复杂条件查询的需求,我们需要在系统中接入 ES 这个查询引擎中间件,MySQL 负责存储实体数据,ES 负责支持复杂查询,数据模型以 MySQL 为基准。
接下来的问题就是如何设计 ES 和 MySQL 的协同架构,我们选择 CQRS 架构。
CQRS(命令查询责任分离,Command Query Responsibility Segregation),也可以简单理解为读写分离架构,是一种将数据写入(命令)和数据读取(查询)完全拆分的架构模式。通过分别优化读和写流程,CQRS 能有效提升系统的性能、可扩展性与稳定性。
写入流程可专注于数据一致性和事务,而查询流程则可根据业务需求进行检索和聚合优化,支持复杂的全文搜索、条件筛选。CQRS 广泛用于高并发、数据存储和搜索需求强烈的场景,避免传统 SQL 数据的 CRUD 架构因数据一致性与高性能复杂查询需求难兼容带来的瓶颈。
CQRS 对比 传统 CRUD
| 维度 | CRUD | CQRS | 提升 |
|---|---|---|---|
| 搜索性能 | 普通数据库查询 | ES 全文检索 | 十分明显 |
| 复杂查询支持 | 有限 | 强大 | 质的提升 |
| 写性能 | 受搜索影响 | 独立优化 | 无影响 |
| 数据一致性 | 强一致 | 最终一致 | 可接受 |
CQRS 流程设计
写操作
- 写操作直接写入 MySQL,支持事务
- 变化数据异步异构到 ES,这里可以采用简单的 CDC 实现。
- 允许 ES 和 MySQL 之间的弱一致性延迟
读操作
- 在 ES 中根据复杂查询条件查出实体 ID 列表
- 根据实体 ID 列表从 MySQL 中反查出实体数据,所有数据以 MySQL 为基准。
数据模型设计
MySQL 实体主表:
create table book_base (
book_id bigint primary key comment '图书ID',
name varchar(255) comment '图书名称',
author varchar(255) comment '图书作者',
press varchar(255) comment '出版社',
publish_time datetime comment '出版时间',
type int comment '图书类型',
language int comment '语言',
country_id int comment '国家',
region_id bigint comment '地区',
introduction text comment '图书简介' -- 非搜索项
)
Elasticsearch 文档结构:
{
"mappings": {
"properties": {
"book_id": {
"type": "long"
},
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"author": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"press": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"publish_time": {
"type": "date",
"format": "yyyy-MM-dd"
},
"type": {
"type": "integer"
},
"language": {
"type": "integer"
},
"country_id": {
"type": "integer"
},
"region_id": {
"type": "long"
}
}
}
}