目录
从零开始
- GitHub - new repository
- 建立新项目
mvn archetype:generate
快速生成项目骨架(不推荐)IDEA - New Project
(等价于上面)- 直接从别人那儿抄一个
.gitignore
忽略提交的README.md
项目说明,显示在项目地址首页- 配置基本的代码质量检查插件
- 越早代价越低
项目的演进:正确性
如何保证改动代码不会破坏原先的功能?
- 改完代码开始测,人肉测试,关键的功能测试,测试完提交;有问题回滚,排查问题
- 自动化测试
- push代码的时候都会触发circleci测试
- 根据业务
好的代码习惯
- 不要妥协
- 不要自己造轮子
- 不要做一个只会从网上抄代码的人
- 如何阅读和使用官方文档?
测试的原则
- 测试包放在test目录下
- 每一个测试是一个类,负责一个很小的功能模块,这个类中的每个方法用于测试一个关键的功能点。
circleci
-
git登录
-
Projects - 对想要CI的项目点Set Up Project
- 如果CI配置文件
.circleci/config.xml
,这里需要多一步创建配置文件
- 如果CI配置文件
-
start building
每当push新的代码都会触发CI
git创建new-feature分支
git checkout -b new-feature
确定算法
为什么互联网被称为网,爬虫被称为爬虫
- 从一个节点出发,遍历所有的节点
算法:广度优先算法的一个变体
如何拓展?
- 假如未来要换数据库/上Elasticsearch
- 爬虫的通用化
SpotBugs插件引入
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>3.1.12.2</version>
<dependencies>
<!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs</artifactId>
<version>4.0.0-beta3</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>spotbugs</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<phase>verify</phase>
插件绑定到verify阶段
<goal>check</goal>
相当于spotbugs:check
-
Maven的生命周期
- 默认的生命周期,比如执行test,就会把test前面所有阶段从头都执行一遍
- 先声明的先执行
-
Maven的插件与目标
- 目标与生命周期阶段的绑定
数据的持久化
数据库的访问方法
如何告诉用户应该如何建表
- 自动化数据库的创建?迁移
使用Flyway数据库自动化迁移工具
mvn flyway:clean && mvn flyway:migrate
ORM初步
对象关系映射(英语:Object Relational Mapping,简称ORM
现在使用的数据库是关系型数据库
怎么样实现Relational到Object这样的绑定呢?
-
把NEWS表变成对象,news表里的列和对象的属性一一对应
-
News.java 并给上Getter and Setter方法
-
时间戳用
Instant
从Java1.8开始
public class News {
private Integer id;
private String url;
private String content;
private String title;
private Instant createdAt;
private Instant modifiedAt;
}
- NEWS表
create table NEWS(
id bigint primary key auto_increment,
title text,
content text,
url varchar(1000),
created_at timestamp default now(),
modified_at timestamp default now()
) DEFAULT CHARSET=utf8mb4;
把和数据库相关的操作剥离成DAO
数据访问对象(data access object,DAO)是为某种类型的数据库或其他持久性机制提供一个抽象接口的对象。
引入MyBatis简化数据库操作
docker容器启数据库
-
非持久化 重启就没有了
docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:5.7.29
-
数据库持久化映射
-v
参数,映射到磁盘docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -v E:/git/sina-crawler/mysql-data:/var/lib/mysql -d mysql:5.7.29
-
移除数据库
docker rm -f mysql
db/mybatis/config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/news?characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="db/mybatis/MyMapper.xml"/>
<mapper resource="db/mybatis/MockMapper.xml"/>
</mappers>
</configuration>
- 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。
session.commit();
提交 如果成功则提交到数据库
session.rollback();
回滚 回滚到没做操作之前的状态
如果成功则提交到数据库有异常则回滚到没做操作之前的状态
MySQL
MySQL 插入的数据乱码
- 把news数据库编程UTF-8
ALTER DATABASE news CHARACTER SET =utf8mb4 COLLATE = utf8mb4_unicode_ci;
- JDBC的链接也改成UTF-8
<configuration>
<url>jdbc:mysql://localhost:3306/news?characterEncoding=UTF-8</url>
</configuration>
- 创建NEWS表的时候设置默认字符集
create table NEWS(
id bigint primary key auto_increment,
title text,
content text,
url varchar(1000),
created_at timestamp default now(),
modified_at timestamp default now()
) DEFAULT CHARSET=utf8mb4;
MySQL索引
B+Tree索引
-
是大多数 MySQL 存储引擎的默认索引类型。
-
默认情况下,在数据之外MySQL会维护一个B+树,一般来说是id,id是主键,主键是主索引
-
假如再建了一个name索引,会维护一个新的B+树,每一个记录都指向它对应的id,根据一个name查找的话,首先执行name索引,找到id之后,回去查主索引拿到真正的
MySQL建索引(建议看官方文档)
mysql官方创建所有官方文档
建立索引
CREATE INDEX created_at_index
ON NEWS (created_at)
查看索引
show index from NEWS
Explain优化查询检测
解释当前语句以什么样的方式执行
explain select * from NEWS where created_at = '2019-02-18'
分析Explain
联合索引创建
例子1:
where a = xx and b = xx
创建(a, b)
联合索引,它会建两个索引(a)
,(a, b)
同理(a, b, c)
,它会建三个索引(a)
,(a, b)
,(a, b, c)
例子2:
创建(a, b, c, d)
联合索引
selete x from xx where a=1 and b=2 and c>3 and d=4
根据这个sql,查询优化器发现 and c>3
这里有个范围查找,意味着索引用处不大,找到这个索引把它后面的数据捞出来
查询优化器每当看到一个范围索引的时候,它就到此为止,然后把前面的a=1 and b=2
试图用联合索引去查,但是后面还有个d=4
是用不到索引
索引优化的过程
a=1 and b=2 and d=4 and c>3
如果我们有(a,b,d)
就可以用联合索引了,不幸的是建的是(a,b,c,d)
的索引,这个过程就是sql索引优化的过程
根据业务中写出的具体的sql,来决定具体建什么索引
(a,b,d,c)
这三个and的顺序都是可以换的,(b,d,a,c)
、(d,b,a,c)
查询优化器会自动的匹配到最优的索引上去
为时间戳建立索引
mysql remove timestamp from date 去掉时间戳的时分秒
update NEWS set created_at=date(created_at), modified_at=date(modified_at)
修改原有的所有,不要新加索引
已经有了一个(created_at),应该修改(created_at)为联合索引,因为联合索引会创建(created_at)和(created_at, modified_at)
CREATE INDEX created_at_modified_at_index
ON NEWS (created_at, modified_at)
最左前缀匹配
created_at = '2019-02-18'
可以使用联合索引 type=ref
explain select * from NEWS where created_at = '2019-02-18' and modified_at < '2019-01-18'
created_at > '2019-02-18'
没有可以用的索引 type=ALL
explain select * from NEWS where created_at > '2019-02-18' and modified_at = '2019-01-18'
Elasticsearch原理与数据索引实战
MySQL 长处在非文本数据的索引,检索本文中的一两个字符串,MySQL就显得力不从心
select * from NEWS where content like '%床前明月光%'
用这个语句查询慢
为什么传统的关系型数据库按照id这个列搜索很快?
- id一般是主键,建立了一个主索引,内部建了一个B+树,是排好序的,可以非常快的查找到
传统的B+树的结构不适于文本检索
- 传统的B+树索引本质上是比较相等操作,在比较
的时候可以快速的找到id=xx
- 文本本质上是一个contains操作,对文本是不是包含这个关键字
倒排索引
什么是倒排索引
作者:武培轩
链接:https://www.zhihu.com/question/23202010/answer/1054033556
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
倒排索引(Inverted Index) 也常被称为反向索引,是搜索引擎中非常重要的数据结构,为什么说它重要呢,我们首先拿一本书《重构 改善既有代码的设计》举个例子:
如果一本书没有目录的话,理论上也是可以读的,只是合上书下次再次阅读的时候,就有些耗费时间了。
通过给一本书加目录页,可以快速了解这本书的大致内容分布以及每个章节的页码数,这样在查询内容的时候效率就会非常高了,所以书的目录就是书本内容的简单索引。
想象一下你要搜索 case语句
这个关键词在这本书的页码,你应该怎么办呢?有些技术类的书籍会在最后提供索引页,这本书的索引页如下:
只需要从索引页中查找 case语句
,就可以查找到关键词在书本中的页码位置了。
看完这个例子,让我们来把图书和搜索引擎做个简单的类比:
图书当中的目录页就相当正向索引(Forward Index),索引页就相当于倒排索引的简单实现,在搜索引擎中,正向索引指的是文档 ID 到文档内容和单词的关联,倒排索引就是单词到文档 ID 的关系。
docker 启动Elasticsearch
拉取镜像
docker pull elasticsearch:7.6.0
创建esdata目录
mkdir esdata
持久化 win用绝对路径
docker run -d -v E:/git/sina-crawler/esdata:/usr/share/elasticsearch/data --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.6.0
-d 后台运行
--name 容器的名字
-p docker容器的端口和本地的端口映射
访问elasticsearch 浏览器http://localhost:9200/
官方文档:elasticsearch the definitive guide
https://www.elastic.co/guide/en/elasticsearch/guide/master/running-elasticsearch.html
从MySQL从读取数据再模拟数据写入elasticsearch
引入elasticsearch-rest-high-level-client
包
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.0</version>
</dependency>
代码
public class ElasticsearchDataGenerator {
public static void main(String[] args) throws IOException {
SqlSessionFactory sqlSessionFactory;
try {
String resource = "db/mybatis/config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
List<News> newsFromMySQL = getNewsFromMySQL(sqlSessionFactory);
for (int i = 0; i < 10; i++) {
new Thread(() -> writeSingleThread(newsFromMySQL)).start();
}
}
private static void writeSingleThread(List<News> newsFromMySQL) {
try (RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")))) {
// 单线程写入2000*1000 = 200_0000数据
for (int i = 0; i < 1000; i++) {
BulkRequest bulkRequest = new BulkRequest();
for (News news : newsFromMySQL) {
IndexRequest request = new IndexRequest("news");
Map<String, Object> data = new HashMap<>();
data.put("contnet", 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);
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println("Current thread : " + Thread.currentThread().getName() + " finishes " + i + ":" + bulkResponse.status().getStatus());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static List<News> getNewsFromMySQL(SqlSessionFactory sqlSessionFactory) {
try (SqlSession session = sqlSessionFactory.openSession()) {
return session.selectList("com.github.hcsp.MockMapper.selectNews");
}
}
}
RestClient操作海量数据,用完记得关闭,把RestClient放到try-with-resources里
搜索使用
http://localhost:9200/news/_search
http://localhost:9200/news/_search?q=title:床前明月光
-
新闻搜索引擎不是精确匹配,搜索的关键字会被分词
床
前
... -
根据分词进行倒排索引,预期得到的结果是相关的数据能被搜出来,不是非常匹配的放在后面
集群简介
官方文档:
https://www.elastic.co/guide/en/elasticsearch/guide/master/distributed-cluster.html
集群健康信息
http://localhost:9200/_cluster/health
{
"cluster_name": "elasticsearch",
"status": "green",
"timed_out": false,
"number_of_nodes": 1,
"number_of_data_nodes": 1,
"active_primary_shards": 0,
"active_shards": 0,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0
}
status
字段是我们最感兴趣的
-
green
- 所有的主分片和备用分片都是可用的
-
yellow
- 所有的主分片可用,但是没有备份分片存在
- 这是一种警告状态,警告我们现在的集群很危险,万一有个节点挂了,有一部分数据变得不可用
-
red
- 并非所有的主分片都是可用状态
为什么使用集群
-
数据的备份
- 避免单点故障
- 避免一个节点挂掉后整个集群数据就不可用了
-
数据的水平扩展
- 假设成千上万人访问节点,节点的压力很大,假如说能把这些数据能平均的分配到更多的节点上,把并发访问请求的压力分散到了多个机器上
多线程 先让结果正确然后再优化跑得更快
public synchronized String getNextLinkThenDelete() throws SQLException {
try (SqlSession session = sqlSessionFactory.openSession(true)) {
String url = session.selectOne("com.github.hcsp.MyMapper.selectNextAvailabaleLink");
if (url != null) {
session.delete("com.github.hcsp.MyMapper.deleteLink", url);
}
return url;
}
}
多线程中经典的先取值再根据判断结果做下一件事情,在多线程环境中是非常危险的,两个线程可能同时在执行这段代码
Github上的merge
-
Create a merge commit
创建一个合并提交
将所有提交合并到基本分支 -
Squash and merge
压缩和合并
把所有的提交压缩成一个提交 -
Rebase and merge
重新建立基础并合并
会把分支的Commits合并平移到Code Commits
Windows下git bash的一些坑
-
解决 .bashrc 文件每次打开终端都需要 source 的问题
在~/.bash_profile
文件中输入source ~/.bashrc
保存退出后,重新打开终端 -
解决git bash乱码问题
-
打开GitBash(git-bash.exe)后,对窗口右键->Options->Text->Locale改为zh_CN,Character set改为UTF-8;
-
直接输入
vi ~/.bashrc PATH=$PATH:$HOME/bin export PATH unset USERNAME export LANG=zh_CN.UTF-8 export LESSCHARSET=utf-8
git 命令行不小心提交
- 不小心提交了不想提交的代码
git reset Head~1
把当前分支代码向后回滚1个提交- idea的9:Version Control 鼠标右键到想回滚提交 Reset Current Branch to Here然后Reset
- 不小心commit还push了
- 如果是在自己的分支上,那就用
不小心提交了不想提交的代码
的方式操作,并且force pushgit push -f
- 如果在主干上,把多提交的文件删掉。
- 如果是在自己的分支上,那就用
git commit --amend
- 提交修正
- 合并到上一个
commit
,不会产生新的commit
- 撤回提交的过程,别人能看到吗?
- 只要没有push,都是只有自己能看到的
how to write a commit message
- 将标题行限制为50个字符,写重要的,第一个字母大写,标题不以标点符号结束
- 正文的每一行不超过72个字符,详细描述提交的变更在做什么
- 用空行将主体与主体分开
- 指定提交的类型。建议使用一组一致的词语来描述您的更改,这可能会更有益。示例:Bugfix、Update、Refactor、Bump 等