最近在分库分表项目中遇到一个相当隐蔽的问题:通过ShardingSphereDataSource获取的Connection查询元数据时,线下环境一切正常,但到了线上却只能查到部分分库的表信息。这个问题困扰了我们团队整整两天,最终发现是Connection元数据查询方式不当导致的。本文将详细剖析这个问题的来龙去脉,并给出完整的解决方案。
在我们的订单系统中,采用了分库分表架构,使用ShardingSphere作为中间件。业务需求是:定期扫描所有分库中表名前缀为"order_"的表,并为这些表添加新的状态字段。
初始实现看起来很简单:获取Connection→查询DatabaseMetaData→遍历结果集。线下测试时完美运行,但上线后却发现只能修改部分分库的表结构。
java复制public List<String> findTablesByPrefix(String prefix, String schemaName) {
List<String> tableNames = new ArrayList<>();
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
try (ResultSet rs = metaData.getTables(schemaName, null, prefix + "%", new String[]{"TABLE"})) {
while (rs.next()) {
tableNames.add(rs.getString("TABLE_NAME"));
}
}
} catch (SQLException e) {
throw new RuntimeException("查询表失败", e);
}
return tableNames;
}
| 环境 | 分库部署方式 | 现象 | 根本原因 |
|---|---|---|---|
| 线下测试 | 所有分库在同一MySQL实例 | 能查到所有分库的表 | 共享同一information_schema |
| 线上生产 | 分库分布在多个MySQL实例 | 只能查到路由到的实例中的表 | 元数据查询受限于实际连接到的MySQL实例 |
在ShardingSphere配置中:
yaml复制spring:
shardingsphere:
datasource:
ds0: # 逻辑库名
url: jdbc:mysql://host1:3306/order_db_0 # 物理库名order_db_0
ds1: # 逻辑库名
url: jdbc:mysql://host2:3306/order_db_1 # 物理库名order_db_1
关键区别:
当调用ShardingSphereDataSource.getConnection()时:
这就是为什么元数据查询会出现问题的根本原因:获取的Connection已经被路由到特定物理库,其元数据查询只能返回该实例的信息。
线上报错日志显示某些表不存在,但数据库确认这些表确实存在。通过对比发现:
通过DEBUG发现,dataSource.getConnection()返回的Connection实际连接的是其中一个分库:
java复制Connection conn = dataSource.getConnection();
String catalog = conn.getCatalog(); // 总是返回同一个物理库名
正确的做法应该是:
java复制public class DataSourceHolder {
private final Map<String, DataSource> physicalDataSourceMap = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 获取ShardingSphere内部管理的所有原始DataSource
Map<String, DataSource> dataSourceMap = ((ShardingSphereDataSource) dataSource)
.getContextManager()
.getDataSourceMap(dataSource.getSchemaName());
// 建立物理库名到DataSource的映射
dataSourceMap.forEach((logicName, ds) -> {
try (Connection conn = ds.getConnection()) {
String physicalName = conn.getCatalog();
physicalDataSourceMap.put(physicalName, ds);
} catch (SQLException e) {
throw new RuntimeException("初始化数据源映射失败", e);
}
});
}
public DataSource getDataSourceByPhysicalName(String physicalName) {
return physicalDataSourceMap.get(physicalName);
}
}
java复制public Map<String, List<String>> findAllTables(String prefix) {
Map<String, List<String>> result = new HashMap<>();
physicalDataSourceMap.forEach((physicalName, ds) -> {
try (Connection conn = ds.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
List<String> tables = new ArrayList<>();
try (ResultSet rs = metaData.getTables(
physicalName, null, prefix + "%", new String[]{"TABLE"})) {
while (rs.next()) {
tables.add(rs.getString("TABLE_NAME"));
}
}
result.put(physicalName, tables);
} catch (SQLException e) {
throw new RuntimeException("查询表失败: " + physicalName, e);
}
});
return result;
}
code复制ShardingSphereDataSource
├── ContextManager
│ └── DataSourceMap (逻辑库名 → 原始DataSource)
├── 路由引擎
└── SQL改写引擎
关键点:
当执行metaData.getTables()时,实际上是在查询MySQL的information_schema表:
sql复制SELECT * FROM information_schema.tables
WHERE table_schema = 'order_db_0'
AND table_name LIKE 'order_%'
这个查询的结果完全取决于你连接到了哪个MySQL实例。
对于需要频繁查询元数据的场景,可以考虑:
这种模式不仅适用于表结构查询,还可用于:
java复制public void addColumnToAllTables(String columnDef) {
physicalDataSourceMap.forEach((name, ds) -> {
try (Connection conn = ds.getConnection()) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("ALTER TABLE ... ADD COLUMN " + columnDef);
}
} catch (SQLException e) {
throw new RuntimeException("执行DDL失败: " + name, e);
}
});
}
java复制public void validateTableStructure() {
Map<String, List<String>> firstDbTables = getTablesFromPhysicalDb("order_db_0");
physicalDataSourceMap.keySet().forEach(dbName -> {
if (!dbName.equals("order_db_0")) {
Map<String, List<String>> currentTables = getTablesFromPhysicalDb(dbName);
// 对比表结构是否一致
}
});
}
这个问题的本质在于混淆了逻辑库和物理库的边界。ShardingSphere作为中间件,虽然提供了统一的访问入口,但底层仍然是多个独立的物理数据库。对于需要突破分库边界进行操作的特殊场景,我们必须绕过路由机制,直接操作原始数据源。
在实际项目中,我建议:
最后提醒一点:在ShardingSphere 5.3.0+版本中,获取原始DataSource的方式有所变化,需要注意API兼容性问题。如果遇到动态建表等高级场景,还需要考虑刷新ShardingSphere的元数据缓存。