从 MySQL 到 Elasticsearch:构建高性能新闻爬虫的数据存储与搜索体系

14 阅读6分钟

从 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;
}

然后实现两个版本:

  • JdbcCrawlerDao
  • MyBatisCrawlerDao

这种方式遵循 面向接口编程原则,实现了:

  • 解耦业务逻辑
  • 方便替换实现
  • 更利于测试

三、使用 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

参数说明:

参数含义
-ddaemon 模式后台运行
-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+树优势:

  1. 树高度低 → 磁盘 IO 次数少
  2. 节点包含多个记录 → 读取效率高
  3. 支持范围查询

例如:

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 水平扩展

可以通过增加机器提升性能。

在分布式系统领域有一句经典的话:

能通过增加机器解决的问题,通常都不是问题。


总结

本节完成了新闻爬虫系统 数据层与搜索层的关键架构设计

  1. 使用 MyBatis 管理 SQL
  2. 抽象 DAO 接口 解耦数据访问
  3. 使用 Docker + MySQL 构建数据库环境
  4. 通过 Flyway 管理数据库版本
  5. 利用 B+树索引 优化数据库查询
  6. 使用 Elasticsearch 倒排索引 实现全文搜索

最终形成了一个典型的 搜索系统架构

爬虫
  ↓
MySQL(结构化存储)
  ↓
数据同步
  ↓
Elasticsearch(全文检索)
  ↓
搜索接口

这种架构也是许多大型互联网系统(新闻、博客、商品搜索等)常用的设计模式。