修改 Cannal,解决数据同步过程时间转换异常问题 & 解决 ES 深分页问题
概述
这几天复习项目和八股文,然后整理了一下项目的问题,主要是修改 Cannal 源码的,感兴趣的小伙伴可以看看。
需求背景
中级件版本:
- MySQL:8.X
- Canal:1.1.7
- ES:8.13.X
在项目中,我们通过监听 MySQL BinLog 日志然后通过 Canal 将数据同步到 ElasticSearch 中,项目的流程图如下:
然后如果用户进行搜索时,系统交互如下:
可以发现 Cannal 在我们这个项目中主要起到的是一个同步数据的作用。
出现问题
在配置好数据同步之后,通过 spring-data-elasticsearch 查询的时候,会出现时间类型无法转换的问题,也就是其中传输商品数据的 saleTime,会出现字段映射失败的情况,报错内容如下:
▼text复制代码java.lang.RuntimeException: org.springframework.data.elasticsearch.core.convert.ConversionException: Unable to convert value '2024-05-13T16:04:16+08:00' to java.util.Date for property 'saleTime' org.springframework.data.elasticsearch.core.convert.ConversionException: Unable to convert value '2024-05-13T16:04:16+08:00' to java.util.Date for property 'saleTime' at org.springframework.data.elasticsearch.core.convert.DatePropertyValueConverter.read(DatePropertyValueConverter.java:56) at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.convertOnRead(MappingElasticsearchConverter.java:524) at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.propertyConverterRead(MappingElasticsearchConverter.java:518)
也就是说,日期类型在 ES 中存储的数据格式是 '2025-02-12T23:04:16+08:00'这种格式,而在代码中是无法转成我们想要的 Date 类型的。
这里首选尝试的方法是设置 saleTime 的 pattern,如:
▼text复制代码@Field(name = "sale_time",type = FieldType.Date, format = {},pattern = "yyyy-MM-dd'T'HH:mm:ss'+08:00' ||yyyy-MM-dd||strict_date_optional_time||epoch_millis") private Date saleTime;
但是我们会发现问题还是没有解决,因为从 ES 到代码这部分没有方法处理,那就只能让 ES 存储的内容去改变一下,让 ES 可以存储 '2025-02-12 23:04:16' 而不是'2025-02-12T23:04:16+08:00',就可以完美解决问题了。
这里我们尝试修改一下 ES 的索引配置,发现还是不行。
▼text复制代码// PUT test_collection { "mappings": { "properties": { "sale_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd'T'HH:mm:ss'+08:00 || strict_date_optional_time || epoch_millis" } } } } '
问题解决
如果以上问题都没有办法解决,那就只能从 Cannal 上看看为什么 ES 会存储
'2025-02-12T23:04:16+08:00' 这种格式的数据了,然后通过查看 ES 源码,我们发现在 ESSyncUtil中,针对日期类型,他会使用如下方式进行格式化:
▼text复制代码DateTime dateTime = new DateTime(((java.sql.Timestamp) val).getTime()); if (dateTime.getMillisOfSecond() != 0) { res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS" + Util.timeZone); } else { res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss" + Util.timeZone); }
这也是为什么会出现 '2025-02-12T23:04:16+08:00' 这种格式数据的原因,因为我们的项目并不涉及到多时区,所以就直接修改了一下 Cannal 的源码。
找到ESSyncUtil中的8处日期转换部分内容,做如下修改:
修改后重新打包,编译成 jar包。因为我们的ES 是8.0的版本,所以最终到workspace/canal-master/client-adapter/es8x/target下找到client-adapter.es8x-1.1.8-SNAPSHOT-jar-with-dependencies.jar这个编译后的包。
然后上传到 Cannal plugin 的安装目录下面:/root/package/canal-adapter/plugin,然后这样问题重启 Cannal,问题就解决了。
补充:基于 ElasticSearch 的search_after解决深分页问题
在 ElasticSearch 中进行分页查询,一般使用到两个参数,即 from 和 size,当我们对 ES 发起一个带有分页参数的查询,如使用from和size参数,ES 需要遍历所有匹配的文档,直到到达指定的起始点 from,然后返回这一点开始的 size 个文档。
▼text复制代码GET /your_index/_search{ "from": 50, "size": 10, "query": { "match_all": {} } }
- from:表示前面跳过了 50 条记录
- size:表示返回 10 条记录
from + size 的总数不能超过Elasticsearch索引的index.max_result_window设置,默认为10000。这意味着如果你设置from为9900,size为100,查询将会成功。但如果from为9900,size为101,则会失败。
ES 的检索机制就决定了,如果我们需要进行分页查询的时候,ES 要先找到所有位于当前页之前的记录。比如要查询 1000 页的数据,并且每页显示 10 条记录,系统需要先找到前面 9990条记录,然后才能获取到你请求的那10条记录。这意味着,随着页码的增加,数据库需要处理的数据量急剧增加,导致查询效率降低,这就是 ES 的深分页问题。
深度分页需要数据库在内存中维护大量的数据,并对这些数据进行排序和处理,这会消耗大量的CPU和内存资源。随着分页深度的增加,查询响应时间会显著增加。在某些情况下,这可能导致查询超时或者系统负载过重。
然后在一般情况下,我们可以通过 scroll 或者 search_after 来解决 ES 深分页问题,区别如下:
然后在项目中,由于不存在随机页访问的场景,所以我们主要选择性能更加好的 search_after 方案,这里主要是因为我们移动端页面 APP 是通过不断滚屏实现分页,这样只能顺序翻页,就可以完美避开 search_after 不支持随机页访问的问题,实现代码如下:
▼text复制代码public SAPageInfo<Collection> deepPageQueryByState(String name, String state, int pageSize, Long lastId) { LambdaEsQueryWrapper<Collection> queryWrapper = new LambdaEsQueryWrapper<>(); queryWrapper.match(Collection::getName, name) .and(wrapper -> wrapper .match(Collection::getState, state) .match(Collection::getDeleted, "0") ) .orderByAsc("collection_id"); SAPageInfo<Collection> saPageInfo; if (lastId == null) { saPageInfo = collectionEsMapper.searchAfterPage(queryWrapper, null, pageSize); } else { saPageInfo = collectionEsMapper.searchAfterPage(queryWrapper, ImmutableList.of(lastId), 10); } return saPageInfo; }
