30项目实战 - 多线程网络爬虫与Elasticsearch新闻搜索引擎

258 阅读15分钟

从零开始做一个项目的原则

  • 把每个项目都当作人生最好的一个项目来精雕细琢
    • 积累自己的reputation声誉
    • 一丝不苟地写好文档
    • 代码质量++
    • 你的认证是肯定能够获得回报的
  • 使用标准化,业界公认的模式和流程
  • (几乎)没有本地以来,使用者能毫无障碍地运行
  • 小步快跑
    • 成就感
    • 越小的变更越容易debug

项目的原则

  • 【强制】使用GitHub+主干/分支模型进行开发
    • 禁止直接push master
    • 所有的变更通过pr进行
  • 【强制】自动化代码质量检查+测试
    • Checkstyle/spotBugs
    • 最基本的自动化测试覆盖
  • 一切工作自动化
  • 规范化提交流程

初始化项目与项目设计流程

  • GitHub-new
  • 建立新项目
    • mvn archetype
    • IDEA - new
    • 直接从别人那儿抄一个
  • .gitignore
  • README
  • 配置基本的代码质量检查插件
    • 越早代价越低

改一下复制过来的一些信息

  • groupId,artifactId

项目的设计流程

  • 自顶向下

1架构图.png - 多人协作

2.png - 模块化 - 各模块间指责明确,界限清晰 - 基本的文档 - 基本的接口 - 小步提交 - 大的变更难以review - 大的变更冲突更加棘手

  • 自下向上
    • 先实现功能

3.png

  • 在实现的过程中不停地抽取共用部分
    • 每当你写出很长很啰嗦的代码的时候,就要重构了
    • DRY: 每当你复制/粘贴的时候,就要重构了
  • 通过重构实现模块化,接口话

项目的演进:可扩展性

  • 模块化的好处,方便替换
  • 单线程 -> 多线程
  • console -> H2 database
  • H2 database -> Elasticsearch

项目的演进:正确性

  • 如何保证改动代码不会破坏原先的功能
  • 通过测试

如何养成好的代码习惯

  • 很丑的方式实现
  • 不要妥协

代码回滚几个版本

  • git reset ~1,回一个版本
  • 不小心提交并且push了的情况
    • 在主干上就老老实实的把多提交的文件删除
    • 在分枝上就一样reset,然后force push

专业的commit 怎么写

  • 先总结一行,然后具体描述

项目目标

  • 爬取新浪新闻页,做一个真正的爬虫
  • 使用数据库储存并进行数据分析
  • 随着数据量的增长,迁移到ES
  • 做一个简单的"新闻搜索引擎"

确定算法

  • 为什么互联网被称为网,爬虫被称为爬虫
    • 从一个节点出发,遍历所有的节点

5.png

  • 算法:广度优先算法的一个变体
  • 如何扩展?
    • 慢慢地把烂代码,啰嗦的代码重构掉
    • 加入我想要未来换数据库/上Elasticsearch
    • 爬虫的通用化
  • 广度优先算法建议学习手写一下,实现遍历一颗树,能学习到队列的数据结构,JDK的队列的实现

代码抽取短小化的好处

    1. 你是个人类,大脑更容易处理短小的方法
    1. 越短的代码越容易复用
    1. 对于java来说,可以对复用代码进行覆盖,实现多态这样的功能

java8的steam

  • 把过程性语言,替换成描述性语言
ArrayList<Element> links = doc.select("a");
           
for (Element aTag : links) {
    linkPool.add(aTag.attr("href"));
}
  • 描述性语言
 // map就是把一个数据变成另一个数据
links.stream().map(aTag -> aTag.attr("href")).forEach(linkPool::add);

merge的三种情况

6.png

  • 第一个按钮等价于切到主分支git merge algorithm-basic,会产生一个新的提交

7.png

  • 第二个选项,squash挤压,基本上等价于git merge --squash,好处是一堆提交压扁成一个提交,
  • 第三个选项,等价于rebase,

另一个代码检查工具,spot-bugs的使用

  • 如何阅读和使用官方文档?
  • Maven的生命周期
  • Maven的插件与目标
    • 目标与生命周期阶段的绑定
  • 运行mvn spotbugs:check
  • maven是有一套生命周期的,有三种,最常用的就是默认的,要执行什么就从头执行到目标声明周期

