spring boot 工具类(11-20)
-------------------------------------------------------------------------------------------
1、springboot 分库分表 汇总查询分页解决方案
在 Spring Boot 中实现分库分表后的汇总查询分页,需要解决跨库表数据聚合、分页逻辑正确性、性能优化等核心问题。以下是一套套完整解决方案,结合 ShardingSphere 框架实现:
一、方案整体架构
- 路由解析:解析 SQL 并路由到目标分库分表。
- 并行查询:在所有目标分库分表上并行执行分页查询。
- 结果聚合:收集各分表结果,合并后重新排序、分页。
- 分页校正:处理跨分表的分页偏移量问题。
二、基于 ShardingSphere 的实现
1. 依赖配置
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2. 分库分表配置(application.yml)
spring:
shardingsphere:
datasource:
names: ds0, ds1 # 两个分库
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db0
username: root
password: root
ds1:
# 配置略(同ds0,指向db1)
rules:
sharding:
tables:
t_order: # 订单表分表
actual-data-nodes: ds${0..1}.t_order_${0..3} # 2库4表
database-strategy: # 分库策略(用户ID取模)
standard:
sharding-column: user_id
sharding-algorithm-name: order_db_inline
table-strategy: # 分表策略(订单ID取模)
standard:
sharding-column: order_id
sharding-algorithm-name: order_table_inline
sharding-algorithms:
order_db_inline:
type: INLINE
props:
algorithm-expression: ds${user_id % 2}
order_table_inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 4}
props:
sql-show: true # 打印SQL,方便调试
三、汇总查询分页核心实现
1. 基础分页查询(ShardingSphere 自动处理)
<select id="selectOrderPage" resultType="com.example.entity.Order">
SELECT * FROM t_order
WHERE status = #{status}
AND create_time >= #{startTime}
ORDER BY create_time DESC
</select>
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public IPage<Order> getOrderPage(Integer pageNum, Integer pageSize, Integer status, String startTime) {
// 分页参数
Page<Order> page = new Page<>(pageNum, pageSize);
// 执行分页查询(ShardingSphere自动处理分表汇总)
return orderMapper.selectOrderPage(page, status, startTime);
}
}
2. 大偏移量分页优化(游标分页)
LIMIT 100000, 10
),传统分页会导致各分表扫描大量数据,性能低下。此时需用游标分页替代:
<select id="selectOrderByCursor" resultType="com.example.entity.Order">
SELECT * FROM t_order
WHERE status = #{status}
AND create_time >= #{startTime}
${cursorCondition} <!-- 动态拼接游标条件 -->
ORDER BY create_time DESC, order_id DESC
LIMIT #{page.size}
</select>
3. 跨库联合查询(关联表分页)
<select id="selectOrderWithUser" resultType="com.example.vo.OrderVO">
SELECT o.*, u.username
FROM t_order o
JOIN t_user u ON o.user_id = u.id <!-- t_user为广播表,所有库都有相同数据 -->
WHERE o.user_id = #{userId}
ORDER BY o.create_time DESC
</select>
spring:
shardingsphere:
rules:
sharding:
broadcast-tables: t_user # 配置t_user为广播表
四、性能优化策略
-
索引优化:
- 分表键(如
user_id
、order_id
)必须建索引。 - 排序字段(如
create_time
)需建联合索引(如(status, create_time, order_id)
)。
- 分表键(如
-
查询过滤:
- 尽量通过分表键、时间范围等条件过滤,减少参与查询的分表数量。
- 避免
SELECT *
,只查询必要字段。
-
结果缓存:
- 对高频分页查询结果进行缓存(如 Redis),缓存键包含分页参数和查询条件。
- 示例:
order:page:status=1:startTime=2023-01-01:page=1:size=10
-
异步并行查询:
- 对于复杂聚合,可手动获取分表列表,通过线程池并行查询后合并结果:
java运行// 伪代码:手动并行查询分表 List<Future<IPage<Order>>> futures = new ArrayList<>(); for (String table : getShardingTables()) { // 获取所有分表名 futures.add(executor.submit(() -> orderMapper.selectByTable(page, table, status, startTime) )); } // 合并结果 List<Order> allOrders = new ArrayList<>(); for (Future<IPage<Order>> future : futures) { allOrders.addAll(future.get().getRecords()); } // 重新排序和分页 allOrders.sort(Comparator.comparing(Order::getCreateTime).reversed()); return PageHelper.offsetPage(allOrders, pageNum, pageSize);
五、常见问题解决方案
-
分页结果重复或遗漏:
- 确保排序字段唯一(如
ORDER BY create_time DESC, order_id DESC
),避免分表数据排序冲突。 - 禁用分表路由的随机算法,确保数据分布稳定。
- 确保排序字段唯一(如
-
关联查询性能差:
- 非分表字段关联时,将小表设为广播表。
- 大表关联通过冗余字段避免跨表查询(如订单表冗余用户名)。
-
count (*) 效率低:
- 使用 ShardingSphere 的
COUNT
优化(MAX()
替代全表扫描)。 - 维护分表数据量缓存,汇总时直接累加缓存值。
- 使用 ShardingSphere 的
总结
- 利用 ShardingSphere 自动路由和结果合并,简化基础分页实现。
- 大偏移量场景采用游标分页,基于排序字段避免全表扫描。
- 关联查询通过广播表、绑定表优化,减少跨库数据传输。
- 结合索引、缓存、异步并行等手段提升性能。
-------------------------------------------------------------------------------------------
2、分库分表 结合clickhouse 实现实时查询
一、架构设计总览
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 业务应用层 │ │ 数据同步层 │ │ 存储查询层 │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 分库分表集群 │────▶│ 实时同步管道 │────▶│ ClickHouse │
│(MySQL/PostgreSQL)│ │(CDC/消息队列)│ │(分析查询引擎)│
└───────────────┘ └───────────────┘ └───────┬───────┘
│
▼
┌───────────────┐
│ 实时查询接口 │
└───────────────┘
- 分库分表集群:存储在线业务数据(如订单、用户),支持高并发 OLTP。
- 数据同步层:将分库分表的增量数据实时同步到 ClickHouse。
- ClickHouse:以列存方式存储全量数据,支持毫秒级实时分析查询。
二、核心组件与实现
1. 分库分表与 ClickHouse 表结构设计
-- 分表策略:按 user_id 分库,order_id 分表
CREATE TABLE t_order_0 (
order_id BIGINT PRIMARY KEY, -- 雪花算法ID(含时间戳)
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL,
INDEX idx_user_time (user_id, create_time)
) ENGINE=InnoDB;
-- 采用 MergeTree 引擎,按时间分区,支持实时写入和快速查询
CREATE TABLE order_analysis (
order_id UInt64,
user_id UInt64,
amount Decimal(10,2),
status UInt8,
create_time DateTime,
-- 冗余分表键,加速关联查询
shard_key UInt8 COMMENT 'user_id % 4(分库键)'
) ENGINE = MergeTree()
PARTITION BY toDate(create_time) -- 按天分区
ORDER BY (user_id, create_time) -- 主键排序,优化范围查询
TTL create_time + INTERVAL 1 YEAR -- 数据自动过期(保留1年)
SETTINGS index_granularity = 8192; -- 索引粒度,平衡内存与性能
2. 实时数据同步方案
通过 Debezium 捕获分库分表的 binlog 变更,经 Kafka 转发到 ClickHouse:
{
"name": "order-connector",
"config": {
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"database.hostname": "mysql",
"database.user": "root",
"database.password": "root",
"database.server.id": "184051",
"database.server.name": "order-server",
"database.include.list": "db0,db1,db2,db3", # 分库列表
"table.include.list": "db[0-3].t_order_[0-15]", # 分表列表(4库16表)
"database.history.kafka.bootstrap.servers": "kafka:9092",
"database.history.kafka.topic": "schema-changes.order",
"transforms": "unwrap,route",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter",
"transforms.route.regex": "order-server.(.*)",
"transforms.route.replacement": "clickhouse.order.$1" # 输出到 Kafka 主题
}
}
-- 创建 Kafka 引擎表(作为数据源)
CREATE TABLE order_kafka (
order_id UInt64,
user_id UInt64,
amount Decimal(10,2),
status UInt8,
create_time DateTime,
shard_key UInt8
) ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'kafka:9092',
kafka_topic_list = 'clickhouse.order.db0.t_order_0,clickhouse.order.db0.t_order_1,...', # 所有分表主题
kafka_group_name = 'clickhouse_consumer',
kafka_format = 'JSONEachRow'; # 按 JSON 格式解析
-- 创建物化视图(自动同步 Kafka 数据到 MergeTree 表)
CREATE MATERIALIZED VIEW order_consumer TO order_analysis
AS SELECT * FROM order_kafka;
在分库分表的业务操作中,通过消息队列异步同步数据到 ClickHouse:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper; // 分库分表的 MyBatis Mapper
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Transactional
public void createOrder(Order order) {
// 1. 写入分库分表(MySQL)
orderMapper.insert(order);
// 2. 异步发送到 Kafka(同步到 ClickHouse)
OrderEvent event = new OrderEvent();
event.setOrderId(order.getOrderId());
event.setUserId(order.getUserId());
event.setShardKey(order.getUserId() % 4); // 携带分库键
kafkaTemplate.send("order-sync", JSON.toJSONString(event));
}
}
3. 实时查询实现
@Service
public class OrderAnalysisService {
@Autowired
private ClickHouseTemplate clickHouseTemplate; // Spring Data ClickHouse
/**
* 实时查询:按用户ID统计订单金额(跨所有分库分表)
*/
public PageResult<UserOrderSummary> queryUserOrderSummary(
Long minUserId, Long maxUserId, int pageNum, int pageSize) {
// 1. 构建 ClickHouse 查询(分页+聚合)
String sql = "SELECT user_id, sum(amount) as total_amount, count(*) as order_count " +
"FROM order_analysis " +
"WHERE user_id BETWEEN ? AND ? " +
"GROUP BY user_id " +
"ORDER BY total_amount DESC " +
"LIMIT ? OFFSET ?";
// 2. 执行查询(ClickHouse 毫秒级响应)
List<UserOrderSummary> records = clickHouseTemplate.query(
sql,
new Object[]{minUserId, maxUserId, pageSize, (pageNum-1)*pageSize},
(rs, rowNum) -> new UserOrderSummary(
rs.getLong("user_id"),
rs.getBigDecimal("total_amount"),
rs.getLong("order_count")
)
);
// 3. 查询总条数(用于分页)
Long total = clickHouseTemplate.queryForObject(
"SELECT count(DISTINCT user_id) FROM order_analysis WHERE user_id BETWEEN ? AND ?",
new Object[]{minUserId, maxUserId},
Long.class
);
return new PageResult<>(records, total, pageNum, pageSize);
}
}
三、性能优化策略
1. ClickHouse 表优化
- 分区策略:按时间分区(如
toDate(create_time)
),查询时指定时间范围可大幅减少扫描数据量。 - 排序键:将高频查询的过滤字段(如
user_id
)和排序字段(如create_time
)设为ORDER BY
主键。 - 稀疏索引:调整
index_granularity
(默认 8192),高频查询表可设为 4096 提升索引效率。
2. 同步优化
- 批量写入:Kafka 消费者积累一定数量数据后批量写入 ClickHouse(如每 1000 条或 100ms 批量提交)。
- 避免重复同步:通过
order_id
作为唯一键,在 ClickHouse 中用ReplacingMergeTree
引擎自动去重:sqlCREATE TABLE order_analysis ( order_id UInt64, -- 其他字段... version UInt64 -- 版本号,用于去重 ) ENGINE = ReplacingMergeTree(version) -- 保留最新版本 PARTITION BY toDate(create_time) ORDER BY order_id;
3. 查询优化
- 预聚合:对高频聚合查询(如 “每日销售额”),用 ClickHouse 物化视图预计算结果:
sql
CREATE MATERIALIZED VIEW daily_sales_summary ENGINE = SummingMergeTree() PARTITION BY date ORDER BY date AS SELECT toDate(create_time) as date, sum(amount) as total_sales, count(*) as order_count FROM order_analysis GROUP BY date;
- 限制返回字段:只查询必要字段,避免
SELECT *
(列存引擎特性,减少数据传输)。
四、适用场景与优势
- 实时报表分析:如实时监控订单量、用户消费趋势,支持秒级更新。
- 跨分库聚合查询:如 “查询所有分库中用户的总消费金额”,避免分库分表的
UNION ALL
性能问题。 - 历史数据查询:分库分表只保留近 3 个月数据,ClickHouse 存储全量历史数据,支持跨年查询。
- 分库分表保障 OLTP 高并发,ClickHouse 保障 OLAP 实时分析。
- 同步延迟低(<1 秒),满足实时性要求。
- 水平扩展能力强:ClickHouse 支持集群部署,可随数据量增长扩容。
总结
- 表结构对齐(分库键冗余到 ClickHouse,便于关联)。
- 低延迟同步(CDC 方案适合生产环境)。
- ClickHouse 索引与分区优化(提升查询效率)。
- StarRocks
- 性能:在近似硬件环境下,StarRocks 的平均查询速度比 ClickHouse 提高了 200%-1600%。这主要得益于其基于 pipeline 的查询执行引擎,能够实现高效的查询处理和并发控制,同时 StarRocks 支持物化视图,可以在数据写入时自动完成上层的细粒度聚合,从而降低查询延迟。
- 数据模型:与 ClickHouse 类似,StarRocks 也是列式存储数据库,适合数据分析场景。但 StarRocks 在实时分析增强方面更具优势,能更好地处理动态数据。
- 适用场景:适用于对查询速度要求极高,且需要处理实时和动态数据的全场景 OLAP 业务,如企业的实时决策分析、复杂的业务报表生成等。
- Doris
- 核心架构:采用 FE(前端)与 BE(后端)分离的 MPP 架构,FE 负责元数据管理和查询规划,BE 处理存储与计算,支持自动扩缩容和故障恢复,集群管理相对简单。而 ClickHouse 是去中心化架构,依赖 ZooKeeper 协调分布式表和数据分片,大规模集群运维复杂度较高。
- 数据一致性:Doris 支持同步更新和删除操作,通过主键模型实现行级实时一致性。ClickHouse 的更新 / 删除为异步操作,依赖后台 Merge 任务,可能导致短暂数据不一致。
- 查询特性:Doris 在复杂查询场景下表现出色,优化了多表 Join 和聚合分析,适合 BI 工具的高并发即席查询,能达到亚秒级响应。ClickHouse 则在单表聚合、过滤场景性能卓越,批量导入速度更快,更适合离线批处理场景。
- SQL 兼容性:Doris 高度兼容 MySQL 协议和标准 SQL,降低了学习成本。ClickHouse 使用自有 SQL 方言,部分功能需特定语法实现,对 MySQL 用户存在适配门槛。
- Druid
- 核心优势:Druid 是分布式、列导向的实时分析数据存储,擅长时序 + 维度混合分析,支持亚秒级查询和全文检索。它在数据仓库场景中,对于 PB 级数据集的快速聚合查询表现良好,还支持各种灵活的过滤器、精确计算、近似算法等。
- 与 ClickHouse 对比:ClickHouse 更专注于纯粹的 OLAP 场景,在大规模数据集的复杂聚合查询和实时分析任务上有出色表现,其列式存储和压缩机制提供了高效的 I/O 性能。而 Druid 在时序数据与维度数据结合分析以及全文检索方面更具特色,适用于需要快速响应和探索性分析的场景,如实时监控仪表盘、广告投放效果分析等。
选择一款合适的OLAP(在线分析处理)数据库对项目至关重要。下面我将几款主流的开源OLAP引擎,包括 ClickHouse、StarRocks、Apache Doris、DuckDB,以及 ByConity 等进行对比,并从多个维度为你分析,助你更好地决策。
先来看看一个核心特性的对比表格,帮你快速了解它们的不同:
特性维度 | ClickHouse | StarRocks | Apache Doris | DuckDB | ByConity |
---|---|---|---|---|---|
核心架构 | 列式存储,多主架构 | MPP架构,向量化执行引擎 | MPP架构 | 嵌入式列式分析数据库 | 云原生,存储计算分离 |
数据模型 | 宽表模型为主 | 支持明细、聚合、更新模型 | 支持多模型(明细/聚合/更新) | 支持多表Join和复杂查询 | 支持主流OLAP优化技术 |
SQL兼容性 | 类SQL(部分语法差异) | 兼容MySQL协议 | 近乎完整SQL兼容 | 支持标准SQL,兼容多种数据源 | 支持TPC-DS复杂查询 |
查询性能特点 | 单表查询极快,擅长聚合分析和时序数据 | 多表关联和高并发查询能力强 | 高并发点查+复杂分析均衡,实时更新强 | 单机复杂查询性能优异,特别是在中等数据量(<100GB)下 | 在复杂查询(尤其是多表关联和窗口函数)场景下表现优异 |
更新与事务 | 更新成本高,异步Mutation或通过特定Engine实现;事务支持弱 | 支持实时更新和强事务(ACID) | 支持轻量级事务(如主键更新) | 支持完整ACID事务 | 支持数据读写的强一致性 |
扩展性 | 水平扩展,配置简单 | 在线弹性扩缩容 | 线性扩展,节点管理完善 | 嵌入式,无扩展性,但支持磁盘溢出 | 支持弹性扩缩容和租户资源隔离 |
运维复杂度 | 中等(需调优配置) | 不依赖于大数据生态 | 运维较友好 | 极简(无需独立服务) | 云原生架构,易于运维 |
典型应用场景 | 日志分析、用户行为分析、时序数据处理、实时报表 | 实时多维分析、高并发BI报表、湖仓一体 | 实时BI报表、交互式分析、实时数仓 | 嵌入式分析、数据科学、本地开发、中小规模数据处理 | 云原生数据仓库,适合复杂分析场景 |
✨ 选型小结
你可以根据以下场景来快速匹配:
-
ClickHouse:适合单表查询为主、大规模日志/时序数据分析、写入吞吐量要求极高,且对实时更新和复杂事务需求不强的场景。
-
StarRocks:适合多表关联查询复杂、高并发点查和聚合分析并存、需要强实时性和事务支持的实时数仓和BI报表系统。
-
Apache Doris:与StarRocks定位相似(Doris是StarRocks早期分支),同样适合高并发的交互式分析和实时数据更新场景,兼容性好。
-
DuckDB:它是本地开发、数据探索、嵌入式应用的绝佳选择。当你需要在单机上快速分析GB到百GB级数据,并与Python/R生态紧密集成时,选它准没错。
-
ByConity:适合追求云原生架构、需要处理极其复杂的分析查询(特别是多表Join和窗口函数),并重视弹性扩展和资源隔离的企业级用户。
💡 决策注意事项
-
数据规模与增长:超大规模数据(PB级)且以插入和查询为主,ClickHouse的性价比可能更高。若数据模型复杂、需频繁关联和更新,则需考虑StarRocks/Doris或ByConity。
-
并发需求:高并发(数千QPS)是StarRocks和Doris的强项。ClickHouse在高并发下需要额外设计(如扩容副本或前置聚合结果到MySQL等)。
-
开发生态与习惯:若团队熟悉MySQL协议,StarRocks和Doris上手更快。DuckDB则能无缝集成到数据科学工作流中。
-
云原生与运维:若计划部署在云上,并希望弹性伸缩、降低运维成本,ByConity的存储计算分离架构值得考虑。ClickHouse和Doris也支持云部署,但ByConity为此做了更多原生设计。
-
混合架构:没有银弹。常见模式如使用 HBase 处理高并发实时读写,用 ClickHouse 分析历史行为数据,或借助 DuckDB 在数据应用内进行轻量级分析。
🧭 总结
选择OLAP数据库就像是“因材施教”,关键看你的“材”是什么:
-
处理日志、事件流等大宽表场景,ClickHouse 性能卓越。
-
需要灵活的模型、复杂的多表关联和高并发查询,StarRocks 或 Apache Doris 更胜任。
-
进行数据探索、本地开发或嵌入式应用,DuckDB 便捷高效。
-
构建云原生数仓,处理超复杂分析,ByConity 展现了潜力。
希望这些信息能帮助你。如果你能分享更多关于你具体业务场景的信息(比如数据规模、查询模式、并发量期望等),我很乐意提供更具体的建议。
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------