修改 Cannal,解决数据同步过程时间转换异常问题 & 解决 ES 深分页问题

概述

这几天复习项目和八股文,然后整理了一下项目的问题,主要是修改 Cannal 源码的,感兴趣的小伙伴可以看看。

需求背景

中级件版本:

  • MySQL:8.X
  • Canal:1.1.7
  • ES:8.13.X

在项目中,我们通过监听 MySQL BinLog 日志然后通过 Canal 将数据同步到 ElasticSearch 中,项目的流程图如下:

image.png

然后如果用户进行搜索时,系统交互如下:

image.png 可以发现 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处日期转换部分内容,做如下修改:

image.png

修改后重新打包,编译成 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,问题就解决了。

image.png

补充:基于 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 深分页问题,区别如下:

image.png

然后在项目中,由于不存在随机页访问的场景,所以我们主要选择性能更加好的 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; }
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
答案说明所有
作者分享
🌟择难路,未有疑,四非学院本运气拉满,春招拿下大厂后端
160
快了,xdm,由于这周上班(小加班),写得有点慢,已经抓紧码字了😝
7
在学校的时候忙毕业的事(主要享受学校时光,有点懒),就每天写一点,发现还不如推倒重写,这今天整理一下😁,先预告一手
11
分析一个真人真事,就是我舍友下午去了一个测试的公司,然后那边说实训2个月,后面打一年工抵工资,我舍友来问我需要注意什么,就我看了一下合同,感觉有点像是招转陪,后面上网查了一下确实是,所以在这里警醒大家,找工作千万要注意,不要因为急给骗了😂
13
晓多科技社招一面 周天体测完全身酸痛,就没去上班,请假在家面试,然后上周投了一个社招1~3 年的岗位,就简单面了一下: 1. 自我介绍 2. 询问背景情况,确认社招,然后校招通过可以提前实习 3. 询问了一下实习情况 4. 直接做一道题,就直接给一个白板,原本要用 IDEA 写,但是我 IDEA 启动不了,直接白板写了,题目要求代码实现,包括数据库表设计和设计模式,我数据库表设计直接用 class 实体类代替了,问题不是很大,然后回答单例完成 ID 生成器包装,策略完成不同类型优惠券计算,然后工厂针对优惠券返回,然后加上责任链进行优惠卷相关参数校验,然后我这里 ID 生成说的时候用雪花算法+基因法冗余了用户 ID 号,然后从 ThreadLocal 里面取出用户 ID对比,避免水平越权,然后代码实现工厂、策略,差不多就这些了 /** * 题目1:电商优惠券系统设计 业务场景: 设计一个电商平台的优惠券系统,需满足: 1.支持多种优惠类型(满减、折扣、赠品)。 2.优惠券可叠加使用,但需校验适用范围(如特定商品/用户等级)。 3.需记录优惠券领取、使用记录,并支持运营按“用户领取量”和“券使用率”分析数据。 重点: 1.数据建模(数据表与关系设计)。 2.面向对象设计(要素+设计模式)。 * * */ 5. 算法题:双向链表反转,直接白板手敲 出去差不多半小时 HR 反馈过了,二面
17
下载 APP