maven的生命周期

  • 默认什么都不做,要做什么需要通过插件告诉

8.png

 <executions>
<!--                    <execution>-->
<!--                        <id>compile</id>-->
<!--                        <phase>compile</phase>-->
<!--                        <goals>-->
<!--                            <goal>check</goal>-->
<!--                        </goals>-->
<!--                    </execution>-->
    <execution>
        <id>verify</id>
        <phase>verify</phase>
        <goals>
            <goal>check</goal>
        </goals>
    </execution>
</executions>

建立数据库

  • LINKS_TO_BE_PROCESSED
    • Link
create Table LINKS_TO_BE_PROCESSED(
link varchar(1000)
);
  • LINKS_ALREADY_PROCESSED
    • Link
create Table LINKS_ALREADY_PROCESSED(
link varchar(10-0)
);
  • NEWS
    • id
    • Title
    • Content
    • URL
    • CREATED_AT
    • MODIFIED_AT
-- text类型表示不变的字符串,非常适 合存新闻,新闻一般就是只读
create table news (
  id bigint primary key auto_increment,
  title text,
  content text,
  url varchar(1000),
  created_at timestamp,
  modified_at timestamp
)

不想解决spotBugs找到的错误

  • google搜索spot SuppressFBWarning
  • 搜索SuppressFBWarnings maven,在报错的方法之前加 @SuppressFBWarnings("")

初始化数据库

  • 使用flayWay的工具,可以理解为数据库结构的版本控制工具
  • 配置pom,编写sql,运行mvn flyway:migrate
<plugin>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-maven-plugin</artifactId>
    <version>5.2.4</version>
    <configuration>
        <url>jdbc:h2:file:/Users/ories/Downloads/java-zhangbo/30项目实战 - 多线程网络爬虫与Elasticsearch新闻搜索引擎/project/xiedaimala-crawler/news</url>
        <user>root</user>
        <password>root</password>
    </configuration>
</plugin>

JDBC版,准备修改成orm映射

package com.github.hcsp.io;


import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.stream.Collectors;

public class Main {
    public static final String USER_NAME = "root";
    public static final String PASSWORD = "root";

