Debezium发布历史90

原文地址: https://debezium.io/blog/2020/04/09/using-debezium-with-apicurio-api-schema-registry/

欢迎关注留言,我是收集整理小能手,工具翻译,仅供参考,笔芯笔芯.

将 Debezium 与 A​​picurio API 和架构注册表结合使用
2020 年 4 月 9 日 作者: Jiri Pechanec
模式 avro apicurio
Debezium 从数据库流式传输的更改事件(用开发人员的话说)是强类型的。这意味着事件使用者应该了解事件中传递的数据类型。传递消息类型数据的问题可以通过多种方式解决:

消息结构以带外方式传递给消费者,消费者能够处理其中存储的数据

消息包含嵌入在消息中的元数据(模式)

该消息包含对注册表的引用,该注册表包含关联的元数据

第一种情况的一个例子是 Apache Kafka 众所周知的JsonConverter. 它可以以两种模式运行——有模式和无模式。当配置为不使用模式时,它会生成一条简单的 JSON 消息,其中消费者需要事先了解每个字段的类型,或者需要执行启发式规则来“猜测”并将值映射到数据类型。虽然这种方法非常灵活,但它对于更高级的情况可能会失败,例如编码为字符串的时间或其他语义类型。此外,与类型相关的约束通常会丢失。

以下是此类消息的示例:

{
“before”: null,
“after”: {
“id”: 1001,
“first_name”: “Sally”,
“last_name”: “Thomas”,
“email”: “sally.thomas@acme.com”
},
“source”: {
“version”: “1.1.0.Final”,
“connector”: “mysql”,
“name”: “dbserver1”,
“ts_ms”: 0,
“snapshot”: “true”,
“db”: “inventory”,
“table”: “customers”,
“server_id”: 0,
“gtid”: null,
“file”: “mysql-bin.000003”,
“pos”: 154,
“row”: 0,
“thread”: null,
“query”: null
},
“op”: “c”,
“ts_ms”: 1586331101491,
“transaction”: null
}
请注意,除了 JSON 的基本类型系统之外,不存在任何类型信息。例如,消费者无法从事件本身推断出数字id字段的长度。

第二种情况的例子又是JsonConverter。通过其schemas.enable选项,JSON 消息将由两部分组成 -schema和payload。该payload部分与前一个案例完全相同;该schema部分包含消息的描述、其字段、字段类型和关联的类型约束。这使得消费者能够以类型安全的方式处理消息。这种方法的缺点是消息大小显着增加,因为模式是一个相当大的对象。由于架构很少更改(您多久更改一次数据库表列的定义?),因此将架构添加到每个事件都会带来巨大的开销。

以下带有模式的消息示例清楚地表明模式本身可能比有效负载大得多,并且使用起来不太经济:

