告别ELK,APO提供基于ClickHouse开箱即用的高效日志方案——APO 0.6.0发布
ELK一直是日志领域的主流产品,但是ElasticSearch的成本很高,查询效果随着数据量的增加越来越慢。业界已经有很多公司,比如滴滴、B站、Uber、Cloudflare都已经使用ClickHose作为ElasticSearch的替代品,都取得了不错的效果,实现了降本增效,费用节约大多在50%以上。但是目前使用ClickHose作为日志方案,存在以下问题。
- 主流的Vector+ClickHose并未实现开箱即用,有许多的管理配置工作
- 绝大多数方案不支持近似全文检索的功能(该功能很重要)
- 使用双数组或者Map的表结构查询效率不高
- ClickVisual是最接近的开箱即用的日志方案,也存在以下问题:
○强依赖Kafka,对于某些中小用户而言方案不够灵活,不友好
○未引入Vector,原生的ClickHose Kafka引擎在大流量情况下可能导致ClickHose内存爆掉(感谢社区大佬 十四反馈)
主流的Vector+ClickHouse方案并未实现开箱即用
目前业界很多公司都是基于Vector+ClickHouse的方案来实现日志的采集和存储,该方案需要管理维护的工作量相对而言比较高,适用于动手能力强的公司。
维护工作:为每种日志手动维护一张表
每个公司的部门团队可能日志规范都不完全一致,如果需要对日志内容进行快速搜索定位故障,就需要提前想好ClickHouse的表结构,然后调整Vector的配置文件,最终实现Vector根据不同日志格式,parse成不同的日志表字段,写入不同的日志表。
比如每种日志都得建立以下类似的表结构,才能完成日志按照ip、url等字段的索引实现快速搜索。但是另外一个部门的日志也许就不需要IP和url字段,那么该部门得重新设计表结构。
CREATE TABLE log
(`ip` String,`time` Datetime,`url` String,`status` UInt8,`size` UInt32,`agent` String
)
ENGINE = MergeTree
ORDER BY date(time)
使用双数组或者Map的表结构查询效率不高
为了能够规避这些维护工作,所以很多公司对固定日志表结构进行了调整,常见的有两种方案,一种是双数组方案,另外一种就是Map方案。
Uber和Signoz的日志实现方案都是基于双数组
其日志表结构类似于下面这种
CREATE TABLE <table_name>
(//Common metadata fields._namespace String,_timestamp Int64,hostname String,zone String,...//Raw log event._source String,//Type-specific field names and field values.string.names Array(String),string.values Array(String),number.names Array(String), number.values Array(Float64),bool.names Array(String),bool.values Array(UInt8),//Materialized fieldsbar.String, Stringfoo.Number Float64,...
)
...
滴滴、B站等日志实现是基于Map结构
引入Map结构能够动态实现日志关键字段搜索
CREATE TABLE ck_bamai_stream.cn_bmauto_local
(`logTime` Int64 DEFAULT 0, --Log打印的时间`logTimeHour` DateTime MATERIALIZED toStartOfHour(toDateTime(logTime / 1000)),--将Log`odinLeaf` String DEFAULT '',`uri` LowCardinality(String) DEFAULT '',`traceid` string DEFAULT '',`cspanid` String DEFAULT '',`dltag` String DEFAULT '',`spanid` String DEFAULT '',`message` String DEFAULT '',`otherColumn` Map<String,String>`_sys_insert_time` DateTime MATERIALIZED now()
)
ENGINE =MergeTree
PARTITION BY toYYYYMMDD(logTimeHour)
ORDER BY(logTimeHour,odinLeaf,uri,traceid)
TTL _sys_insert_time +toIntervalDay(7),_sys_insert_time + toIntervalDay(3)To VOLUME 'hdfs
SETTINGS index_granularity = 8192,min_bytes_for_wide_part=31457280
Create Table <log_app_name> ON CLUSTER ...
{_timestamp Datetime64(3),`log,level` String CODC(ZSTD(1)),`log.msg` String CODC(ZSTD(1)),`log.trace_id` String CODC(ZSTD(1)),...string_map MapV2(String, Nullable(String))CODEC(ZSTD(1))number_map MapV2(String, Nullable(Float64))CODEC(ZSTD(1))bool_map MapV2(String, Nullable(UInt8))
}
ENGIN = ReplicatedMergeTree(...)
PARTITION BY toYYYYMMDD(_timestamp)
ORDER BY timestamp
TTL toDateTime(timestamp) + toIntervalDay(...),toDateTime(timestamp) + toIntervalDay(...) TO VOLUME `cold_volume`
Map的动态字段搜索效率低
https://clickhouse.ac.cn/docs/knowledgebase/improve-map-performance
根据社区反馈,map底层实现为线性数组,map查询效率通常低于列查询3~10倍,特别是日志量规模越大,map查询效率越低。
同时支持Map类型的最低clickhosue版本为21.11
-
列式存储优势:
ClickHouse 的核心优势在于它是列式存储数据库,这意味着当执行查询时,只需要读取查询中涉及的列,而不必加载不相关的列。列式存储还能够通过数据类型特定的压缩技术显著减少 IO 操作,从而加快查询速度 -
基于 Map 的查询:
Map 是一种键值对数据结构,在查询时需要额外的开销来解析嵌套结构,并且无法像列式存储那样直接跳过不相关的数据。虽然 ClickHouse 对 Map 数据类型有一些优化,但它在处理复杂结构时往往会比简单的列查询慢
双数组的搜索效率也不高
虽然 ClickHouse 对Array有一定的优化,但双数组结构仍然比单纯的列查询开销大。
影响性能的因素:
- 多级解析开销:查询双数组时,需要进行多层嵌套解析。例如,访问数组中的子数组意味着需要遍历父数组,然后进一步解析子数组的结构,这比单纯读取一个列复杂得多
- 随机存取:双数组的访问模式往往比简单的列查询更加随机化。访问数组中的元素可能导致更多的跳转,影响缓存命中率,从而降低性能
- 内存使用和数据存储:嵌套数组会使得 ClickHouse 的数据存储和内存管理更加复杂,因为数组中的数据长度不固定,导致压缩效果比单纯列差,数据块的大小也更加难以优化
性能上的差距取决于具体的查询模式和数据结构:
- 在简单查询场景(例如,读取一个基本的列数据),单纯的列查询会比双数组快得多,特别是在处理大规模数据时。性能差距可能达到 数倍甚至十倍 以上,尤其是当查询不涉及嵌套结构时。
- 在复杂查询场景(例如,查询涉及嵌套数组、需要频繁地进行数组拆解和操作),双数组的查询性能通常明显低于单纯的列查询。查询双数组的额外解析和处理开销,会使查询时间增加。根据不同的嵌套深度和数据量,性能可能下降 数倍。
ClickHouse的官方文档中日志方案也由于引入了Map效率不高
ClickHouse官方blog :
https://ClickHouse.com/blog/storing-log-data-in-ClickHouse-fluent-bit-vector-open-telemetry
提到有以下几种表结构:
OTEL的日志字段表
CREATE TABLE otel.otel_logs
(`Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),`TraceId` String CODEC(ZSTD(1)),`SpanId` String CODEC(ZSTD(1)),`TraceFlags` UInt32 CODEC(ZSTD(1)),`SeverityText` LowCardinality(String) CODEC(ZSTD(1)),`SeverityNumber` Int32 CODEC(ZSTD(1)),`ServiceName` LowCardinality(String) CODEC(ZSTD(1)),`Body` String CODEC(ZSTD(1)),`ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),`LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),//数据索引INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
Vector 字段表
CREATE TABLE vector.vector_logs
(`file` String,`timestamp` DateTime64(3),`kubernetes_container_id` LowCardinality(String),`kubernetes_container_image` LowCardinality(String),`kubernetes_container_name` LowCardinality(String),`kubernetes_namespace_labels` Map(LowCardinality(String), String),`kubernetes_pod_annotations` Map(LowCardinality(String), String),`kubernetes_pod_ip` IPv4,`kubernetes_pod_ips` Array(IPv4),`kubernetes_pod_labels` Map(LowCardinality(String), String),`kubernetes_pod_name` LowCardinality(String),`kubernetes_pod_namespace` LowCardinality(String),`kubernetes_pod_node_name` LowCardinality(String),`kubernetes_pod_owner` LowCardinality(String),`kubernetes_pod_uid` LowCardinality(String),`message` String,`source_type` LowCardinality(String),`stream` Enum('stdout', 'stderr')
)
ENGINE = MergeTree
ORDER BY (`kubernetes_container_name`, timestamp)
fluent字段表
CREATE TABLE fluent.fluent_logs
(`timestamp` DateTime64(9),`log` String,`kubernetes` Map(LowCardinality(String), String),`host` LowCardinality(String),`pod_name` LowCardinality(String),`stream` LowCardinality(String),`labels` Map(LowCardinality(String), String),`annotations` Map(LowCardinality(String), String)
)
ENGINE = MergeTree
ORDER BY (host, pod_name, timestamp)
日志需要近似全文检索
基于ElasticSearch的日志方案,由于可以基于ElasticSearch实现的日志内容分词,所以很容易实现全文检索,但是基于ClickHouse就很难实现该功能。
那是不是基于ClickHouse的方案就完全没有办法呢?
ClickHouse的索引介绍
- tokenbf_v1 按非字母数字字符(non-alphanumeric)拆分。相当于按符号分词,而通常日志中会有大量符号
在大牛的文章中,
https://juejin.cn/post/7130514546069864456
详细介绍了全文检索的实现,有兴趣的可以仔细看下大牛的文章。
最理想的日志方案应该满足什么条件?
我们认为理想的基于ClickHouse的日志方案应该满足以下几条:
- 使用列来进行检索,而不是map或者双array,保证高效的查询效率
- 用户不需要为了不同部门的日志内容,进行维护单独的表结构
- 支持对原始日志内容进行近似的全文检索
我们调研了国内外几乎所有基于ClickHouse的日志方案,最后发现国内开源项目ClickVisual项目的思路最相近,ClickVisual几乎可以做到开箱即用。
ClickVisual的方案不足
ClickVisual工作原理:
- 针对每种日志格式定义不同的parse规则,ClickVisual为每种规则生成一张新的日志表。该日志表存的就是解析之后的日志,之后的日志查询都是针对该日志表。因为解析之后的日志已经按照ClickHouse列存储了,所以关键字段查询是非常快的
- 基于Kafka表引擎,读取Kafka原始日志,落库至临时表中
- 魔术开始的地方:
基于ClickHouse的物化视图,将原始日志中的新增日志_raw_log_内容按照日志解析规则parse成日志列格式,并将解析好的日志存入该规则对应的日志表中
临时表
CREATE TABLE default.test_stream
(`status` String,`timestamp` Float64,`message` String CODEC(ZSTD(1))
)
ENGINE = Kafka
SETTINGS kafka_broker_list = '127.0.0.1:9092',
kafka_topic_list = 'test',
kafka_group_name = 'default_test',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 1,
kafka_skip_broken_messages = 0
物化视图
CREATE MATERIALIZED VIEW default.test_view TO default.test
(`status` String,`_time_second_` DateTime,`_time_nanosecond_` DateTime64(9),`_raw_log_` String,// 日志表的列`level` Nullable(String)//根据需要调整列...
) AS
SELECTstatus,toDateTime(toInt64(timestamp)) AS _time_second_,fromUnixTimestamp64Nano(toInt64(timestamp * 1000000000)) AS _time_nanosecond_,message AS _raw_log_,// 物化视图处理成列toNullable(toString(replaceAll(JSONExtractRaw(message, 'level'), '"', ''))) AS level// 根据需要添加更多解析规则...
FROM default.test_stream
WHERE 1 = 1
按照日志解析规则将_raw_log_parse成新的真实日志表
CREATE TABLE default.test
(`status` String,`_time_second_` DateTime,`_time_nanosecond_` DateTime64(9),`_raw_log_` String CODEC(ZSTD(1)),// 该列通过物化视图解析得到`level` Nullable(String),// 根据需要添加更多列...INDEX idx_raw_log _raw_log_ TYPE tokenbf_v1(30720, 2, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(_time_second_)
ORDER BY _time_second_
TTL toDateTime(_time_second_) + toIntervalDay(1)
SETTINGS index_granularity = 8192
每当需要分析新的索引字段,clickvisual会执行Add Colum为日志表添加新的列,同时更新物化视图添加新的解析处理规则。
ClickVisual的不足:
根据上述的原理:
- 不支持高效的近似全文检索,工作原理可以看出由于ClickVisual并未对_raw_log_进行跳数索引,所以也就导致ClickVisual不能高效的支持近似的对原始日志全文检索
- 由于ClickVisual完全依赖Kafka表引擎来实现日志的摄入,虽然ClickVisual也支持引入ClickHouse的已有日志表结构进行查询,但是很可能并不是直接针对列查询,而是针对map数据查询,只有通过Kafka引擎来摄入的日志才能生成新日志表结构,最终查询才是针对列式查询,才能有较高的查询效率
- Kafka表引擎读写日志速度无法控制,如果日志量非常多,导致ClickHouse物化视图工作过程中内存爆掉
APO 日志设计方案
ClickVisual已经非常接近理想日志方案了,只是我们需要对ClickVisual的逻辑进行调整。
- 不使用ClickHouse的Kafka表引擎来完成日志的摄取工作,而是改成Vector的方式完成日志的摄取工作。这样就不再依赖Kafka,对于中小用户日志规模没有那么大的用户,可以直接使用,而不需要维护Kafka。虽然去掉了Kakfa,同时增加了Vector,但是Vector的运维工作相比kafka而言,Vector几乎不需要运维
- 引入了Vector之后,可以通过配置Vector来调整参数,确保在大量日志洪锋的时候,也不至于将ClickHouse内存打爆
- 用户如果真的需要引入Kafka,也有已经维护好的Kafka,完全可以使用Vector先将原始日志写入Kafka,然后使用Vector从Kafka读取出来,继续实现后续的日志处理
- APO引入了ClickHouse null表引擎,来实现原始日志(从Vector写入的) 转换成按照日志解析格式解析之后的真实日志表。
- 所有的查询都是针对真实日志表的列查询,所以性能比较高
- 在真实日志表中,额外存储了_raw_log_,配合跳数索引完成 近似日志全文检索。
欢迎使用APO全量日志功能
APO v0.6.0更新日志:
新增功能
- 支持全量日志的采集、处理与展示功能
缺陷修复
- 修复服务端点存在特殊字符时,无法获取到依赖延时曲线的问题
- 修复无故障场景下频繁采集故障现场数据的问题
- 修复部分场景下数据库调用无指标的问题
- 修复传统服务器场景下,网络质量状态无法关联到告警的问题
- 修复传统服务器场景下,OneAgent配置注入失败的问题
其他
- 允许在创建 ClickHouse 表时选择是否创建副本
- 向下兼容 ClickHouse 版本,当前支持最低版本为 22.8.x
APO介绍:
国内开源首个 OpenTelemetry 结合 eBPF 的向导式可观测性产品
apo.kindlingx.com
https://github.com/CloudDetail/apo
相关文章:

告别ELK,APO提供基于ClickHouse开箱即用的高效日志方案——APO 0.6.0发布
ELK一直是日志领域的主流产品,但是ElasticSearch的成本很高,查询效果随着数据量的增加越来越慢。业界已经有很多公司,比如滴滴、B站、Uber、Cloudflare都已经使用ClickHose作为ElasticSearch的替代品,都取得了不错的效果ÿ…...

Excel使用技巧:定位Ctrl+G +公式+原位填充 Ctrl+Enter快速填充数据(处理合并单元格)
Excel的正确用法: Excel是个数据库,不要随意合并单元格。 数据输入的时候一定要按照行列输入,中间不要留空,不然就没有关联。 定位CtrlG 公式原位填充 CtrlEnter快速填充数据 如果把合并的单元格 取消合并,只有第一…...

JAVA学习-练习试用Java实现“成绩归类”
问题: 编写程序,输入一批学生的成绩,遇0或负数则输入结束,要求统计并输出优秀(大于85)、通过(60~84)和不及格(小于60)的学生人数。 示例 &#x…...

【Hive】7-拉链表的设计与实现
拉链表的设计与实现 数据同步问题 背景 例如:MySQL中有一张用户表: tb_user,每个用户注册完成以后,就会在用户表中新增该用户的信息,记录该用户的id、手机号码、用户名、性别、地址等信息。 每天都会有用户注册,产生…...

Maxwell 底层原理 详解
Maxwell 是一个 MySQL 数据库的增量数据捕获(CDC, Change Data Capture)工具,它通过读取 MySQL 的 binlog(Binary Log)来捕获数据变化,并将这些变化实时地发送到如 Kafka、Kinesis、RabbitMQ 或其他输出端。…...

使用短效IP池的优势是什么?
短效IP池作为代理IP服务中一种独特的资源管理方式,其应用已经在数据采集、市场分析和网络安全等多个领域中展示出强大的功能。尽管“短效”听起来似乎意味着某种限制,然而在某些特定的应用场景下,短效IP池却提供了无可比拟的优势。本文将详细…...

zynq烧写程序到flash后不运行
🏆本文收录于《全栈Bug调优(实战版)》专栏,主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&am…...

JMeter如何设置HTTP代理服务器?
1、 2、添加线程组 3、设置HTTP代理服务器,目标控制器选择“测试计划>线程组” 过滤掉不需要的信息 4、设置电脑手动代理 5、点击启动,在浏览器操作就可以了...

React面试题笔记(一)
一、react基础面试题 1.react中keys的作用是什么? key是是用于追踪哪些列表被修改,被添加或者被移除的辅助标识。 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是…...

3.Java入门笔记--基础语法
1.字面量 概念:计算机用来处理数据的,字面量就是告诉程序员数据在程序中的书写格式 常用数据:整数,小数直接写;字符单引号(A)且只能放一个字符;字符串双引号("Hel…...

关于SOCKS协议的常见误区有哪些?
代理协议在设备与代理服务器之间的数据交换中起到了关键作用。在这方面,SOCKS代理协议是常见的选择之一,被广泛应用于下载、传输和上传网络数据的场景。然而,关于SOCKS代理协议存在一些常见的误解,让我们来逐一了解。 一、使用SO…...

无极低码课程【redis windows下服务注册密码修改】
下载Windows版本的Redis linux环境 (自行下载) 1.打开官网https://redis.io/downloads/ windows环境 1.打开github https://github.com/microsoftarchive/redis/releases 然后选择你喜欢的版本zip或msi下载 2.这里下载zip版,解压后后,打开安装目录 3.双击redis-server…...

多ip访问多网站
1,关闭防火墙和安全软件 [rootlocalhost ~]# systemctl stop firewalld.service [rootlocalhost ~]# setenforce 02,挂载点,下载nginx [rootlocalhost ~]# mount /dev/sr0 /mnt [rootlocalhost ~]# dnf install nginx -y 3,一个虚拟机增加多个ip地址 [rootloc…...

Pytest参数详解 — 基于命令行模式!
1、--collect-only 查看在给定的配置下哪些测试用例会被执行 2、-k 使用表达式来指定希望运行的测试用例。如果测试名是唯一的或者多个测试名的前缀或者后缀相同,可以使用表达式来快速定位,例如: 命令行-k参数.png 3、-m 标记(…...

指针——函数指针数组
(一)前文回顾 1、前篇代码分析 void(*signal(int , void(*)(int)))(int) ; 那么这串代码究竟是什么呢? 别慌,让我们来一步一步拆解,首先我们通过之前的学习,已经明白了什么是函数指针(如果有…...

MySQL中的增查操作:探索数据的奥秘,开启数据之门
本节,我们继续深入了解MySQL,本章所讲的基础操作,针对的是表的增删查改! 一、Create 新增 1.1、语法 INSERT [INTO] table_name[(column [, column] ...)] VALUES(value_list) [, (value_list)] ... value_list: value, [, va…...

oracle_查询建表语句
查询建表语句 SELECTdbms_metadata.get_ddl ( TABLE, <table_name> ) FROMdualdbms_metadata.get_ddl:是Oracle提供的一个函数,用于获取数据库对象的DDL语句,它允许你查看或导出数据库对象的创建脚本‘TABLE’: 是这个函数…...

004-按照指定功能模块名称分组
按照指定功能模块名称分组 一、说明1.现在有一个需求:2.具体做法 二、代码案例三、效果展示 一、说明 1.现在有一个需求: 需要把一个功能模块的几个功能点放在同一个文档目录下,这几个功能点分布在不同的 Controller 2.具体做法 需要把他…...

ChatGPT写作助手:论文写作必备提示词一览
学境思源,一键生成论文初稿: AcademicIdeas - 学境思源AI论文写作 随着人工智能技术的发展,ChatGPT在学术写作领域的应用越来越广泛。它不仅能够帮助撰写论文,还可以通过不同的提示词完成构思、文献综述、数据分析、润色等任务&a…...

大数据开发电脑千元配置清单
大数据开发电脑配置清单 电脑型号HUANANZHI 台式电脑操作系统Windows 11 专业版 64位(Version 23H2 / DirectX 12)处理器英特尔 Xeon(至强) E5-2673 v3 2.40GHz主板HUANANZHI X99-P4T(P55 芯片组)显卡NVIDIA GeForce GT 610 ( 2…...

VP9官方手册-帧内预测
8.5.1 intra prediction process...

windows 自定义scheme协议。
浏览器打开自定义scheme参考上一篇:Chromium 自定义scheme协议启动过程分析c 1、注册表里面按照如下格式填写自定义scheme协议导入: Windows Registry Editor Version 5.00[HKEY_CLASSES_ROOT\jdtest] "URL:jdtest Protocol" "URL Proto…...

什么是SQLite?
一、什么是SQLite? SQLite是一个进程内的软件库,实现了自给自足的、无服务器的、零配置的、事务性的SQL数据库引擎。它是一个零配置的数据库,这意味着与其他数据库不一样,您不需要在系统中配置。 就像其它数据库,SQLite引擎不是…...

域1:安全与风险管理 第2章-人员安全与风险管理
第二章的内容确实较为丰富,需要细心且耐心地逐步消化和理解。不妨放慢阅读速度,对每个要点都进行深入思考,确保自己真正掌握了其核心意义。这样,虽然可能花费更多时间,但能够更扎实地掌握第二章的知识,为后…...

php中的错误和异常捕获
目录 一: 异常(Exceptions) 二: 错误(Errors) 三:实际项目的异常和错误处理 在PHP中,异常(Exceptions)和错误(Errors)是两个不同的…...

nextjs项目中,使用postgres的完整案例
目的 通过此案例,可以简单快速的过一下数据库的操作,熟悉app-router这种模式下,client component和server component的两种组件中基本的接口使用。 技术栈 nextjs14.2.* app-routervercel/postgres0.10.*typescript5 重要事情说三遍1 ap…...

tsconfig.json 内容解读
tsconfig.json 文件是 TypeScript 项目的主要配置文件,用于指定编译选项和项目设置。通过这个文件,你可以控制编译器的行为,例如输出文件的路径、模块解析方式、严格类型检查等。 以下是一些常见的 tsconfig.json 属性及其详细解释ÿ…...

KClass-关于kotlin中的反射
override val responseType: KClass<SetOperationModeResponse> SetOperationModeResponse::class,这段代码的含义: responseType 是一个只读属性(val),它的类型是 KClass<SetOperationModeResponse>。KClass<T&…...

Java集合剖析2】Java集合底层常用数据结构
一、数据结构与集合 接下来就要学习集合具体的实现类了,集合的实现类底层可能用1种或多种数据结构来存储数据。所以在学习集合的实现类前,我们有必要了解一下一些常见的数据结构,这样我们在后面查看集合实现类的底层源码时,才不会…...

java 第10天 String创建以及各类常用方法
一.String创建的两种形式 1.通过new的当时 String strnew String(); 2.不new的方式 String s1""; 二.new 和不new的方式的区别是什么 不new创建的字符串首先是拿着值去常量池中查找,是否有该内容,有就用常量池该字符串的地址࿰…...