    private static String getNextLink(Connection connection, String sql) throws SQLException {
        ResultSet resultSet = null;
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                return resultSet.getString(1);
            }
        } finally {
            if (resultSet != null) {
                resultSet.close();
            }
        }
        return null;
    }

    private static String getNextLinkThenDelete(Connection connection) throws SQLException {
        String link = getNextLink(connection, "select link from LINKS_TO_BE_PROCESSED LIMIT 1");
        if (link != null) {
            updateDatabase(connection, link, "DELETE FROM LINKS_TO_BE_PROCESSED where link = ?");
        }
        return link;
    }


    @SuppressFBWarnings("DMI_CONSTANT_DB_PASSWORD")
    public static void main(String[] args) throws IOException, SQLException {


        // 待处理的链接池
        // 从数据库加载即将处理的链接的代码
        Connection connection = DriverManager.getConnection("jdbc:h2:file:/Users/ories/Downloads/java-zhangbo/30项目实战 - 多线程网络爬虫与Elasticsearch新闻搜索引擎/project/xiedaimala-crawler/news", USER_NAME, PASSWORD);

        String link;

        // 从数据库中加载下一个链接,如果能加载到,则进行循环
        while ((link = getNextLinkThenDelete(connection)) != null) {
            // 询问数据库,当前链接是不是已经处理过来
            if (isLinkProcessed(connection, link)) {
                continue;
            }
            // 判断是否是需要处理的链接
            if (isInterestingLink(link)) {
                System.out.println(link);
                Document doc = httpGetAndParseHtml(link);
                parseUrlsFromPageAndStoreIntoDatabase(connection, doc);
                // 如果是新闻的详情页面的就储存它,否则什么都不做
                storeIntoDatabaseIfItIsNewPage(connection, doc, link);
                updateDatabase(connection, link, "INSERT INTO LINKS_ALREADY_PROCESSED (LINK) values (?)");
                // 将处理过的链接,加入处理过的链接池
            }
        }
    }

    private static void parseUrlsFromPageAndStoreIntoDatabase(Connection connection, Document doc) throws SQLException {
        for (Element aTag : doc.select("a")) {
            String href = aTag.attr("href");

            if (href.startsWith("//")) {
                href = "https:" + href;
            }

            if (!href.toLowerCase().startsWith("javascript")) {
                updateDatabase(connection, href, "INSERT INTO LINKS_TO_BE_PROCESSED (LINK) values (?)");
            }

        }
    }

    private static boolean isLinkProcessed(Connection connection, String link) throws SQLException {
        ResultSet resultSet = null;
        try (PreparedStatement statement = connection.prepareStatement("SELECT LINK from LINKS_ALREADY_PROCESSED where link = ?")) {
            statement.setString(1, link);
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                return true;
            }
        } finally {
            if (resultSet != null) {
                resultSet.close();
            }
        }
        return false;
    }

    private static void updateDatabase(Connection connection, String link, String sql) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setString(1, link);
            statement.executeUpdate();
        }
    }

    private static void storeIntoDatabaseIfItIsNewPage(Connection connection, Document doc, String link) throws SQLException {
        ArrayList<Element> articleTags = doc.select("article");
        if (!articleTags.isEmpty()) {
            for (Element articleTag : articleTags) {
                String title = articleTags.get(0).child(0).text();
                String content = articleTag.select("p").stream().map(Element::text).collect(Collectors.joining("\n"));

                System.out.println(title);
                try (PreparedStatement statement = connection.prepareStatement("insert into news (url, title, content, CREATED_AT, MODIFIED_AT) values ( ?,?,?,now(),now() )")) {
                    statement.setString(1, link);
                    statement.setString(2, title);
                    statement.setString(3, content);
                    statement.executeUpdate();
                }
            }
        }
    }

    // 这是我们感兴趣的,我们只处理新浪站内的链接
    private static Document httpGetAndParseHtml(String link) throws IOException {

        CloseableHttpClient httpclient = HttpClients.createDefault();

        if (link.startsWith("//")) {
            link = "https:" + link;
        }

        HttpGet httpGet = new HttpGet(link);
        httpGet.addHeader("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1");

        try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
            // 获取访问的响应头
            HttpEntity entity1 = response1.getEntity();
            String html = EntityUtils.toString(entity1);
            return Jsoup.parse(html);
        }
    }

    // 我们只关心news.sina的,我们要排除登录页面
    private static boolean isInterestingLink(String link) {
        return (isNewsPage(link) || isIndexPage(link)) && isNotLoginPage(link);
    }

    private static boolean isIndexPage(String link) {
        return "https://sina.cn".equals(link);
    }

    private static boolean isNewsPage(String link) {
        return link.contains("news.sina.cn");
    }

    private static boolean isNotLoginPage(String link) {
        return !link.contains("passport.sina.cn");
    }
}

java世界的orm框架

  • mybatis

重构把数据库的部分剥离出去

  • 改成三个文件Crawler.java,CrawlerDao.java,JdbcCrawlerDao.java,将数据库逻辑和爬虫逻辑分离
  • 好处是CrawlerDao.java是一个接口,如果之后要改数据库,只要改JdbcCrawlerDao.java中的数据库连接一行代码即可
  • Crawler.java
package com.github.hcsp.io;


import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.stream.Collectors;

public class Crawler {


    private CrawlerDao dao = new JdbcCrawlerDao();

    public void run() throws SQLException, IOException {
        // 待处理的链接池
        // 从数据库加载即将处理的链接的代码

        String link;

        // 从数据库中加载下一个链接,如果能加载到,则进行循环
        while ((link = dao.getNextLinkThenDelete()) != null) {
            // 询问数据库,当前链接是不是已经处理过来
            if (dao.isLinkProcessed(link)) {
                continue;
            }
            // 判断是否是需要处理的链接
            if (isInterestingLink(link)) {
                System.out.println(link);
                Document doc = httpGetAndParseHtml(link);
                parseUrlsFromPageAndStoreIntoDatabase(doc);
                // 如果是新闻的详情页面的就储存它,否则什么都不做
                storeIntoDatabaseIfItIsNewPage(doc, link);
                dao.updateDatabase(link, "INSERT INTO LINKS_ALREADY_PROCESSED (LINK) values (?)");
                // 将处理过的链接,加入处理过的链接池
            }
        }
    }


