在這個教學裏,我們會使用到以下關鍵技術棧:
- Spring Boot
- Elasticsearch
- Logstash
- MySQL
Spring Boot 主要作爲我們對外的 API 接口,Elasticsearch 則是作爲我們的搜索引擎。假設我們本來已經在使用 MySQL 作爲我們的數據庫,這篇教程會教你如何利用 Logstash 實現 MySQL 與 Elasticsearch 的實時同步。
Spring Boot 設置
添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
<version>6.3.1</version>
</dependency>
我們需要通過 jest (Java Rest Client) 向 elasticsearch 9200 端口發送 HTTP Rest 請求來進行查詢。
application.properties 設置
spring.elasticsearch.jest.uris=http://localhost:9200
spring.elasticsearch.jest.read-timeout=5000
EsStudyCard.java
@Document(indexName = "cards", type = "card")
public class EsStudyCard {
@Id
private int id;
private String title;
private String description;
// setters and getters
}
這裏需要注意的是,我們不能對同一個實體類 (entity class) 同時使用註解 @Entity 與 @Document,因爲 @Entity 表示由 JPA 掌管這個實體類,而 @Document 表示由 Elasticsearch 掌管這個實體類,兩個一起使用的話會產生衝突並導致異常。因此我們需要創建另一個實體類專門給 Elasticsearch 使用。
如果是使用 Docker 容器或外部 Elasticsearch 服務器,請將 localhost 改成相應的主機名或主機地址。
EsStudyCardService.java
@Service
public class EsStudyCardService {
@Autowired
private JestClient jestClient;
public List<EsStudyCard> search(String content) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(
QueryBuilders
.boolQuery()
.should(QueryBuilders.matchQuery("title", content).fuzziness("10").operator(Operator.OR))
.should(QueryBuilders.matchQuery("description", content).fuzziness("10").operator(Operator.OR))
.minimumShouldMatch(1)
);
Search search = new Search
.Builder(searchSourceBuilder.toString())
.addIndex("cards")
.addType("card")
.build();
try {
JestResult result = jestClient.execute(search);
return result.getSourceAsObjectList(EsStudyCard.class);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
這是模糊搜索的一個寫法。首先我們創建一個 SearchSourceBuilder 的實例,接着通過調用 query() 方法並傳遞一個 QueryBuilder 實例 (通過 QueryBuilders 接口創建)。在這裏我們的 QueryBuilder 會查詢任何滿足模糊搜索條件的數據。這裏我們使用 should() 方法並加上 minimumShouldMatch(1) 方法來表示滿足 title or description fields 的邏輯。
在 should() 方法裏傳遞 QueryBuilder,在這裏我們使用 fuziness() 方法表示我們想要使用模糊搜索,傳遞的參數表示模糊搜索詞的 edit distance。接着我們調用 operator() 方法並傳遞 Operator.OR 表示如果我們這裏的 content 包含了多個詞,則會對應每個詞進行模糊搜索並用 OR 的邏輯關係將他們串聯起來。舉例,如果我們的搜索詞是 “red apple”,則搜索條件爲滿足 “red” 模糊搜索 OR 滿足 “apple” 模糊搜索。
接着我們通過 Search 來指定 index 與 type。然後使用 JestClient 發送查詢請求。在這裏我們只需要確保我們加入了 JestClient 的依賴 (如之前所示),然後使用 @Autowired 註解,Spring Boot 就會幫我們注入 JestClient 的實例了。
EsStudyCardController.java
@PostMapping(value = "/esstudycard/search")
public ResponseEntity<Object> search(@RequestBody JSONObject payload) {
String content = (String) payload.get("content");
List<EsStudyCard> studyCards = esStudyCardService.search(content);
JSONObject jsonObject = new JSONObject();
jsonObject.put("data", studyCards);
return ResponseEntity.ok().body(jsonObject);
}
接着在 controller 類裏面只需要簡單調用 EsStudyCardService 的 search 方法就可以了。
Logstash 配置文件
以 MySQL 爲例,我們需要下載相應的 jdbc 驅動,並放在指定的目錄下。
接着我們創建一個如下的 Logstash 配置文件。這裏以 Docker 容器爲例,通過 volume 的形式將該 Logstash 配置文件映射到 /usr/share/logstash/pipeline (CentOS 系統). Logstash 會自動檢測 pipeline 文件下的配置文件。
input {
jdbc {
jdbc_connection_string => "jdbc:mysql://localhost:3306/vanpanda"
jdbc_user => "root"
jdbc_password => "root"
jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
statement => "SELECT * FROM study_cards where modified_timestamp > :sql_last_value order by modified_timestamp;"
use_column_value => true
tracking_column_type => "timestamp"
tracking_column => "modified_timestamp"
schedule => "* * * * *"
jdbc_default_timezone => "America/Los_Angeles"
sequel_opts => {
fractional_seconds => true
}
}
}
output {
elasticsearch {
index => "cards"
document_type => "card"
document_id => "%{id}"
hosts => ["localhost:9200"]
}
}
該配置文件主要是告訴 Logstash 數據源的位置。這裏我們的數據源就是我們的 MySQL 數據庫。根據該配置文件,Logstash 每分鐘會發送我們指定的查詢語句(query),接着將返回的數據插入到 Elasticsearch 中,實現近似實時同步。
請將 localhost 改成相應的主機名或主機地址。
查看 Elasticsearch 的狀態或數據
我們可以通過 web api 的方式來查看 Elasticsearch 的狀態,包括它的index。
http://localhost:9200/{index_name}
http://localhost:9200/_cat/indices?v
http://localhost:9200/{index_name}/_search?pretty
將上述 url 直接複製到瀏覽器即可通過 Elasticsearch 9200 端口的 Http Rest api 進行查找。
讓 Logstash 持續運行,並檢查數據更新
當 Logstash 完成第一次任務之後會自動 shut down。我們需要 Logstash 持續在 Docker 的容器中運行並每隔一段時間就執行一次任務來確保 Elasticsearch 與 MySQL 可以達到幾乎實時同步的效果。
在 Logstash 的配置文件裏,在 input jdbc 裏配置 schedule 設定,否則 Logstash 會認爲這只是一次性任務,並在第一次執行之後就會自動關閉。
input {
jdbc {
...
schedule => "* * * * *" #加入這個,表示每分鐘更新
}
}
例子中的語法代表每分鐘執行一次任務。我們可以查看語法文檔自定義時間。
確保 Logstash 不會重複更新舊數據
如果我們沒有告訴 Logstash 如何判斷數據的新舊,Logstash 每次執行任務時,便會把舊數據刪除,再重新插入一次,導致資源浪費。
對 Logstash 做如下配置:
input {
jdbc {
...
# 需要利用 Modified Date / Timestamp 來判斷數據新舊
statement => "SELECT" * FROM testtable where Date > :sql_last_value order by Date"
# 告訴 Logstash 使用 column value for
use_column_value => true :sql_last_value
# 指定 column Date 作爲 :sql_last_value 的值
tracking_column => Date
# 如果 column type 是 timestamp,則一定要配置這項。默認爲 numeric
tracking_column_type => timestamp
}
參考文檔:Migrating MySql Data Into Elasticsearch Using Logstash
錯誤信息與解決辦法
Unable to find driver class via URLClassLoader in given driver jars: com.mysql.jdbc.Driver and com.mysql.jdbc.Driver
logstash 版本 6.2.x 以及以上,conf 文件裏不要設定 jdbc_driver_library,將 connector
driver 的 jar 文件直接放在 /usr/share/logstash/logstash-core/lib/jars/ 即可。
參考文檔:Stackoverflow 的一篇提問
Logstash 運行之後,Elasticsearch 裏只有一條數據,其他數據沒有存進來。
主要原因是因爲數據的id重複了,因此 Elasticsearch 會將舊數據刪除,並插入新數據。 通過檢查 index 的狀態
(http://localhost:9200/_cat/indices?v),可以發現有數據被 deleted 了。
當我們在設置 Logstash 的 conf 文件時,在 output document_id 這一項,使用通配符 %{field_name} 來指定一個唯一的 id。比如說,我們從數據庫 select 出來的 fields 裏有一個作爲 primary key 的 field 名爲 id,我們則可以這樣設置:%{id}。Logstash 就會以這個 field 裏的值來作爲 Elasticsearch 裏的 id。
參考文檔:elastic 官網的一篇提問
Logstash sql_last_value is always 0
如果 tracking_column 是 timestamp的話,記得設定
tracking_column_type => timestamp
Logstash sql_last_value timestamp is not consistent with database
一個可能的原因是因爲 logstash :sql_last_value 的 timezone 與數據庫的不一致。因爲
Elasticsearch 跟 Logstash 默認使用 UTC timezone。
解決方法:在 Logstash 的配置文件裏,設置 jdbc_default_timezone 爲你想要的 timezone,則 Elasticsearch 與 Logstash 會據此進行轉換。
參考文檔:Logstash-jdbc-input Timezone issue
MySQL Docker 容器第一次啓動的時候出現 ERROR 1396 (HY000) at line 1: Operation CREATE USER failed for ‘root’@’%’ 然後就關掉了
可能是因爲我們在 docker-compose.yml 裏面傳遞參數給 MySQL 容器的時候,重複創建名爲 root
的用戶所導致的錯誤。嘗試換個用戶名試試看。或者如果要繼續使用 root 用戶來創建表的話,只需要在 docker-compose.yml 設置 MYSQL_ROOT_PASSWORD 就可以了。
參考文檔:ERROR 1396 (HY000): Operation CREATE USER failed for ‘root’@’%’ #129
Logstash :sql_last_value 的 timestamp 把秒之後的毫秒給 truncate 掉了,這時候如果數據庫的 timestamp 保有毫米信息的話,則會發生重複選取數據的問題。
例如: 如果數據庫的時間戳爲 2019-11-20 09:28:03.042000,Logstash 的 sql_last_value 則會變成 2019-11-20 09:28:03。而我們的 query 是想選取 > :sql_last_value 的,由於數據庫裏的這條數據有毫秒信息,那麼他永遠都會比較大而導致重複選取數據。
這個問題已經在 logstash-jdbc-input 4.3.5 版本里修復了。如果我們用的是最新的 Logstash 版本,則我們容器自動安裝的 jdbc-input 插件也應該是有這個修復的版本。
此時,我們還必須在 Logstash 配置文件爲 jdbc 做如下配置纔可以使這個修復生效。
sequel_opts => {
fractional_seconds => true
}
參考文檔:Force all usage of sql_last_value to be typed according to the settings #260
在 Spring Boot 裏,當我們對同一個Entity同時使用 @Entity 與 @Document 時,出現以下錯誤:
The bean ‘studyCardPagedJpaRepository’, defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
因爲 @Entity 表示由 JPA 掌管這個實體類,而 @Document 表示由 Elasticsearch
掌管這個實體類,兩個一起使用的話會產生衝突並導致異常。因此我們需要創建另一個實體類專門給 Elasticsearch 使用。
參考文檔:CSDN 的一篇博文
參考文檔:Spring Data 踩坑記錄
作者仍在學習中, 如果有什麼錯誤,請各位指出幷包含,謝謝!
作者:David Chou(溫哥華 Simon Fraser University 計算機學生)