{
“schema”: {
“type”: “struct”,
“fields”: [
{
“type”: “struct”,
“fields”: [
{
“type”: “int32”,
“optional”: false,
“field”: “id”
},
{
“type”: “string”,
“optional”: false,
“field”: “first_name”
},
{
“type”: “string”,
“optional”: false,
“field”: “last_name”
},
{
“type”: “string”,
“optional”: false,
“field”: “email”
}
],
“optional”: true,
“name”: “dbserver1.inventory.customers.Value”,
“field”: “before”
},
{
“type”: “struct”,
“fields”: [
{
“type”: “int32”,
“optional”: false,
“field”: “id”
},
{
“type”: “string”,
“optional”: false,
“field”: “first_name”
},
{
“type”: “string”,
“optional”: false,
“field”: “last_name”
},
{
“type”: “string”,
“optional”: false,
“field”: “email”
}
],
“optional”: true,
“name”: “dbserver1.inventory.customers.Value”,
“field”: “after”
},
{
“type”: “struct”,
“fields”: [
{
“type”: “string”,
“optional”: false,
“field”: “version”
},
{
“type”: “string”,
“optional”: false,
“field”: “connector”
},
{
“type”: “string”,
“optional”: false,
“field”: “name”
},
{
“type”: “int64”,
“optional”: false,
“field”: “ts_ms”
},
{
“type”: “string”,
“optional”: true,
“name”: “io.debezium.data.Enum”,
“version”: 1,
“parameters”: {
“allowed”: “true,last,false”
},
“default”: “false”,
“field”: “snapshot”
},
{
“type”: “string”,
“optional”: false,
“field”: “db”
},
{
“type”: “string”,
“optional”: true,
“field”: “table”
},
{
“type”: “int64”,
“optional”: false,
“field”: “server_id”
},
{
“type”: “string”,
“optional”: true,
“field”: “gtid”
},
{
“type”: “string”,
“optional”: false,
“field”: “file”
},
{
“type”: “int64”,
“optional”: false,
“field”: “pos”
},
{
“type”: “int32”,
“optional”: false,
“field”: “row”
},
{
“type”: “int64”,
“optional”: true,
“field”: “thread”
},
{
“type”: “string”,
“optional”: true,
“field”: “query”
}
],
“optional”: false,
“name”: “io.debezium.connector.mysql.Source”,
“field”: “source”
},
{
“type”: “string”,
“optional”: false,
“field”: “op”
},
{
“type”: “int64”,
“optional”: true,
“field”: “ts_ms”
},
{
“type”: “struct”,
“fields”: [
{
“type”: “string”,
“optional”: false,
“field”: “id”
},
{
“type”: “int64”,
“optional”: false,
“field”: “total_order”
},
{
“type”: “int64”,
“optional”: false,
“field”: “data_collection_order”
}
],
“optional”: true,
“field”: “transaction”
}
],
“optional”: false,
“name”: “dbserver1.inventory.customers.Envelope”
},
“payload”: {
“before”: null,
“after”: {
“id”: 1001,
“first_name”: “Sally”,
“last_name”: “Thomas”,
“email”: “sally.thomas@acme.com”
},
“source”: {
“version”: “1.1.0.Final”,
“connector”: “mysql”,
“name”: “dbserver1”,
“ts_ms”: 0,
“snapshot”: “true”,
“db”: “inventory”,
“table”: “customers”,
“server_id”: 0,
“gtid”: null,
“file”: “mysql-bin.000003”,
“pos”: 154,
“row”: 0,
“thread”: null,
“query”: null
},
“op”: “c”,
“ts_ms”: 1586331101491,
“transaction”: null
}
}
登记处
第三种方法结合了前两种方法的优点,同时消除了前两种方法的缺点,但代价是引入了一个新组件(注册表),用于存储和版本消息模式。

有多种模式注册表实现可用;接下来我们将重点关注Apicurio 注册表,它是一个开源(Apache 许可证 2.0)API 和架构注册表。该项目不仅提供注册表本身,还提供客户端库,并以序列化器和转换器的形式与 Apache Kafka 和 Kafka Connect 紧密集成。

Apicurio 使 Debezium 和消费者能够交换其模式存储在注册表中的消息,并且仅传递对消息本身中的模式的引用。随着捕获的源表的结构和消息模式的发展,注册表也会创建模式的新版本,因此不仅当前模式而且历史模式都可用。

Apicurio 提供多种开箱即用的序列化格式:

具有外部化模式支持的 JSON

阿帕奇阿夫罗

协议缓冲区

每个序列化器和反序列化器都知道如何自动与 Apicurio API 交互,因此消费者作为实现细节与其隔离。唯一需要的信息是注册表的位置。

Apicurio 还为 IBM 和 Confluence 的模式注册表提供 API 兼容层。这是一个非常有用的功能,因为它允许使用kafkacat等第三方工具,即使它们不知道 Apicurio 的本机 API。

JSON转换器
在 Debezium 示例存储库中,有一个基于Docker Compose的示例,它与标准 Debezium 教程示例设置并行部署 Apicurio 注册表。
图片来自于原文
在这里插入图片描述

图 1. 部署拓扑

要遵循该示例,您需要克隆 Debezium示例存储库。

自 Debezium 1.2 起,Debezium 容器映像附带了 Apicurio 转换器支持。

您可以通过使用debezium/connect或debezium/connect-base图像版本 >=1.2 并添加环境变量来启用 Apicurio 转换器ENABLE_APICURIO_CONVERTERS=true。

$ cd tutorial
$ export DEBZIUM_VERSION=1.1