    @SuppressFBWarnings("DMI_CONSTANT_DB_PASSWORD")
    public static void main(String[] args) throws IOException, SQLException {
        new Crawler().run();
    }

    private void parseUrlsFromPageAndStoreIntoDatabase(Document doc) throws SQLException {
        for (Element aTag : doc.select("a")) {
            String href = aTag.attr("href");

            if (href.startsWith("//")) {
                href = "https:" + href;
            }

            if (!href.toLowerCase().startsWith("javascript")) {
                dao.updateDatabase(href, "INSERT INTO LINKS_TO_BE_PROCESSED (LINK) values (?)");
            }

        }
    }




    private void storeIntoDatabaseIfItIsNewPage(Document doc, String link) throws SQLException {
        ArrayList<Element> articleTags = doc.select("article");
        if (!articleTags.isEmpty()) {
            for (Element articleTag : articleTags) {
                String title = articleTags.get(0).child(0).text();
                String content = articleTag.select("p").stream().map(Element::text).collect(Collectors.joining("\n"));
                dao.insertNewsIntoDatabase(link, title, content);
            }
        }
    }

    // 这是我们感兴趣的,我们只处理新浪站内的链接
    private static Document httpGetAndParseHtml(String link) throws IOException {

        CloseableHttpClient httpclient = HttpClients.createDefault();

        if (link.startsWith("//")) {
            link = "https:" + link;
        }

        HttpGet httpGet = new HttpGet(link);
        httpGet.addHeader("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1");

        try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
            // 获取访问的响应头
            HttpEntity entity1 = response1.getEntity();
            String html = EntityUtils.toString(entity1);
            return Jsoup.parse(html);
        }
    }

    // 我们只关心news.sina的,我们要排除登录页面
    private static boolean isInterestingLink(String link) {
        return (isNewsPage(link) || isIndexPage(link)) && isNotLoginPage(link);
    }

    private static boolean isIndexPage(String link) {
        return "https://sina.cn".equals(link);
    }

    private static boolean isNewsPage(String link) {
        return link.contains("news.sina.cn");
    }

    private static boolean isNotLoginPage(String link) {
        return !link.contains("passport.sina.cn");
    }
}
  • CrawlerDao.java
package com.github.hcsp.io;

import java.sql.SQLException;

public interface CrawlerDao {
    String getNextLink(String sql) throws SQLException;

    String getNextLinkThenDelete() throws SQLException;

    void updateDatabase(String link, String sql) throws SQLException;

    void insertNewsIntoDatabase(String url, String title, String content) throws SQLException;

    boolean isLinkProcessed(String link) throws SQLException;
}
  • JdbcCrawlerDao.java
package com.github.hcsp.io;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JdbcCrawlerDao implements CrawlerDao{
    public static final String USER_NAME = "root";
    public static final String PASSWORD = "root";

    private final Connection connection;

    public JdbcCrawlerDao() {
        try {
            this.connection = DriverManager.getConnection("jdbc:h2:file:/Users/ories/Downloads/java-zhangbo/30项目实战 - 多线程网络爬虫与Elasticsearch新闻搜索引擎/project/xiedaimala-crawler/news", USER_NAME, PASSWORD);
        } catch (SQLException e) {
            throw new RuntimeException();
        }
    }


    public String getNextLink(String sql) throws SQLException {
        ResultSet resultSet = null;
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                return resultSet.getString(1);
            }
        } finally {
            if (resultSet != null) {
                resultSet.close();
            }
        }
        return null;
    }

    public String getNextLinkThenDelete() throws SQLException {
        String link = getNextLink("select link from LINKS_TO_BE_PROCESSED LIMIT 1");
        if (link != null) {
            updateDatabase(link, "DELETE FROM LINKS_TO_BE_PROCESSED where link = ?");
        }
        return link;
    }

    public void updateDatabase(String link, String sql) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setString(1, link);
            statement.executeUpdate();
        }
    }

    public void insertNewsIntoDatabase(String url, String title, String content) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("insert into news (url, title, content, CREATED_AT, MODIFIED_AT) values ( ?,?,?,now(),now() )")) {
            statement.setString(1, url);
            statement.setString(2, title);
            statement.setString(3, content);
            statement.executeUpdate();
        }
    }

    public boolean isLinkProcessed(String link) throws SQLException {
        ResultSet resultSet = null;
        try (PreparedStatement statement = connection.prepareStatement("SELECT LINK from LINKS_ALREADY_PROCESSED where link = ?")) {
            statement.setString(1, link);
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                return true;
            }
        } finally {
           if (resultSet != null) {
                resultSet.close();
            }
        }
        return false;
    }
}
  • MyBatisCrawlerDao.java中的骨架搭建
