SpringBoot集成ShardingSphere分表中间件
ShardingSphere简介
ShardingSphere 由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的基于数据库作为存储节点的增量功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
关系型数据库当今依然占有巨大市场份额,是企业核心系统的基石,未来也难于撼动,我们更加注重在原有基础上提供增量,而非颠覆。
ShardingSphere-JDBC
定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
-
适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC;
-
支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, HikariCP 等;
-
支持任意实现 JDBC 规范的数据库,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 访问的数据库。
总结 兼容性好 , 目前主流的ORM框架均能够支持、而且只需要在pom文件中引入依赖即可、使用起来非常方便。
查看更多关于分库分表、读写分离:https://mp.weixin.qq.com/s/aFXZ8rT9g4oj3ZnioC2vkg
添加依赖
<!-- 关系型数据库中间件sharding -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.1</version>
</dependency>
配置数据源
@Configuration
public class DruidConfig{
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource)
{
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
setDataSource(targetDataSources, DataSourceType.SHARDING.name(), "shardingDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources);
}
}
/**
* 数据源
*/
public enum DataSourceType
{
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE,
/**
* 分表
*/
SHARDING
}
/**
* 在Mapper类中配置分表数据源
*/
@DataSource(DataSourceType.SHARDING)
public interface TestMapper {
// 针对分表的所有增删改查操作,都要重新写对应的SQL语句,否则直接mybatis接口不会生效
}
添加yml配置
spring:
main:
# 一个实体类对应多张表,必须设置这个
allow-bean-definition-overriding: true
shardingsphere:
props:
sql:
# 打印SQL
show: false
datasource:
names: master
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/times_tool?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
sharding:
tables:
ts_dir_oss:
# 配置表规则
actual-data-nodes: master.ts_dir_oss_$->{2013..2023}_$->{(1..12).collect{t ->t.toString().padLeft(2,'0')}}_$->{(1..2).collect{t ->t.toString().padLeft(2,'0')}}
# 配置主键id生成策略,指定雪花算法
key-generator:
column: id
type: SNOWFLAKE
table-strategy:
standard:
# 分片字段
sharding-column: point_time
# 标准策略 + 精确分片算法 SQL就是(=或in)
precise-algorithm-class-name: com.cn.framework.datasource.PointTimePreciseShardingAlgorithm
# 标准策略 + 范围分片算法 主要是(between A and B)
range-algorithm-class-name: com.cn.framework.datasource.PointTimeRangeShardingAlgorithm
# 复合分片策略 提供对SQL语句中的(=,in,beteewn A and B)的分片操作支持
# complex:
# sharding-columns: point_time
# algorithm-class-name: com.cn.framework.datasource.ComplexShardingAlgorithm
代码实现:一张表存储半月数据
复合分片策略
/**
* 复合分片策略 提供对SQL语句中的(=,in,beteewn A and B)的分片操作支持.
*/
@Slf4j
public class ComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm {
@Override
public Collection<String> doSharding(Collection collection, ComplexKeysShardingValue complexKeysShardingValue) {
Set<String> tables = new HashSet<>();
Map<String, Range<String>> rangeMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
if(CollUtil.isNotEmpty(rangeMap)){
for(String s : rangeMap.keySet()){
Range<String> valueRange = rangeMap.get(s);
if(valueRange!=null){
Date begin = DateUtil.parse(valueRange.lowerEndpoint(), DateUtils.YYYY_MM);
Date end = DateUtil.parse(valueRange.upperEndpoint(),DateUtils.YYYY_MM);
log.info("lowerSuffix:{},upperSuffix:{}",begin,end);
for(Object tab:collection){
String tableName = (String)tab;
String dateStr = StrUtil.subAfter(tableName, "ts_dir_oss_", true).replace("_", "-");
Date date = DateUtil.parse(dateStr,DateUtils.YYYY_MM);
if((date.after(begin) || date.equals(begin)) && (date.before(end) || date.equals(end) )){
tables.add(tableName);
}
}
}
}
}
Map<String, List<String>> map = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
if(CollUtil.isNotEmpty(map)){
for(String s : map.keySet()){
List<String> list = map.get(s);
if(list.size()>0){
String v = list.get(0);
Date date = DateUtil.parseDate(v);
String suffix = getSuffixByDate(date);
for(Object tab: collection){
String tableName = (String)tab;
if(tableName.endsWith(suffix)){
tables.add(tableName);
}
}
}
}
}
log.info("match tableNames:{}", tables.toString());
return tables;
}
public String getSuffixByDate(Date date){
String dateStr = DateUtil.format(date,"yyyy_MM");
int day = DateUtil.date(date).dayOfMonth();
if(day<=15) {
return dateStr + "_01";
}else{
return dateStr + "_02";
}
}
}
标准策略+精确分片算法
/**
* 标准策略 + 精确分片算法 SQL就是(=或in)
*/
@Slf4j
public class PointTimePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> availableTargetName, PreciseShardingValue<String> preciseShardingValue) {
Date date = DateUtil.parseDate(preciseShardingValue.getValue());
String suffix = getSuffixByDate(date);
for(String tabName: availableTargetName){
if(tabName.endsWith(suffix)){
log.info("match tableName:{}", tabName);
return tabName;
}
}
throw new IllegalArgumentException("未找到匹配的数据表");
}
public String getSuffixByDate(Date date){
String dateStr = DateUtil.format(date,"yyyy_MM");
int day = DateUtil.date(date).dayOfMonth();
if(day<=15) {
return dateStr + "_01";
}else{
return dateStr + "_02";
}
}
}
标准策略+范围分片算法
/**
* 标准策略 + 范围分片算法 主要是(between A and B)
*/
@Slf4j
public class PointTimeRangeShardingAlgorithm implements RangeShardingAlgorithm<String> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetName, RangeShardingValue<String> rangeShardingValue) {
Range<String> valueRange = rangeShardingValue.getValueRange();
Date begin = DateUtil.parse(valueRange.lowerEndpoint(), DateUtils.YYYY_MM);
Date end = DateUtil.parse(valueRange.upperEndpoint(),DateUtils.YYYY_MM);
log.info("lowerSuffix:{},upperSuffix:{}",begin,end);
Set<String> tables = new HashSet<>();
for(String tableName:availableTargetName){
String dateStr = StrUtil.subAfter(tableName, "ts_dir_oss_", true).replace("_", "-");
Date date = DateUtil.parse(dateStr,DateUtils.YYYY_MM);
if((date.after(begin) || date.equals(begin)) && (date.before(end) || date.equals(end) )){
tables.add(tableName);
}
}
log.info("match tableNames:{}", tables.toString());
return tables;
}
}
动态新增数据表脚本
CREATE TABLE IF NOT EXISTS ts_dir_oss_${tableSuffix} (
`id` BIGINT(20) AUTO_INCREMENT NOT NULL,
`nodes_id` bigint(20) DEFAULT NULL COMMENT '目录节点id',
`point_id` varchar(48) DEFAULT NULL COMMENT '测点名称',
`point_time` varchar(18) DEFAULT NULL COMMENT '测点时间',
`path` varchar(225) DEFAULT NULL COMMENT '地址',
`size` bigint(11) DEFAULT NULL COMMENT '文件大小',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_point_time` (`point_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
动态更新分片策略
@Component
public class ShardingTableRuleActualTablesRefresh {
private Logger logger = LoggerFactory.getLogger(ShardingTableRuleActualTablesRefresh.class);
@Resource(name = "shardingDataSource")
private DataSource dataSource;
@PostConstruct
public void initData(){
try {
List<String> sdList = new ArrayList<>();
// 启动项目时初始化分片规则,查出分片配置表信息
actualTablesRefresh(sdList);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 动态刷新变量表的分片策略, 核心通过反射更新tableRule
* @param idList
*/
public void actualTablesRefresh(List<String> sdList) {
try {
logger.info("-----开始刷新表节点-----");
if(CollUtil.isEmpty(idList)){
logger.error("传入idList参数为空");
return;
}
ShardingDataSource shardingDataSource = (ShardingDataSource)dataSource;
//运行时获取分片规则
ShardingRule rule = shardingDataSource.getRuntimeContext().getRule();
//获取分表策略集合
Collection<TableRule> tableRules = rule.getTableRules();
for (TableRule tableRule : tableRules) {
//获取真实节点
List<DataNode> actualDataNodes = tableRule.getActualDataNodes();
Field actualDataNodesField = TableRule.class.getDeclaredField("actualDataNodes");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(actualDataNodesField, actualDataNodesField.getModifiers() & ~Modifier.FINAL);
//数据源名
String dataSourceName = actualDataNodes.get(0).getDataSourceName();
//逻辑表名
String logicTableName = tableRule.getLogicTable();
//根据真实业务,新增节点的逻辑
for (String sd : sdList) {
actualDataNodes.add(new DataNode(dataSourceName+"."+logicTableName+"_" + sd));
}
actualDataNodesField.setAccessible(true);
actualDataNodesField.set(tableRule, actualDataNodes);
Set<String> actualTables = Sets.newHashSet();
Map<DataNode, Integer> dataNodeIntegerMap = Maps.newHashMap();
//更新actualTables、dataNodeIntegerMap
AtomicInteger a = new AtomicInteger(0);
actualDataNodes.forEach((dataNode -> {
actualTables.add(dataNode.getTableName());
if (a.intValue() == 0){
a.incrementAndGet();
dataNodeIntegerMap.put(dataNode, 0);
}else {
dataNodeIntegerMap.put(dataNode, a.intValue());
a.incrementAndGet();
}
}));
//动态刷新:actualTables
Field actualTablesField = TableRule.class.getDeclaredField("actualTables");
actualTablesField.setAccessible(true);
actualTablesField.set(tableRule, actualTables);
//动态刷新:dataNodeIndexMap
Field dataNodeIndexMapField = TableRule.class.getDeclaredField("dataNodeIndexMap");
dataNodeIndexMapField.setAccessible(true);
dataNodeIndexMapField.set(tableRule, dataNodeIntegerMap);
//动态刷新:datasourceToTablesMap
Map<String, Collection<String>> datasourceToTablesMap = Maps.newHashMap();
datasourceToTablesMap.put(dataSourceName, actualTables);
Field datasourceToTablesMapField = TableRule.class.getDeclaredField("datasourceToTablesMap");
datasourceToTablesMapField.setAccessible(true);
datasourceToTablesMapField.set(tableRule, datasourceToTablesMap);
logger.info("-----------------end----------------");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意事项
- sharding不支持部分复杂的聚合查询,如果要有统计数据需求,可以用一个统计表实时或定时实现数据同步。
- 数据表需要根据切片规则提前创建好,否则程序会报错提示找不到指定表。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步