Start the deployment

$ docker-compose -f docker-compose-mysql-apicurio.yaml up -d --build

Start the connector

curl -i -X POST -H “Accept:application/json”
-H “Content-Type:application/json”
http://localhost:8083/connectors/ -d @register-mysql-apicurio-converter-json.json

Read content of the first message

$ docker run --rm --tty
–network tutorial_default debezium/tooling bash
-c ‘kafkacat -b kafka:9092 -C -o beginning -q -t dbserver1.inventory.customers -c 1 | jq .’
结果消息应如下所示:

{
“schemaId”: 48,
“payload”: {
“before”: null,
“after”: {
“id”: 1001,
“first_name”: “Sally”,
“last_name”: “Thomas”,
“email”: “sally.thomas@acme.com”
},
“source”: {
“version”: “1.1.0.Final”,
“connector”: “mysql”,
“name”: “dbserver1”,
“ts_ms”: 0,
“snapshot”: “true”,
“db”: “inventory”,
“table”: “customers”,
“server_id”: 0,
“gtid”: null,
“file”: “mysql-bin.000003”,
“pos”: 154,
“row”: 0,
“thread”: null,
“query”: null
},
“op”: “c”,
“ts_ms”: 1586334283147,
“transaction”: null
}
}
JSON 消息包含完整的有效负载,同时包含对 id 模式的引用48。可以使用idDebezium 文档定义的模式符号名称从注册表查询模式。在这种情况下,两个命令

$ docker run --rm --tty
–network tutorial_default
debezium/tooling bash -c ‘http http://apicurio:8080/ids/64 | jq .’

$ docker run --rm --tty
–network tutorial_default
debezium/tooling bash -c ‘http http://apicurio:8080/artifacts/dbserver1.inventory.customers-value | jq .’
产生相同的模式描述:

{
“type”: “struct”,
“fields”: [
{
“type”: “struct”,
“fields”: [
{
“type”: “int32”,
“optional”: false,
“field”: “id”
},
{
“type”: “string”,
“optional”: false,
“field”: “first_name”
},
{
“type”: “string”,
“optional”: false,
“field”: “last_name”
},
{
“type”: “string”,
“optional”: false,
“field”: “email”
}
],
“optional”: true,
“name”: “dbserver1.inventory.customers.Value”,
“field”: “before”
},

],
“optional”: false,
“name”: “dbserver1.inventory.customers.Envelope”
}
这与我们之前在“JSON with schema”示例中看到的相同。

连接器注册请求与前一个请求有几行不同:


“key.converter”: “io.apicurio.registry.utils.converter.ExtJsonConverter”,
“key.converter.apicurio.registry.url”: “http://apicurio:8080”,
“key.converter.apicurio.registry.global-id”:
“io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy”,

“value.converter”: “io.apicurio.registry.utils.converter.ExtJsonConverter”,
“value.converter.apicurio.registry.url”: “http://apicurio:8080”,
“value.converter.apicurio.registry.global-id”:
“io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy”

Apicurio JSON 转换器用作键和值转换器
Apicurio 注册表端点
此设置确保可以自动注册架构 ID,这是 Debezium 部署中的典型设置
Avro 转换器
到目前为止,我们仅演示了将消息序列化为 JSON 格式。虽然在注册表中使用 JSON 格式有很多优点,例如易于人类阅读,但它仍然不太节省空间。

要真正只传输数据而不产生任何重大开销,使用 Avro 格式等二进制格式序列化非常有用。在这种情况下,我们将仅打包数据,而无需任何字段名称和其他仪式,并且该消息将再次包含对存储在注册表中的模式的引用。

让我们看看 Avro 序列化如何轻松地与 Apicurio 的 Avro 转换器一起使用。

Tear down the previous deployment

$ docker-compose -f docker-compose-mysql-apicurio.yaml down

Start the deployment

$ docker-compose -f docker-compose-mysql-apicurio.yaml up -d --build

Start the connector

curl -i -X POST -H “Accept:application/json”
-H “Content-Type:application/json”
http://localhost:8083/connectors/
-d @register-mysql-apicurio-converter-avro.json
我们可以使用模式名称查询注册表:

$ docker run --rm --tty
–network tutorial_default
debezium/tooling
bash -c ‘http http://apicurio:8080/artifacts/dbserver1.inventory.customers-value | jq .’
生成的模式描述与之前的模式描述略有不同,因为它具有 Avro 风格:

{
“type”: “record”,
“name”: “Envelope”,
“namespace”: “dbserver1.inventory.customers”,
“fields”: [
{
“name”: “before”,
“type”: [
“null”,
{
“type”: “record”,
“name”: “Value”,
“fields”: [
{
“name”: “id”,
“type”: “int”
},
{
“name”: “first_name”,
“type”: “string”
},
{
“name”: “last_name”,
“type”: “string”
},
{
“name”: “email”,
“type”: “string”
}
],
“connect.name”: “dbserver1.inventory.customers.Value”
}
],
“default”: null
},
{
“name”: “after”,
“type”: [
“null”,
“Value”
],
“default”: null
},

],
“connect.name”: “dbserver1.inventory.customers.Envelope”
}
连接器注册请求与标准请求也有几行不同:


“key.converter”: “io.apicurio.registry.utils.converter.AvroConverter”,
“key.converter.apicurio.registry.url”: “http://apicurio:8080”,
“key.converter.apicurio.registry.converter.serializer”:
“io.apicurio.registry.utils.serde.AvroKafkaSerializer”,
“key.converter.apicurio.registry.converter.deserializer”:
“io.apicurio.registry.utils.serde.AvroKafkaDeserializer”,
“key.converter.apicurio.registry.global-id”:
“io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy”,

“value.converter”: “io.apicurio.registry.utils.converter.AvroConverter”,
“value.converter.apicurio.registry.url”: “http://apicurio:8080”,
“value.converter.apicurio.registry.converter.serializer”:
“io.apicurio.registry.utils.serde.AvroKafkaSerializer”,
“value.converter.apicurio.registry.converter.deserializer”:
“io.apicurio.registry.utils.serde.AvroKafkaDeserializer”,
“value.converter.apicurio.registry.global-id”:
“io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy”,

Apicurio Avro 转换器用作键和值转换器
Apicurio 注册表端点
规定转换器应使用哪个串行器和解串器
此设置确保可以自动注册架构 ID,这是 Debezium 部署中的典型设置
例如,为了演示接收器端消息的消耗,我们可以使用Kafka Connect Elasticsearch 连接器。接收器配置将再次仅通过转换器配置进行扩展,并且接收器连接器可以使用启用 Avro 的主题,而不需要任何其他更改。

{
“name”: “elastic-sink”,
“config”: {
“connector.class”: “io.confluent.connect.elasticsearch.ElasticsearchSinkConnector”,
“tasks.max”: “1”,
“topics”: “customers”,
“connection.url”: “http://elastic:9200”,
“transforms”: “unwrap,key”,
“transforms.unwrap.type”: “io.debezium.transforms.ExtractNewRecordState”,
“transforms.unwrap.drop.tombstones”: “false”,
“transforms.key.type”: “org.apache.kafka.connect.transforms.ExtractField$Key”,
“transforms.key.field”: “id”,
“key.ignore”: “false”,
“type.name”: “customer”,
“behavior.on.null.values”: “delete”,

"key.converter": "io.apicurio.registry.utils.converter.AvroConverter",
"key.converter.apicurio.registry.url": "http://apicurio:8080",
"key.converter.apicurio.registry.converter.serializer":
    "io.apicurio.registry.utils.serde.AvroKafkaSerializer",
"key.converter.apicurio.registry.converter.deserializer":
    "io.apicurio.registry.utils.serde.AvroKafkaDeserializer",
"key.converter.apicurio.registry.global-id":
    "io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy",

"value.converter": "io.apicurio.registry.utils.converter.AvroConverter",
"value.converter.apicurio.registry.url": "http://apicurio:8080",
"value.converter.apicurio.registry.converter.serializer":
    "io.apicurio.registry.utils.serde.AvroKafkaSerializer",
"value.converter.apicurio.registry.converter.deserializer":
    "io.apicurio.registry.utils.serde.AvroKafkaDeserializer",
"value.converter.apicurio.registry.global-id":
    "io.apicurio.registry.utils.serde.strategy.GetOrCreateIdStrategy",

}
}
结论
在本文中,我们讨论了消息/模式关联的多种方法。Apicurio 注册表是作为模式存储和版本控制的解决方案提出的,我们已经演示了 Apicurio 如何与 Debezium 连接器集成,以有效地将带有模式的消息传递给消费者。

