从零开始做一个项目的原则
- 把每个项目都当作人生最好的一个项目来精雕细琢
- 积累自己的reputation声誉
- 一丝不苟地写好文档
- 代码质量++
- 你的认证是肯定能够获得回报的
- 使用标准化,业界公认的模式和流程
- (几乎)没有本地以来,使用者能毫无障碍地运行
- 小步快跑
- 成就感
- 越小的变更越容易debug
项目的原则
- 【强制】使用GitHub+主干/分支模型进行开发
- 禁止直接push master
- 所有的变更通过pr进行
- 【强制】自动化代码质量检查+测试
- Checkstyle/spotBugs
- 最基本的自动化测试覆盖
- 一切工作自动化
- 规范化提交流程
初始化项目与项目设计流程
- GitHub-new
- 建立新项目
- mvn archetype
- IDEA - new
- 直接从别人那儿抄一个
- .gitignore
- README
- 配置基本的代码质量检查插件
- 越早代价越低
改一下复制过来的一些信息
- groupId,artifactId
项目的设计流程
- 自顶向下
- 多人协作
- 模块化
- 各模块间指责明确,界限清晰
- 基本的文档
- 基本的接口
- 小步提交
- 大的变更难以review
- 大的变更冲突更加棘手
- 自下向上
- 先实现功能

- 在实现的过程中不停地抽取共用部分
- 每当你写出很长很啰嗦的代码的时候,就要重构了
- DRY: 每当你复制/粘贴的时候,就要重构了
- 通过重构实现模块化,接口话
项目的演进:可扩展性
- 模块化的好处,方便替换
- 单线程 -> 多线程
- console -> H2 database
- H2 database -> Elasticsearch
项目的演进:正确性
- 如何保证改动代码不会破坏原先的功能
- 通过测试
如何养成好的代码习惯
- 很丑的方式实现
- 不要妥协
代码回滚几个版本
git reset ~1,回一个版本- 不小心提交并且push了的情况
- 在主干上就老老实实的把多提交的文件删除
- 在分枝上就一样reset,然后force push
专业的commit 怎么写
- 先总结一行,然后具体描述
项目目标
- 爬取新浪新闻页,做一个真正的爬虫
- 使用数据库储存并进行数据分析
- 随着数据量的增长,迁移到ES
- 做一个简单的"新闻搜索引擎"
确定算法
- 为什么互联网被称为网,爬虫被称为爬虫
- 从一个节点出发,遍历所有的节点

- 算法:广度优先算法的一个变体
- 如何扩展?
- 慢慢地把烂代码,啰嗦的代码重构掉
- 加入我想要未来换数据库/上Elasticsearch
- 爬虫的通用化
- 广度优先算法建议学习手写一下,实现遍历一颗树,能学习到队列的数据结构,JDK的队列的实现
代码抽取短小化的好处
-
- 你是个人类,大脑更容易处理短小的方法
-
- 越短的代码越容易复用
-
- 对于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的三种情况

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

- 第二个选项,squash挤压,基本上等价于
git merge --squash,好处是一堆提交压扁成一个提交, - 第三个选项,等价于rebase,
另一个代码检查工具,spot-bugs的使用
- 如何阅读和使用官方文档?
- Maven的生命周期
- Maven的插件与目标
- 目标与生命周期阶段的绑定
- 运行
mvn spotbugs:check - maven是有一套生命周期的,有三种,最常用的就是默认的,要执行什么就从头执行到目标声明周期
maven的生命周期
- 默认什么都不做,要做什么需要通过插件告诉

<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();
}
查找加速
- 平衡二叉树,问题,不平衡

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

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

- b+树经常用于文件,磁盘存储
索引的概念
- 默认以id主键作为B+树维护
- 字符串做索引效率比较低
- 每一个数据指向主索引
联合索引
- 两个记录联合比较,(a,b)索引,a,(a,b)索引
- a,b,c->,a,(a,b),(a,b,c)
- 最左前缀匹配原则
- 碰到范围查找,前面的使用联合索引,后面的就用不到索引
- 要根据业务需要灵活的建索引

索引的好处
- 提升查找速度,千万级别的数据也可以毫秒级的查找
建立索引的原则
- 尽量选择不重复的列作为索引(比如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树数据结构不适合文本搜索

- 倒排索引就是每一个字去做映射
安装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搜索引擎和关系型数据库的区别

灌数据
- 搜索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,采用集群的原因,第一个原因,数据的备份, 一个节点挂了也不至于不可用,第二个原因,希望能够水平扩展,承担并发的压力。 - 在分布式领域中,能加机器解决的问题都不是问题,