一、 需求概述
在使用 Mysql 数据库存储设备上报日志时,存在一张设备实时状态表,随着时间推移,数据量变得十分庞大。为了更好地管理和查询数据,提高数据库性能,需要对该表进行水平分表操作。同时,存在分页查询的需求,不过仅在针对单个设备状态查询时才需要分页展示结果,以方便查看设备在不同时间段的状态信息,避免一次性返回大量数据影响性能和使用体验。
二、分表键的选择策略详解
1. 哈希取模分片
哈希取模分片是常用的水平分表策略,通过对选定的分片键(如设备编号)进行哈希运算后取模,确定数据存储的分表。
-
原理及优势:
- 均匀分布数据:对于设备实时状态表这种大数据量且设备众多的情况,能让数据均匀分散到各分表。例如,假设有 10 张分表,对设备编号哈希取模 10,不同设备的状态日志可均衡落入这 10 张表,避免数据倾斜,使各表数据量相近,查询时各分表负载均衡,提升数据库整体性能。
- 简单高效的路由:查询时按相同哈希取模规则,可快速定位对应分表。比如查询某个设备状态日志,经设备编号哈希取模运算,就能知晓去哪个分表获取数据,减少全表扫描和复杂查找逻辑,尤其适用于单个设备状态查询场景。
-
缺点及注意事项:
- 扩容复杂:业务发展需增加分表数量时(如从 10 张扩到 20 张),哈希取模规则改变,原本数据分布打乱,需进行数据迁移来重新平衡各表数据,操作复杂耗时,涉及大量数据读写和系统调整,所以规划分表数量初期要考虑未来业务增长规模,预留扩展空间。
- 哈希冲突:虽然哈希算法通常能保证唯一性,但理论上存在不同设备编号哈希计算后结果相同(取模后也相同)的情况,即哈希冲突。不过实际应用中,选择合适哈希函数(如 MD5、SHA 等或数据库自带算法)可将冲突概率降至极低,开发人员仍需在代码中考虑应对冲突,比如增加额外处理逻辑区分冲突记录。
例如,在 Spring Boot + MyBatis 手动分表时,按以下代码逻辑实现哈希取模确定分表(以设备编号后三位数字简单取模为例,实际可采用更严谨算法):
public class TableShardingUtil {private static final int TABLE_COUNT = 10; // 假设分表数量为 10public static String getTableNameByDeviceId(String deviceId) {int deviceIdSuffix = Integer.parseInt(deviceId.substring(deviceId.length() - 3)); // 获取设备编号后三位并转为整数int tableIndex = deviceIdSuffix % TABLE_COUNT; // 取模确定分表索引return "device_status_" + String.format("%03d", tableIndex); // 构建分表名,格式化为三位数字,如 device_status_001}
}
在 MyBatis 的 SQL 语句中利用该方法构建动态表名(XML 映射文件中):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.DeviceStatusMapper"><select id="getDeviceStatusByDeviceIdPage" resultMap="DeviceStatusResultMap">SELECT * FROM #{tableName} <!-- 这里使用动态表名 -->WHERE device_id = #{deviceId}LIMIT #{offset}, #{limit}</select>
</mapper>
对应的 Mapper 接口方法传入计算得到的表名:
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;@Mapper
public interface DeviceStatusMapper {List<DeviceStatus> getDeviceStatusByDeviceIdPage(@Param("tableName") String tableName, // 新增表名参数@Param("deviceId") String deviceId,@Param("offset") int offset,@Param("limit") int limit);
}
业务逻辑层调用时先算出表名再传递给 Mapper 方法查询:
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;@Service
public class DeviceStatusService {@Resourceprivate DeviceStatusMapper deviceStatusMapper;public List<DeviceStatus> getDeviceStatusByDeviceIdPage(String deviceId, int pageNum, int pageSize) {String tableName = TableShardingUtil.getTableNameByDeviceId(deviceId);int offset = (pageNum - 1) * pageSize;return deviceStatusMapper.getDeviceStatusByDeviceIdPage(tableName, deviceId, offset, pageSize);}
}
通过这种方式实现基于哈希取模的分片键策略,动态依据设备编号确定分表并进行分页查询操作。
2. 范围分片
-
原理及优势:
- 按业务逻辑自然划分:依据数据的某个范围属性来划分分表,像按时间范围(如按天、月、年等)对设备实时状态表分表就是典型的范围分片。这种方式契合按时间段查询数据的业务习惯,查询特定时间段内设备状态日志时,可直接定位对应时间范围分表,减少不必要数据检索,提高查询效率,且对基于时间序列的数据分析、历史数据归档等操作更便捷,数据组织形式直观易懂。
- 易于数据管理和维护:进行数据清理、备份等操作时,基于范围分片能按时间等范围属性方便地批量处理分表数据,比如定期清理久远时间范围分表中的过期数据,不影响其他活跃时间段的数据表。
-
缺点及注意事项:
- 可能出现数据倾斜:若业务数据在某些范围内分布不均,易导致数据倾斜。比如某些时间段设备上报状态日志多,有些时间段少,对应的分表数据量差异大,查询时各分表负载不均衡,影响整体性能。所以选择范围分片时,要充分考虑业务数据在该范围属性上的分布特点,必要时结合其他策略缓解数据倾斜,如细分范围或配合哈希取模均匀分布数据。
- 跨表查询需求处理复杂:涉及跨越多个范围分表查询(如查询一个设备较长时间跨度内状态,跨越多个月分表)时,需编写复杂查询逻辑整合多表数据,不像单表查询简单直接,要特别注意处理分表连接、数据去重等问题,避免数据不一致或查询结果不准确。
3. 一致性哈希分片
-
原理及优势:
- 数据分布相对稳定:一致性哈希是特殊哈希算法,面对节点(分表可看作节点)增减时,相比普通哈希取模,能最大程度减少数据迁移量。例如在分布式数据库环境中,新增或减少分表数量时,一致性哈希可保证只有少部分数据需重新分配到新分表,使数据分布在动态变化场景下保持相对稳定,减少对业务影响。
- 可扩展性较好:对于业务发展、数据量增长需不断扩充分表的情况,一致性哈希分片能更平滑适应变化,降低分表扩展带来的运维成本和数据调整难度,提高系统整体可扩展性,更利于应对复杂多变的业务需求。
-
缺点及注意事项:
- 实现相对复杂:一致性哈希算法原理和实现比普通哈希取模复杂,要求开发人员有更深入理解和专业编程能力进行代码实现与部署,增加开发和维护难度。在一些对可扩展性要求不高的简单应用场景,使用一致性哈希可能增加不必要复杂度。
- 存在数据倾斜风险:虽然一致性哈希能一定程度均匀分配数据,但在极端情况(如节点分布不均或数据哈希值分布有偏差)下,也可能出现数据倾斜,导致部分分表负载过重,影响查询性能,实际应用中需关注数据分布并适当优化。
三、水平分表及分页查询实现示例
(一)数据库表结构设计
1.设备实时状态表结构(分表前)
假设设备上报的日志主要包含设备的基本信息、状态信息以及上报时间等内容,以下是一个简单的表结构设计示例:
字段名 | 类型 | 说明 | 是否可空 | 主键 |
---|---|---|---|---|
id | bigint | 自增唯一标识,每条日志记录的唯一编号 | 否 | 是 |
device_id | varchar(50) | 设备编号,用于唯一标识每一台设备 | 否 | 否 |
device_name | varchar(100) | 设备名称,方便直观了解设备情况 | 否 | 否 |
status_code | int | 设备状态码,不同数值代表不同的运行状态,例如 0 表示正常,1 表示故障等 | 否 | 否 |
status_detail | text | 设备状态详细描述,比如故障具体原因等信息 | 是 | 否 |
report_time | datetime | 设备上报该状态的时间 | 否 | 否 |
other_info | varchar(255) | 其他可能的补充信息,如设备所在位置等(可根据实际情况扩展) | 是 | 否 |
在这个表结构中,id
作为主键保证每条记录的唯一性,便于数据的索引和管理。而 device_id
是区分不同设备的关键字段,后续水平分表就会基于它来进行操作,report_time
用于记录状态上报的时间点,方便后续按时间维度查询和分析设备状态变化情况等。
2.分表后的表结构
根据 device_id
作为分表键进行水平分表,分表后的每张表结构与原始表结构基本一致,只是数据根据分表规则分散到了不同的表中。
例如,假设按照设备编号对 10 取模的方式将数据分到 10 张表中,表名可以分别命名为 device_status_0
、device_status_1
、device_status_2
…… device_status_9
。
以 device_status_0
为例,其表结构如下:
字段名 | 类型 | 说明 | 是否可空 | 主键 |
---|---|---|---|---|
id | bigint | 自增唯一标识,每条日志记录的唯一编号 | 否 | 是 |
device_id | varchar(50) | 设备编号,用于唯一标识每一台设备 | 否 | 否 |
device_name | varchar(100) | 设备名称,方便直观了解设备情况 | 否 | 否 |
status_code | int | 设备状态码,不同数值代表不同的运行状态,例如 0 表示正常,1 表示故障等 | 否 | 否 |
status_detail | text | 设备状态详细描述,比如故障具体原因等信息 | 是 | 否 |
report_time | datetime | 设备上报该状态的时间 | 否 | 否 |
other_info | varchar(255) | 其他可能的补充信息,如设备所在位置等(可根据实际情况扩展) | 是 | 否 |
其他 device_status_1
到 device_status_9
等表结构均与之相同,只是每张表中存储的数据是根据 device_id
取模规则分配过来的对应设备的状态日志信息。
(二)使用 springboot + mybatis 手动水平分表并实现分页
- 分表设计:
首先要确定分表键,对于设备实时状态表来说,设备编号(device_id)是比较合适的分表键选择。因为往往是针对单个设备的操作和查询较多,以设备编号进行分表能让同一设备的数据集中存储在一张分表中,方便后续查询和管理。可以按照一定规则,比如根据设备编号对分表数量取模的方式,将数据均匀分散到不同的分表中,例如有 10 张分表,设备编号为 1001 的设备,通过 1001 % 10 确定其存储在对应的分表中。 - 代码实现:
在 Spring Boot 项目中,配置好 MyBatis 相关依赖和数据库连接信息。创建针对不同分表的 Mapper 接口和对应的 XML 映射文件。在查询单个设备状态并分页时,需要在 Mapper 接口中定义相应的方法,例如:
List<DeviceStatus> getDeviceStatusByPage(@Param("deviceId") String deviceId, @Param("offset") int offset, @Param("limit") int limit);
在 XML 映射文件中编写 SQL 语句,通过传入的设备编号确定要查询的分表,结合传入的偏移量(offset)和每页数量(limit)来实现分页查询,示例 SQL 如下:
SELECT * FROM device_status_${deviceId % 10}
WHERE device_id = #{deviceId}
LIMIT #{offset}, #{limit};
在 Service 层调用该 Mapper 方法,传入相应参数即可实现单个设备状态的分页查询,通过这种手动方式灵活控制分表和分页逻辑,但需要自行处理较多的细节,如分表规则的维护等。
(三)使用 springboot + sharing-jdbc + mybatis 实现水平分表并分页查询
- 分表配置:
同样选择设备编号(device_id)作为分表键。在 Spring Boot 项目中引入 Sharding-JDBC 相关依赖,然后通过配置文件(如 application.yml)进行分表规则配置。例如:
sharding:tables:device_status:actual-data-nodes: device_status_$->{0..9}.device_statustable-strategy:inline:sharding-column: device_idalgorithm-expression: device_status_$->{device_id % 10}
这样 Sharding-JDBC 会按照配置的规则自动根据设备编号对数据进行分表存储。
2. 分页查询实现:
在 MyBatis 的 Mapper 接口中定义查询方法,和上面类似,例如:
List<DeviceStatus> getDeviceStatusByPageWithSharding(@Param("deviceId") String deviceId, @Param("offset") int offset, @Param("limit") int limit);
在 XML 映射文件中编写 SQL 语句时,无需像手动分表那样关注具体分表的选择,只需要按照常规的查询语法编写,Sharding-JDBC 会在底层根据配置的分表规则自动路由到正确的分表上进行查询并实现分页,示例 SQL 如下:
SELECT * FROM device_status
WHERE device_id = #{deviceId}
LIMIT #{offset}, #{limit};
在 Service 层调用该方法即可轻松实现单个设备状态的分页查询,Sharding-JDBC 帮助简化了分表相关的很多复杂操作,提高了开发效率。