您可以在 GitHub 上 Debezium 示例存储库的教程项目中找到将 Debezium 连接器与 Apicurio 注册表一起使用的完整示例。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/352281.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每次请求sessionid变化【SpringBoot+Vue】

引言:花了一晚上的时间,终于把问题解决了,一开始后端做完后,用apifox所有接口测试都是可以的,但当前端跑起来后发现接收不到后端的数据。 当我写完前后端,主页面和获取当前页面信息接口后,配置了cros注解 CrossOrigin…

【PythonRS】Rasterio库安装+基础函数使用教程

Rasterio是一个Python库,专门用于栅格数据的读写操作。它支持多种栅格数据格式,如GeoTIFF、ENVI和HDF5,为处理和分析栅格数据提供了强大的工具。RasterIO适用于各种栅格数据应用,如卫星遥感、地图制作等。通过RasterIO&#xff0c…

奇怪问题说 - 测试篇

文章目录 1.什么是软件测试2.软件测试和开发的区别3.软件测试的发展:4.软件测试岗位5.软件测试在不同类型公司的定位6.一个优秀的软件测试人员具备的素质6.1综合能力6.2掌握自动化测试技术6.3优秀的测试用例设计能力6.4探索性思维6.5有责任感和一定的压力 7.软件测试…

SpringSecurity(15)——OAuth2密码模式

工作流程 将用户和密码传过去,直接获取access_token,用户同意授权动作是在第三方应用上完成,而不是在认证服务器,第三方应用申请令牌时,直接带用户名和密码去向认证服务器申请令牌。这种方式认证服务器无法判断用户是…

力扣hot100 字符串解码 栈 辅助栈

