从 MySQL 到 Elasticsearch:构建高性能新闻爬虫的数据存储与搜索体系
在实现 多线程网络爬虫与 Elasticsearch 新闻搜索引擎 项目时,数据存储与搜索性能是系统设计的核心。本节将围绕以下几个关键技术展开:
- MyBatis Mapper 的使用与参数规则
- 抽象 DAO 接口实现数据库访问层解耦
- 使用 Docker 部署 MySQL 并结合 Flyway 进行数据库迁移
- MyBatis 批处理与性能优化
- MySQL 索引原理与优化策略
- 使用 Elasticsearch 构建全文搜索引擎
通过这些技术的组合,可以构建一个 既能高效存储结构化数据,又能快速完成全文检索的新闻搜索系统。
一、MyBatis Mapper 配置与参数规则
在 MyBatis 中,SQL 语句通常写在 Mapper.xml 文件中。使用时需要注意以下几个规则:
1 常量必须使用单引号
在 MyBatis 的 XML SQL 中,如果是字符串常量,需要使用 单引号。
例如:
where tableName == 'links_already_processed'
2 参数解析规则
MyBatis 在解析参数时遵循 JavaBean 约定:
- 如果传入的是 Java对象
-
- 通过 getter 方法 获取属性
- 如果传入的是 Map
-
- 通过 key 查找对应 value
例如:
#{link}
- 如果
parameterType是对象 → 调用getLink() - 如果是
HashMap→ 读取map.get("link")
3 MyBatis Mapper 示例
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.hcsp.MyMapper">
<select id="selectNextAvailableLink" resultType="String">
select link
from LINKS_TO_BE_PROCESSED
LIMIT 1
</select>
<delete id="deleteLink" parameterType="String">
DELETE
FROM LINKS_TO_BE_PROCESSED
where link = #{link}
</delete>
<insert id="insertNews" parameterType="com.github.hcsp.News">
insert into news (url, title, content, CREATED_AT, MODIFIED_AT)
values (#{url}, #{title}, #{content}, now(), now())
</insert>
<select id="countLink" resultType="int">
select count(link)
from LINKS_TO_BE_PROCESSED
where link = #{link}
</select>
<insert id="insertLink" parameterType="HashMap">
insert into
<choose>
<when test="tableName == 'links_already_processed'">
links_already_processed
</when>
<otherwise>
links_to_be_processed
</otherwise>
</choose>
(link)
values #{link}
</insert>
</mapper>
这里使用了 MyBatis 的 动态 SQL 标签 <choose> 来根据参数决定插入哪张表。
二、DAO 抽象层设计
为了让系统支持 不同的数据访问实现(JDBC 或 MyBatis) ,可以抽象出统一接口。
public interface CrawlerDao {
String getNextLinkThenDelete() throws SQLException;
void insertNewsIntoDatabase(String url, String title, String content) throws SQLException;
boolean isLinkProcessed(String link) throws SQLException;
void insertProcessedLink(String link) throws SQLException;
void insertLinkToBeProcessed(String href) throws SQLException;
}
然后实现两个版本:
JdbcCrawlerDaoMyBatisCrawlerDao
这种方式遵循 面向接口编程原则,实现了:
- 解耦业务逻辑
- 方便替换实现
- 更利于测试
三、使用 Docker 部署 MySQL
为了方便开发环境管理,可以使用 Docker 运行 MySQL。
1 启动 MySQL
docker run --name mysql \
-e MYSQL_ROOT_PASSWORD=root \
-p 3306:3306 \
-d mysql:5.7.27
删除容器:
docker rm -f mysql
2 持久化数据库数据
如果不做持久化,删除容器后数据会丢失。
创建数据目录:
mkdir mysql-data
运行容器:
docker run --name mysql \
-e MYSQL_ROOT_PASSWORD=root \
-p3306:3306 \
-v "`pwd`/mysql-data":/var/lib/mysql \
-d mysql:5.7.27
参数说明:
| 参数 | 含义 |
|---|---|
| -d | daemon 模式后台运行 |
| -v | 挂载本地目录 |
| -p | 端口映射 |
四、Flyway 数据库迁移
Flyway 用于 数据库版本管理与自动建表。
运行迁移:
mvn flyway:migrate
如果失败可以清理:
mvn flyway:clean
mvn flyway:migrate
五、MySQL JDBC 驱动配置
如果 Flyway 执行失败,可能是缺少 MySQL JDBC 驱动。
Maven 依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
MyBatis 配置:
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/news"/>
建议添加编码参数:
jdbc:mysql://localhost:3306/news?characterEncoding=UTF-8
六、MySQL 字符集配置
推荐使用 utf8mb4,可以完整支持 Unicode。
创建数据库:
create database news;
修改字符集:
ALTER DATABASE news
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
七、MyBatis 批处理优化
批量插入数据时可以使用 MyBatis 的 批处理模式:
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
}
定期提交避免内存占用过大:
if (count % 2000 == 0) {
session.flushStatements();
}
这样可以显著提升批量写入性能。
八、MySQL 索引原理
MySQL 的索引底层结构通常是 B+树。
相比 平衡二叉树:
| 数据结构 | 特点 |
|---|---|
| 平衡二叉树 | 树高较高 |
| B树 | 多叉树 |
| B+树 | 所有数据在叶子节点 |
B+树优势:
- 树高度低 → 磁盘 IO 次数少
- 节点包含多个记录 → 读取效率高
- 支持范围查询
例如:
SELECT * FROM NEWS
WHERE created_at BETWEEN '2021-01-01' AND '2021-02-01'
九、MySQL 索引设计原则
1 主键默认有索引
id
2 尽量使用不重复字段
例如:
id
url
3 字符串索引效率较低
因为比较成本更高。
4 联合索引
例如:
CREATE INDEX created_at_modified_at_index
ON NEWS (created_at, modified_at);
遵循 最左匹配原则:
(a,b,c)
可以使用
a
(a,b)
(a,b,c)
例如:
可以使用索引:
WHERE created_at = '2019-01-01'
AND modified_at < '2019-02-01'
无法完全使用索引:
WHERE created_at > '2019-01-01'
AND modified_at = '2019-02-01'
十、使用 EXPLAIN 分析 SQL
可以使用:
EXPLAIN SELECT * FROM NEWS
WHERE created_at = '2021-08-29';
重点关注字段:
| 字段 | 含义 |
|---|---|
| table | 查询表 |
| type | 查询类型 |
| key | 使用的索引 |
最差情况:
type = ALL
表示 全表扫描。
十一、MySQL 不适合全文搜索
MySQL 适合:
- 数值查询
- 日期查询
- 精确匹配
但对于 文本搜索:
title like '%外交部%'
效率较低。
因此需要引入专业搜索引擎。
十二、Elasticsearch 原理
Elasticsearch 使用 倒排索引(Inverted Index) 。
与传统 B 树不同:
传统结构:
文档 → 单词
倒排索引:
单词 → 文档
例如:
外交部 → 文档1 文档3 文档5
这使得 全文搜索效率极高。
十三、使用 Docker 安装 Elasticsearch
docker run -d \
--name elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
elasticsearch:7.4.0
持久化数据:
mkdir esdata
docker run -d \
-v "`pwd`/esdata":/usr/share/elasticsearch/data \
--name elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
elasticsearch:7.4.0
访问:
http://localhost:9200
十四、Elasticsearch 数据写入
Java 示例:
for (News news : newsFromMySQL) {
IndexRequest request = new IndexRequest("news");
Map<String, Object> data = new HashMap<>();
data.put("content",
news.getContent().length() > 10
? news.getContent().substring(0,10)
: news.getContent());
data.put("url", news.getUrl());
data.put("title", news.getTitle());
data.put("createdAt", news.getCreatedAt());
data.put("modifiedAt", news.getModifiedAt());
request.source(data, XContentType.JSON);
bulkRequest.add(request);
}
使用 Bulk API 批量写入可以大幅提升性能。
十五、Elasticsearch 查询示例
全部查询:
http://localhost:9200/news/_search
关键字查询:
http://localhost:9200/news/_search?q=title:外交部
统计文档数量:
http://localhost:9200/_count
十六、Elasticsearch 集群的意义
访问:
http://localhost:9200/_cluster/health
Elasticsearch 采用 分布式架构,主要原因:
1 数据备份
节点故障时数据不会丢失。
2 水平扩展
可以通过增加机器提升性能。
在分布式系统领域有一句经典的话:
能通过增加机器解决的问题,通常都不是问题。
总结
本节完成了新闻爬虫系统 数据层与搜索层的关键架构设计:
- 使用 MyBatis 管理 SQL
- 抽象 DAO 接口 解耦数据访问
- 使用 Docker + MySQL 构建数据库环境
- 通过 Flyway 管理数据库版本
- 利用 B+树索引 优化数据库查询
- 使用 Elasticsearch 倒排索引 实现全文搜索
最终形成了一个典型的 搜索系统架构:
爬虫
↓
MySQL(结构化存储)
↓
数据同步
↓
Elasticsearch(全文检索)
↓
搜索接口
这种架构也是许多大型互联网系统(新闻、博客、商品搜索等)常用的设计模式。