package com.github.hcsp.io;

import java.sql.SQLException;

public class MyBatisCrawlerDao implements CrawlerDao{

    @Override
    public String getNextLink(String sql) throws SQLException {
        return null;
    }

    @Override
    public String getNextLinkThenDelete() throws SQLException {
        return null;
    }

    @Override
    public void updateDatabase(String link, String sql) throws SQLException {

    }

    @Override
    public void insertNewsIntoDatabase(String url, String title, String content) throws SQLException {

    }

    @Override
    public boolean isLinkProcessed(String link) throws SQLException {
        return false;
    }
}

Mybatis.xml的比较

  • 常量要用单引号
  • 传递进来的param找属性,如果传递进来的是map,那么参数命就是key
  • 找的约定就是javabean的约定,如果是java对象,就调用他的getter方法,hashmap就是调用对应的key对应的value
  • myMap.xml
<?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>

切换数据库到mysql

docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:5.7.27
docker rm -f mysql
  • 搜索jdbc mysql localhost
jdbc:mysql://localhost:3306/news
  • mvn flyway:migrate
  • 报错:原因是当前环境可能没有mysql的jdbc驱动
  • 搜索 com.mysql.cj.jdbc.Driver maven
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>
  • 使用mysql-connector-java
  • 使用小松鼠create database news
  • 如果flyway失败需要用,mvn flyway:clean
  • myBatis也要修改驱动,改两个地方
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/news"/>

mysql是否区分大小写是可以配置的

  • 这种情况应该使用统一的大小写

修改msql的news数据库字符集

 ALTER DATABASE news CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

修改jdbc链接成utf8

<url>jdbc:mysql://localhost:3306/news?characterEncoding=UTF-8</url>

mysql容器持久化

docker rm -f mysql
mkdir mysql-data
docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p3306:3306 -p 3306:3306 -d mysql:5.7.27

# 这样可以
docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p3306:3306 -v "`pwd`/mysql-data":/var/lib/mysql -d mysql:5.7.27

# 这种可以
docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p3306:3306 -v "/Users/ories/Downloads/java-zhangbo/30项目实战 - 多线程网络爬虫与Elasticsearch新闻搜索引擎/project/xiedaimala-crawler/mysql-data":/var/lib/mysql -d mysql:5.7.27

  • 重新创建数据库
create database news;
ALTER DATABASE news CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
mvn flyway:clean && mvn flyway:migrate
  • docker命令中 d是demon模式可以在后台运行

mybatis中的设置

  • 解决camelCase的问题
  • preparedStatment中有addBatch,和execute batch批处理的方式
  • myBatis本身也有批处理的方式,ExecutorType.BATCH
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
}
  • 防止卡死,两千条存一下
if (count % 2000 == 0) {
    session.flushStatements();
}

查找加速

  • 平衡二叉树,问题,不平衡

9.png

  • mysql数据库中用的是B+数,一般还有B树和B*树

B树和B+树的好处

10.png

  • 第一,多叉树,每个节点包括多个记录,树的高度非常低,使得磁盘io非常有利,只需要很少的查找次数
  • 节点所有的包含范围,进行范围查找between这种时,可以迅速的读出头节点和尾节点
    • 磁盘读取的时候有预读,读取一个点的时候会把附近扇区的数据也进行缓存,相邻的记录放一起的 数据结构放在一起就会非常有优势
  • 平衡二叉树树的高度很高,可能要找很多次,不利于磁盘的io。

11.png

  • b+树经常用于文件,磁盘存储

索引的概念

  • 默认以id主键作为B+树维护
  • 字符串做索引效率比较低
  • 每一个数据指向主索引

