提示:
Db2Connection
主要负责与DB2数据库的交互,特别针对CDC功能,提供了获取和处理数据库更改日志的能力,同时包含数据库连接管理、查询执行和结果处理的通用功能
前言
提示:
Db2Connection
类旨在简化与 Db2 数据库的交互,提供了一套全面的数据库操作接口,特别是针对需要利用 Db2 的变更数据捕获能力的场景。通过这个类,开发者可以更方便地执行数据库操作,而无需直接处理复杂的 JDBC 连接和查询细节。
提示:以下是本篇文章正文内容
一、核心功能
核心功能详细说明
Db2Connection
类被设计为一个高度封装的数据库连接管理器,专门针对 IBM Db2 数据库。其核心功能主要集中在以下几个方面:
1. 数据库连接管理
- 初始化与配置:通过构造函数接收
Db2ConnectorConfig
对象,初始化数据库连接所需的配置信息,如数据库URL、用户名、密码等。 - 连接建立与释放:虽然具体的连接建立和释放逻辑可能封装在底层的
java.sql.Connection
或者Db2的特定JDBC驱动中,Db2Connection
提供了一个统一的接口来管理这些操作,隐藏了底层细节。
2. 变更数据捕获 (CDC) 功能
- LSN(Log Sequence Number)管理:
getMaxLsn()
: 查询并返回数据库中的最大LSN,这是追踪数据库更改的关键信息。incrementLsn(Lsn lsn)
: 更新LSN,通常在处理完一批更改后调用,以记录最新的位置。timestampOfLsn(Lsn lsn)
: 将LSN转换为时间戳,利用缓存机制提高查询效率。
- 获取更改记录:
getChangesForTable(TableId tableId, Lsn startLsn, Lsn endLsn, Consumer<Db2CdcRecord> consumer)
: 获取指定表在给定LSN范围内的所有更改记录,通过consumer
参数处理每一条记录。getChangesForTables(List<TableId> tableIds, Lsn startLsn, Lsn endLsn, Consumer<Db2CdcRecord> consumer)
: 批量获取多个表的更改记录,提高效率。
3. 表与数据库元数据操作
- 表结构与CDC状态查询:
getTableSchemaFromTable(TableId tableId)
: 获取指定表的结构信息,包括列名、数据类型等。getTableSchemaFromChangeTable(CdcEnabledTable changeTable)
: 从CDC启用的表获取表结构。listOfChangeTables()
: 返回数据库中所有CDC启用的表列表。listOfNewChangeTables()
: 返回新添加的CDC启用表列表。
- 数据库锁定与时间戳:
lockTable(TableId tableId)
: 对特定表施加独占锁,保证数据一致性。getCurrentTimestamp()
: 返回数据库的当前时间戳,用于各种时间敏感的业务逻辑。
4. 性能与优化
- LSN到时间戳缓存:
LsnToTimestampCache
用于缓存LSN到时间戳的映射,避免了频繁的数据库查询,显著提高了性能。 - 查询优化:通过预编译SQL语句、参数化查询等方式,减少SQL解析和执行的开销,提升查询效率。
5. 平台适配与扩展性
PlatformAdapter
的应用:通过策略模式,PlatformAdapter
抽象出了特定于Db2数据库的操作,使得Db2Connection
可以容易地扩展到支持其他类型的数据库,只需更换不同的适配器即可。
属性
Db2ConnectorConfig config
:存储了数据库连接所需的配置信息,如数据库URL、用户名、密码等。PlatformAdapter platform
:一个策略模式的应用,用于抽象出特定于 DB2 数据库的操作,如 SQL 语法差异、特定的数据库功能调用等。LsnToTimestampCache lsnToTimestampCache
:缓存 LSN 到时间戳的映射,以提高性能。String realDatabaseName
:存储数据库的真实名称,用于在多数据库环境中区分不同的数据库实例。
构造函数
Db2Connection(Db2ConnectorConfig config)
:接收一个配置对象作为参数,初始化连接配置、平台适配器、LSN到时间戳缓存及数据库的真实名称。
方法
getMaxLsn()
:查询并返回数据库的最大 LSN,用于确定数据库日志的最新位置。getChangesForTable(TableId tableId, Lsn startLsn, Lsn endLsn, Consumer<Db2CdcRecord> consumer)
:获取指定表在给定 LSN 范围内的所有更改记录,使用consumer
回调来处理每条记录。getChangesForTables(List<TableId> tableIds, Lsn startLsn, Lsn endLsn, Consumer<Db2CdcRecord> consumer)
:批量获取多个表的更改记录,提高效率。incrementLsn(Lsn lsn)
:更新 LSN,通常用于在处理完一批更改后更新跟踪的 LSN 位置。timestampOfLsn(Lsn lsn)
:将 LSN 映射到对应的时间戳,利用缓存提高查询速度。getCurrentTimestamp()
:返回数据库的当前时间戳,用于比较或校验时间相关的业务逻辑。lockTable(TableId tableId)
:对特定表施加独占锁,保证数据一致性。CdcEnabledTable
:内部类,封装了 CDC 启用表的信息,如表 ID、捕获名称等。listOfChangeTables()
:返回数据库中所有 CDC 启用的表列表。listOfNewChangeTables()
:返回新添加的 CDC 启用表列表。getTableSchemaFromTable(TableId tableId)
:获取指定表的结构信息。getTableSchemaFromChangeTable(CdcEnabledTable changeTable)
:从 CDC 启用表获取表结构。getRealDatabaseName()
:返回数据库的真实名称。connectionString()
:生成用于连接数据库的 JDBC URL。
设计原则与模式
- 封装:
Db2Connection
封装了所有与 Db2 数据库交互的细节,对外提供简单易用的接口。 - 依赖注入:构造函数接收
Db2ConnectorConfig
对象,遵循依赖注入原则,提高了代码的可测试性和灵活性。 - 策略模式:通过
PlatformAdapter
应用策略模式,使得Db2Connection
可以轻松扩展以支持其他类型的数据库。 - 缓存:
LsnToTimestampCache
的使用体现了缓存模式,减少了数据库查询次数,提高了性能。
Db2Connection
类通过高度封装和面向对象的设计,为与 Db2 数据库的交互提供了统一且强大的接口,同时利用策略模式和缓存技术增强了系统的可扩展性和性能。
二、代码分析
/** * Db2Connection类继承自JdbcConnection,专门用于建立与DB2数据库的连接。 * 该类提供了特定于DB2的函数和常量,以支持与DB2数据库的交互。 */ public class Db2Connection extends JdbcConnection { // 查询当前服务器的SQL语句,用于获取数据库名称。 private static final String GET_DATABASE_NAME = "SELECT CURRENT SERVER FROM SYSIBM.SYSDUMMY1"; // DB2 // 日志记录器,用于记录类的运行时信息。 private static Logger LOGGER = LoggerFactory.getLogger(Db2Connection.class); // 用于占位符的字符串,表示将被替换的表格名称。 private static final String STATEMENTS_PLACEHOLDER = "#"; // 锁定表的SQL语句,#将被替换为实际表名。 private static final String LOCK_TABLE = "SELECT * FROM # WITH CS"; // DB2 // 将LSN(日志序列号)转换为时间戳的SQL语句。 private static final String LSN_TO_TIMESTAMP = "SELECT CURRENT TIMEstamp FROM sysibm.sysdummy1 WHERE ? > X'00000000000000000000000000000000'"; // 查询表中键列列表的SQL语句。 private static final String GET_LIST_OF_KEY_COLUMNS = "SELECT " + "CAST((t.TBSPACEID * 65536 + t.TABLEID )AS INTEGER ) as objectid, " + "c.colname,c.colno,c.keyseq " + "FROM syscat.tables as t " + "inner join syscat.columns as c on t.tabname = c.tabname and t.tabschema = c.tabschema and c.KEYSEQ > 0 AND " + "t.tbspaceid = CAST(BITAND( ? , 4294901760) / 65536 AS SMALLINT) AND t.tableid= CAST(BITAND( ? , 65535) AS SMALLINT)"; // 当修改表数据时,列偏移量的常量。 private static final int CHANGE_TABLE_DATA_COLUMN_OFFSET = 4; // 表示引用字符的常量,用于处理数据库中的特殊字符。 private static final String QUOTED_CHARACTER = "\""; /** * 定义了用于连接DB2数据库的JDBC URL模式。 * 此模式使用占位符语法,以便根据配置属性动态构建URL。 */ private static final String URL_PATTERN = "jdbc:db2://${" + JdbcConfiguration.HOSTNAME + "}:${" + JdbcConfiguration.PORT + "}/${" + JdbcConfiguration.DATABASE + "}"; /** * 静态初始化ConnectionFactory,用于创建与DB2数据库的连接。 * 使用DB2驱动类名、类加载器以及默认端口来配置工厂。 */ private static final ConnectionFactory FACTORY = JdbcConnection.patternBasedFactory(URL_PATTERN, DB2Driver.class.getName(), Db2Connection.class.getClassLoader(), JdbcConfiguration.PORT.withDefault(Db2ConnectorConfig.PORT.defaultValueAsString())); /** * 实际数据库名称,可能在大小写上与连接器配置中给出的数据库名称不同。 */ private final String realDatabaseName; /** * 缓存Lsn到时间戳的映射,用于性能优化。 */ private final BoundedConcurrentHashMap<Lsn, Instant> lsnToInstantCache; /** * 连接器配置实例,用于管理数据库连接的配置。 */ private final Db2ConnectorConfig connectorConfig; /** * 平台适配器,用于处理特定于DB2平台的操作。 */ private final Db2PlatformAdapter platform; /** * 创建一个新的DB2连接。 * * @param config 配置实例,不能为null。 */ public Db2Connection(Db2ConnectorConfig config) { super(config.getJdbcConfig(), FACTORY, QUOTED_CHARACTER, QUOTED_CHARACTER); connectorConfig = config; lsnToInstantCache = new BoundedConcurrentHashMap<>(100); realDatabaseName = retrieveRealDatabaseName(); platform = connectorConfig.getDb2Platform().createAdapter(connectorConfig); } /** * 获取数据库中当前最大的日志序列号(LSN)。 * LSN用于标识日志中最后一次提交的事务的位置,作为最新数据状态的标记。 * * @return 当前最大的LSN。 * @throws SQLException 如果在查询过程中发生SQL错误。 */ public Lsn getMaxLsn() throws SQLException { // 执行最大LSN的查询并将其结果映射到Lsn对象。 return queryAndMap(platform.getMaxLsnQuery(), singleResultMapper(rs -> { final Lsn ret = Lsn.valueOf(rs.getBytes(1)); LOGGER.trace("当前最大lsn是 {}", ret); return ret; }, "最大LSN查询必须返回确切的一个值")); } /** * 提供给定表在特定LSN范围内的所有变更记录。 * 这通常用于数据库2变更捕获(CDC)流程,其中应用程序检索 * * @param tableId 表标识符 - 请求的表变更 * @param fromLsn 起始LSN - 变更区间下限(闭合) * @param toLsn 终止LSN - 变更区间上限(闭合) * @param consumer 变更处理器 - 处理变更结果的对象 * @throws SQLException 如果在执行过程中发生SQL错误。 */ public void getChangesForTable(TableId tableId, Lsn fromLsn, Lsn toLsn, ResultSetConsumer consumer) throws SQLException { final String query = platform.getAllChangesForTableQuery().replace(STATEMENTS_PLACEHOLDER, cdcNameForTable(tableId)); prepareQuery(query, statement -> { statement.setBytes(1, fromLsn.getBinary()); statement.setBytes(2, toLsn.getBinary()); }, consumer); } /** * 提供DB2 CDC捕获进程为一组表记录的所有更改。 * * @param changeTables 请求获取更改的表数组 * @param intervalFromLsn 区间下界(闭区间)的更改起始位置 * @param intervalToLsn 区间上界(闭区间)的更改结束位置 * @param consumer 变更处理器 * @throws SQLException 当数据库访问出错时抛出异常 */ public void getChangesForTables(Db2ChangeTable[] changeTables, Lsn intervalFromLsn, Lsn intervalToLsn, BlockingMultiResultSetConsumer consumer) throws SQLException, InterruptedException { final String[] queries = new String[changeTables.length]; final StatementPreparer[] preparers = new StatementPreparer[changeTables.length]; int idx = 0; for (Db2ChangeTable changeTable : changeTables) { final String query = platform.getAllChangesForTableQuery().replace(STATEMENTS_PLACEHOLDER, changeTable.getCaptureInstance()); queries[idx] = query; // 如果表在查询缓冲区中间被添加,我们需要 // 调整从第一个可用的LSN开始 LOGGER.trace("获取表 {} 在范围[{}, {}]内的更改", changeTable, intervalFromLsn, intervalToLsn); preparers[idx] = statement -> { statement.setBytes(1, intervalFromLsn.getBinary()); statement.setBytes(2, intervalToLsn.getBinary()); }; idx++; } prepareQuery(queries, preparers, consumer); } /** * 获取数据库日志中的下一个可用位置。 * * @param lsn 当前的位置LSN * @return 数据库中下一个位置的LSN * @throws SQLException 当数据库访问出错时抛出异常 */ public Lsn incrementLsn(Lsn lsn) throws SQLException { return lsn.increment(); } /** * 将提交的LSN映射到发生提交的时间点。 * * @param lsn 提交的LSN * @return 记录到数据库日志中的提交时间 * @throws SQLException 当数据库访问出错时抛出异常 */ public Instant timestampOfLsn(Lsn lsn) throws SQLException { final String query = LSN_TO_TIMESTAMP; if (lsn.getBinary() == null) { return null; } Instant cachedInstant = lsnToInstantCache.get(lsn); if (cachedInstant != null) { return cachedInstant; } return prepareQueryAndMap(query, statement -> { statement.setBytes(1, lsn.getBinary()); }, singleResultMapper(rs -> { final Timestamp ts = rs.getTimestamp(1); final Instant ret = (ts == null) ? null : ts.toInstant(); LOGGER.trace("LSN {} 的时间戳是 {}", lsn, ret); if (ret != null) { lsnToInstantCache.put(lsn, ret); } return ret; }, "LSN到时间戳查询必须返回一个确切的值")); } @Override public Optional<Instant> getCurrentTimestamp() throws SQLException { return queryAndMap("SELECT CURRENT_TIMESTAMP result FROM sysibm.sysdummy1", rs -> rs.next() ? Optional.of(rs.getTimestamp(1).toInstant()) : Optional.empty()); } /** * 对给定的表创建排他锁。 * * @param tableId 要锁定的表ID * @throws SQLException 当数据库访问出错时抛出异常 */ public void lockTable(TableId tableId) throws SQLException { final String lockTableStmt = LOCK_TABLE.replace(STATEMENTS_PLACEHOLDER, tableId.table()); execute(lockTableStmt); } private String cdcNameForTable(TableId tableId) { return Db2ObjectNameQuoter.quoteNameIfNecessary(tableId.schema() + '_' + tableId.table()); } /** * 表示启用了CDC的表的类。 */ public static class CdcEnabledTable { private final String tableId; private final String captureName; private final Lsn fromLsn; private CdcEnabledTable(String tableId, String captureName, Lsn fromLsn) { this.tableId = tableId; this.captureName = captureName; this.fromLsn = fromLsn; } public String getTableId() { return tableId; } public String getCaptureName() { return captureName; } public Lsn getFromLsn() { return fromLsn; } } /** * 获取所有变更表的集合。 * * @return 变更表集合 * @throws SQLException 当数据库访问出错时抛出异常 */ public Set<Db2ChangeTable> listOfChangeTables() throws SQLException { // 实现细节省略 } /** * 获取指定LSN区间内新启用CDC的表集合。 * * @param fromLsn 区间开始的LSN * @param toLsn 区间结束的LSN * @return 新的变更表集合 * @throws SQLException 当数据库访问出错时抛出异常 */ public Set<Db2ChangeTable> listOfNewChangeTables(Lsn fromLsn, Lsn toLsn) throws SQLException { // 实现细节省略 } /** * 从表对象获取表结构。 * * @param changeTable 变更表对象 * @return 表结构 * @throws SQLException 当数据库访问出错时抛出异常 */ public Table getTableSchemaFromTable(Db2ChangeTable changeTable) throws SQLException { // 实现细节省略 } /** * 从变更表获取表结构。 * * @param changeTable 变更表对象 * @return 表结构 * @throws SQLException 当数据库访问出错时抛出异常 */ public Table getTableSchemaFromChangeTable(Db2ChangeTable changeTable) throws SQLException { // 实现细节省略 } /** * 根据捕获名称生成变更表名称。 * * @param captureName 捕获名称 * @return 变更表名称 */ public String getNameOfChangeTable(String captureName) { return captureName + "_CT"; } /** * 获取真实的数据库名称。 * * @return 数据库名称 */ public String getRealDatabaseName() { return realDatabaseName; } @Override protected boolean isTableUniqueIndexIncluded(String indexName, String columnName) { // 忽略无名称的索引 return indexName != null; } /** * 检索实际的数据库名称。 * 尝试查询并映射结果以返回数据库名称, * 如果发生 SQLException,将被捕获并抛出运行时异常。 * @return 数据库名称字符串 */ private String retrieveRealDatabaseName() { try { return queryAndMap( GET_DATABASE_NAME, singleResultMapper(rs -> rs.getString(1), "无法检索数据库名称")); } catch (SQLException e) { throw new RuntimeException("无法获取数据库名称", e); } } /** * 为当前配置返回JDBC连接字符串。 * * @return 一个{@code String},其中{@code urlPattern}中的变量被配置中的值替换 */ public String connectionString() { return connectionString(URL_PATTERN); } /** * 根据DB2文档,NULL值高于所有其他值。 * * @return 包含true的Optional,表示NULL值在排序中位于最后 */ @Override public Optional<Boolean> nullsSortLast() { return Optional.of(true); } /** * 构建带有引号的表标识符字符串。 * * @param tableId 表ID对象 * @return 带有适当模式和表名引用的字符串 */ @Override public String quotedTableIdString(TableId tableId) { StringBuilder quoted = new StringBuilder(); if (tableId.schema() != null && !tableId.schema().isEmpty()) { quoted.append(Db2ObjectNameQuoter.quoteNameIfNecessary(tableId.schema())).append("."); } quoted.append(Db2ObjectNameQuoter.quoteNameIfNecessary(tableId.table())); return quoted.toString(); } /** * 准备多个查询语句,执行并处理结果集。 * * @param multiQuery 查询数组 * @param preparers 准备器数组,用于准备每个查询 * @param resultConsumer 结果集消费者,用于处理结果 * @throws SQLException SQL异常 * @throws InterruptedException 中断异常 */ @Override public JdbcConnection prepareQuery(String[] multiQuery, StatementPreparer[] preparers, BlockingMultiResultSetConsumer resultConsumer) throws SQLException, InterruptedException { // ... 方法体代码 ... } /** * 准备单个查询语句,执行并处理结果集。 * * @param preparedQueryString 预编译的查询字符串 * @param preparer 准备器,用于准备查询 * @param resultConsumer 结果集消费者,用于处理结果 * @throws SQLException SQL异常 * @throws InterruptedException 中断异常 */ @Override public JdbcConnection prepareQueryWithBlockingConsumer(String preparedQueryString, StatementPreparer preparer, BlockingResultSetConsumer resultConsumer) throws SQLException, InterruptedException { // ... 方法体代码 ... } // 其他方法的注释类似上述示例,根据方法功能进行描述 /** * 创建表ID对象。 * * @param databaseName 数据库名称 * @param schemaName 模式名称 * @param tableName 表名称 * @return TableId对象 */ @Override public TableId createTableId(String databaseName, String schemaName, String tableName) { return new TableId(null, schemaName, tableName); } /** * 验证日志位置是否有效。 * * @param partition 分区信息 * @param offset 偏移量上下文 * @param config 连接器配置 * @return 布尔值,表示日志位置是否有效 */ public boolean validateLogPosition(Partition partition, OffsetContext offset, CommonConnectorConfig config) { // ... 方法体代码 ... } // 其余方法的注释依此类推
}
总结
提示:Db2Connection
类是Db2数据库与应用程序之间的桥梁,封装了数据库访问的复杂性,提供了高效、安全的数据检索和操作能力,尤其专注于CDC相关功能,以支持实时数据流处理和数据同步场景。
针对多表情况下的Db2Connection
类
当处理大量表时,Db2Connection
类的性能优化变得尤为重要。以下是一些关键的优化思路和手段:
1. 连接池优化
- 复用连接:确保使用连接池(如HikariCP, C3P0)来复用数据库连接,减少频繁创建和销毁连接带来的开销。
- 合理设置大小:根据应用负载调整连接池的最小和最大连接数,避免资源浪费或不足。
2. 查询优化
- 批处理:对于多个相似的查询,考虑使用批处理技术,一次发送多个SQL语句,减少网络往返次数。
- 索引使用:确保常用查询字段上有适当的索引,尤其是涉及JOIN操作的字段。
- SQL语句优化:避免全表扫描,使用更高效的查询语句,如限制返回的行数、选择性更高的WHERE条件等。
3. 缓存机制
- 结果缓存:对于不变或缓慢变化的数据,可以使用缓存来存储查询结果,减少直接数据库查询。
- Lsn缓存:优化Lsn到时间戳的映射,使用高效的数据结构(如ConcurrentHashMap)来加速查找。
4. 异步处理
- 非阻塞IO:利用异步数据库驱动(如Reactive Streams)或线程池执行数据库操作,避免主线程阻塞。
- 并行查询:如果可能,将查询任务分配到不同的线程或节点上并行执行。
5. 数据分区
- 水平分区:将大表按业务逻辑或地理位置分割成小表,减少单个查询需要扫描的数据量。
- 垂直分区:将表拆分为多个具有较少列的小表,每个表只包含相关联的数据,减少不必要的数据传输。
6. 数据库配置优化
- 调优参数:根据数据库服务器的硬件配置和应用需求,调整数据库的缓冲池大小、锁等待超时等参数。
- 并发控制:合理设置并发用户数和事务隔离级别,避免死锁和资源争抢。
7. 代码层面优化
- 最小化数据传输:仅请求所需的数据,避免SELECT *,减少网络带宽消耗。
- 结果集处理:优化
ResultSetConsumer
的实现,减少数据处理延迟,如使用流式处理框架(如Apache Flink或Spark Streaming)。
8. 监控与分析
- 性能监控:定期检查数据库和应用的日志,使用工具(如Db2 Performance Advisor)分析慢查询和资源瓶颈。
- 基准测试:实施基准测试,对比不同优化方案的效果,选择最佳实践。
通过上述策略的综合应用,可以显著提升Db2Connection
类在处理多表场景下的性能,确保应用的响应速度和稳定性。