前一段时间公司要进行数据转移,将我们ES数据库中的数据转移到客户的服务器上,并且使用定时将新增的数据同步,在这过程中学到了很多,在此记录一下!
Mysql + Redis + ElasticSearch + Kafka
为了降低服务器的压力,在每天的零时进行推送数据,推送前比较上一次推送记录在Redis中的数据,此记录为ES数据库中的时间字段,每次推送结束前都会将最新的时间更新在这个key中,如果获取ES数据库中的字段与key一样,说明今日无数据更新。
因为ES索引的数据量在千万以上,所以没有选择分页,而是选择了ES的滚轮查询。
public static void getDayData(RestHighLevelClient client,
KafkaTemplate kafkaTemplate,
RedisUtil redisUtil,
String field,
String indexName,
String topic) {
//发送创建索引所需的相关信息 索引名 属性 分片
HashMap<String, Object> map1 = new HashMap<>();
map1.put("indices", indexName);
map1.put("mappings", ElasticSearchUtil.getIndexMappings(client, indexName));
map1.put("settings", ElasticSearchUtil.getIndexSettingsShards(client, indexName));
kafkaTemplate.send(str, JSON.toJSONString(map1));
int i = 0;
final Scroll scroll = new Scroll(TimeValue.timeValueSeconds(30L));
SearchRequest request = new SearchRequest(indexName);
request.scroll(scroll);
SearchSourceBuilder builder = new SearchSourceBuilder();
//查询此索引的所有数据
builder.query(
QueryBuilders.rangeQuery(field)
.gt(redisUtil.hget(indexName,"push_time"))
).sort(field, SortOrder.ASC);
builder.size(1000);
request.source(builder);
SearchResponse response = null;
try {
response = client.search(request, RequestOptions.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
}
String scrollId = response.getScrollId();
SearchHit[] hits = response.getHits().getHits();
// 没有新增数据
if(hits == null)
log.info("索引 {} 今日无新增数据",indexName);
for (SearchHit hit : hits) {
Map<String, Object> map = hit.getSourceAsMap();
map.put("_id", hit.getId());
kafkaTemplate.send(topic, JSON.toJSONString(map));
i++;
}
//完成第一次后 更新key
redisUtil.hset(indexName, "push_time", hits[hits.length - 1].getSourceAsMap().get(field));
//通过在循环中调用搜索滚动 API 来检索所有搜索命中 直到不返回任何文件
while (hits != null && hits.length > 0) {
// 处理返回的搜索结果
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(scroll);
try {
response = client.scroll(scrollRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
}
scrollId = response.getScrollId();
hits = response.getHits().getHits();
for (SearchHit hit : hits) {
Map<String, Object> map = hit.getSourceAsMap();
map.put("_id", hit.getId());
kafkaTemplate.send(topic, JSON.toJSONString(map));
i++;
System.out.println(i);
}
//从第二次开始 每次都要更新key
redisUtil.hset(indexName, "push_time", hits[hits.length - 1].getSourceAsMap().get(field));
}
log.info("索引 {} 总共推送了 {} 条", indexName, i);
// 滚动完成后清除滚动上下文
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
try {
client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
}
}
TimeValue.timeValueSeconds(30L)
builder.size(1000)
这个参数最开始设置的是5L,但是条件查询的大小设置为了1000,可能会出现到了预计的时间但是没有找到1000条数据从而产生报错,所以尽可能将滚轮滚动的时间设置大一些,反正搜索完就会进行下一次滚动,不会产生拉低效率的问题!
获取索引属性信息和分片的工具类:
/**
* 获取 索引 mappings
* @param client
* @param index
* @return
*/
public static Map<String, Object> getIndexMappings(RestHighLevelClient client, String index) {
GetMappingsRequest request = new GetMappingsRequest();
request.indices(index);
GetMappingsResponse resp = null;
try {
resp = client.indices().getMapping(request, RequestOptions.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
}
return resp.mappings().get(index).getSourceAsMap();
}
/**
* 获取 索引 settings的分片
*
* @param client
* @param index
* @return
*/
public static Map<String, String> getIndexSettingsShards(RestHighLevelClient client, String index) {
Map<String, String> resMap = new HashMap<>();
GetSettingsRequest request = new GetSettingsRequest();
request.indices(index);
try {
GetSettingsResponse resp = client.indices().getSettings(request, RequestOptions.DEFAULT);
Settings settings = resp.getIndexToSettings().get(index);
System.out.println(settings);
resMap.put("number_of_shards", settings.get("index.number_of_shards"));
} catch (Exception e) {
e.printStackTrace();
}
return resMap;
}
为了更好的区分和控制,发送创建索引消息的Topic与发送数据的Topic不是同一个,这时候存在一个问题,就是创建索引完成的时间无法控制,从而无法发送数据
我们使用到了JUC的辅助类CountDownLatch,作为一个减数器,如果索引创建完毕,减数器减一,释放锁,非常好用!
@KafkaListener(id = "IPAttack", topics = "IPAttack")
public void IPAttackContinuous(List<String> records) throws InterruptedException {
BulkProcessor bulkProcessor = GetBulkProcessor.getBulkProcessor(client);
// 等待 index 创建
if (!ElasticSearchUtil.isExistsIndex(client, index)) {
log.error("索引: {} 还未创建", index);
//加锁 减数器的值设置为1
cdl = new CountDownLatch(1);
//减数器归0才能执行下面的代码
cdl.await();
}
//批量入库
for (String record : records) {
Map map = JSONObject.parseObject(record, Map.class);
String _id = map.get("_id").toString();
map.remove("_id");
bulkProcessor.add(new IndexRequest(index).id(_id).source(map));
}
bulkProcessor.flush();
bulkProcessor.awaitClose(30L, TimeUnit.SECONDS);
}
我们发现如果没有创建好索引,线程会阻塞导致无法执行下面的代码!
@KafkaListener(id = "CreateIndex",topics = "CreateIndex")
public void createIndexListener(String record){
Map map = JSON.parseObject(record,Map.class);
String index = map.get("indices").toString();
if(!ElasticSearchUtil.isExistsIndex(client,index)){
log.info("索引: {} 开始创建", index);
CreateIndexRequest indices = new CreateIndexRequest(index);
indices.settings((Map<String, ?>) map.get("settings"));
indices.mapping((Map<String, ?>)map.get("mappings"));
try{
client.indices().create(indices, RequestOptions.DEFAULT);
}catch (Exception e){
e.printStackTrace();
}
//创建索引完毕释放锁
if(DnsReceive.cdl != null){
DnsReceive.cdl.countDown();
}
log.info("索引: {} 创建完成", index);
}else{
log.info("已经存在索引:{}",index);
}
}
countDown()方法执行后,说明索引创建完毕,此时减数器减一,发送数据的Topic接收到就会开始批量数据入库
数据推送完毕后,可以将此次推送的数据量、索引名等等信息记录在MySQL中,这边还没有要求所以没有写
整体下来锻炼了逻辑思维和写代码的能力,完成以后又想了一遍觉得其实没有那么难,但对于小白刚入职场的我来说,是一次历练,无论对于我想问题的方式还是排错的切入点都有很好的帮助!