联合索引

  • 两个记录联合比较,(a,b)索引,a,(a,b)索引
  • a,b,c->,a,(a,b),(a,b,c)
  • 最左前缀匹配原则
  • 碰到范围查找,前面的使用联合索引,后面的就用不到索引
  • 要根据业务需要灵活的建索引

13.png

索引的好处

  • 提升查找速度,千万级别的数据也可以毫秒级的查找

建立索引的原则

  • 尽量选择不重复的列作为索引(比如id)
  • 可以不新建索引,如果已经有a索引,就不必要再建立a,b联合索引,应该把a扩展成a,b索引
  • count(*)需要扫描全表,所以会比较慢

索引的优化

  • 索引对范围查询的提升是很低的
select * from NEWS where id = 1234
  • 利用函数,mysql把小时,分,秒给去掉,搜索mysql function remove timestamp to date
select id, title, created_at, date(created_at) from NEWS where id = 1234;
update NEWS set created_at = date(created_at), modified_at = date(modified_at);
select * from NEWS where created_at = '2021-08-29'
-- 小松鼠,去掉100的limit
  • mysql建索引的语句
CREATE INDEX created_at_index ON NEWS (created_at);
-- 查看索引
show index from NEWS;
-- 再去执行,只花了1秒都不到
select * from NEWS where created_at = '2021-08-29'

如何分析如何加索引

  • explain,网上搜索mysql性能优化
explain select * from NEWS where created_at = '2021-08-29'
  • select_type,查询类型
  • table,查询哪一张表
  • type,这个是最重要的查询类型,all是最坏的情况
explain select * from NEWS where modified_at = '2021-08-29'
  • 创建created_at和modified_at的联合索引
CREATE INDEX created_at_modified_at_index ON NEWS (created_at, modified_at);
-- 查看索引
show index from NEWS;
-- 联合索引 created_at + modified_at
select * from NEWS where created_at = '2019-01-01' and modified_at < '2019-02-01'
-- 换成这样就无法匹配索引
select * from NEWS where created_at > '2019-01-01' and modified_at = '2019-02-01'

  • 通过explain去解释,=没有可匹配的,由于最左匹配原则,type就为range,表示左边created_at可以用索引,但是右边modified_at还是范围查询
explain select * from NEWS where created_at > '2019-01-01' and modified_at = '2019-02-01' 
explain select * from NEWS where created_at = '2019-01-01' and modified_at < '2019-02-01'
-- 删掉索引
drop index created_at_index on NEWS
  • 索引不是越多越好,而是越符合自己的业务需求越好
  • 需要根据自己的实际情况建几个索引,以及怎么建索引,根据业务的调用频率,要求,索引是占用空间的

字符串搜索

select * from NEWS where id = 1234;
select * from NEWS where content like '%野蛮行径%';
  • mysql的长处在于非文本的数据的搜索,比如日期,数值等等
  • 检索文本中的一两个字符串,mysql就会力不从心
  • 碰到这种文本的搜索就用elasticsearch

elasticsearch的原理

  • 倒排索引
  • 传统的B树数据结构不适合文本搜索

14.png

  • 倒排索引就是每一个字去做映射

安装elasticsearch

  • docker ps
  • 搜索elasticsearch docker
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
docker ps
  • 访问localhost:9200
  • 书的名字:elasticsearch: the definitive Guide

elasticsearch搜索引擎和关系型数据库的区别

15.png

灌数据

  • 搜索elasticsearch java api
  • 搜索elasticsearch high level rest client maven
// 查询数量
http://localhost:9200/_count?pretty
// 全部搜索
http://localhost:9200/_search
// 关键字搜索
http://localhost:9200/_search?q=title:外交部
  • elasticsearch批处理操作,bulk
  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);

//                    IndexResponse response = client.index(request, RequestOptions.DEFAULT);
//                    System.out.println(
//                            response.status().getStatus()
//                    );
                }
  • 搜索接口
// 全部搜索
http://localhost:9200/news/_search
// 关键字搜索
http://localhost:9200/news/_search?q=title:外交部
  • 搜索elastic java client search
  • 查看分片健康度http://localhost:9200/_cluster/health,采用集群的原因,第一个原因,数据的备份, 一个节点挂了也不至于不可用,第二个原因,希望能够水平扩展,承担并发的压力。
  • 在分布式领域中,能加机器解决的问题都不是问题,