Problem: 394. 字符串解码 文章目录 思路💖 辅助栈 思路 👨‍🏫 路飞 💖 辅助栈 ⏰ 时间复杂度: O ( n ) O(n) O(n) 🌎 空间复杂度: O ( n ) O(n) O(n) class Solution {public String decodeString(String s…

1.26 C++ day3

思维导图 作业&#xff1a; 设计一个Per类&#xff0c;类中包含私有成员:姓名、年龄、指针成员身高、体重&#xff0c;再设计一个Stu类&#xff0c;类中包含私有成员:成绩、Per类对象p1&#xff0c;设计这两个类的构造函数、析构函数和拷贝构造函数。 代码 #include <ios…

C语言指针数组的一篇补充

这段代码是我今早在想指针数组应该怎么去了解清楚的时候想到的一个代码&#xff0c;纠结了1半个多小时将代码理清楚&#xff0c;分享给大家看一下&#xff0c;对我最近发布的博文应该有一个补充帮助理解的作用。 对于这段代码的解释&#xff1a; 要正确理解指针数组是一个数组&…

java版代码生成器

之前实现的JRT代码生成器是M版的&#xff0c;那么用户必须用M库才能有代码生成器的功能。为了提供给就是不用M库的用户使用&#xff0c;JRT再提供脚本版的java代码生成器&#xff0c;方便直接连关系库生成JRT的代码。 实现&#xff1a; import JRT.Core.MultiPlatform.JRTCon…

代理IP有没有风险和安全问题?

在数字时代&#xff0c;随着互联网的日益普及&#xff0c;代理IP作为一种网络技术&#xff0c;其安全风险和潜在问题也逐渐成为人们关注的焦点。今天我们就来看看&#xff0c;代理IP到底有什么安全问题&#xff0c;我们又该如何避免这些问题呢&#xff1f; 这得从代理IP是什么来…

解读BEVFormer,新一代CV工作的基石

文章出处 BEVFormer这篇文章很有划时代的意义&#xff0c;改变了许多视觉领域工作的pipeline[2203.17270] BEVFormer: Learning Birds-Eye-View Representation from Multi-Camera Images via Spatiotemporal Transformers (arxiv.org)https://arxiv.org/abs/2203.17270 BEV …

fatal error:require():Failed opening required

今天部署网站遇到了个错误 fatal error:require():Failed opening required 这个错误经常遇到 大多是网站 是开启了 open_basedir 但今天这个错误很神奇 先说解决方法 1. 检测一下是不是真的 不存在这个文件 即使100%确定 也建议你再仔细看一下 这个文件存不存在 今天我遇…

高光谱图像加载、归一化和增强(jupyter book)

1.获取高光谱图像&#xff1a;我用的是indian_pines的数据集&#xff0c;感兴趣的兄弟可以自行去官方网下载&#xff0c;gt的那个是它的标签哦&#xff0c;别搞错了。 2.图像加载&#xff1a; &#xff08;1&#xff09;从本地路径加载 import scipy.io as sio# 文件路径 fil…

在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作

前言&#xff1a;什么是操作筛选器 操作筛选器是 ASP.NET Core Web API 中的一种过滤器&#xff0c;用于在执行控制器操作&#xff08;Action&#xff09;之前或之后执行一些代码&#xff0c;完成特定的功能&#xff0c;比如执行日志记录、身份验证、授权、异常处理等通用的处…

css设置不可点击

文章目录 一、前言二、MDN三、使用四、注意五、总结六、最后 一、前言 在网页开发中&#xff0c;经常会遇到一种情况&#xff0c;就是需要将某个元素的点击事件屏蔽&#xff0c;使其在用户点击时没有任何反应。这时候&#xff0c;我们可以通过CSS的pointer-events属性设置为no…

详解SpringCloud微服务技术栈:ElasticSearch搜索结果处理(排序、分页、高亮)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;详解SpringCloud微服务技术栈&#xff1a;DSL查询ES文档高级语法、相关性算分数学原理总结 &#x1f4da;订阅专栏&#xff1a;微…

【华为 ICT HCIA eNSP 习题汇总】——题目集9

1、缺省情况下&#xff0c;广播网络上 OSPF 协议 Hello 报文发送的周期和无效周期分别为&#xff08;&#xff09;。 A、10s&#xff0c;40s B、40s&#xff0c;10s C、30s&#xff0c;20s D、20s&#xff0c;30s 考点&#xff1a;①路由技术原理 ②OSPF 解析&#xff1a;&…

Android源码设计模式解析与实战第2版笔记(一)

第一章 走向灵活软件之路 — 面向对象的六大原则 优化代码的第一步 — 单一职责原则 单一职责原则的英文名称是Single Responsibility Principle&#xff0c;缩写是SRP。 SRP&#xff1a;就一个类而言&#xff0c;应该仅有一个引起它变化的原因。 一个类中应该是一组相关性很…

Elasticsearch:使用 Gemini、Langchain 和 Elasticsearch 进行问答

本教程演示如何使用 Gemini API创建 embeddings 并将其存储在 Elasticsearch 中。 我们将学习如何将 Gemini 连接到 Elasticsearch 中存储的私有数据&#xff0c;并使用 Langchian 构建问答功能。 准备 Elasticsearch 及 Kibana 如果你还没有安装好自己的 Elasticsearch 及 Ki…

小游戏选型(二):第三方社交小游戏厂家对比,即构/声网/融云/云信等

前言&#xff1a; 上一篇文章我们主要介绍社交游戏化趋势&#xff0c;并分析了直播平台面临的买量贵、变现难等问题&#xff0c;探讨了小游戏作为新的运营变现玩法的优势。同时还列举了各大直播平台TOP5的小游戏。今天我们继续介绍小游戏系列内容&#xff0c;本文是该系列的第…

力扣算法-Day20

541. 反转字符串II 给定一个字符串 s 和一个整数 k&#xff0c;从字符串开头算起&#xff0c;每计数至 2k 个字符&#xff0c;就反转这 2k 字符中的前 k 个字符。 如果剩余字符少于 k 个&#xff0c;则将剩余字符全部反转。如果剩余字符小于 2k 但大于或等于 k 个&#xff0c…