相信有很多朋友在做分库分表时,会考虑使用ShardingJDBC来解决分页、排序等问题,但是做的时候发现网上很多教程包括官网的教程大多都是静态配置的,都是直接在配置项中写明分表策略,事先定义创建好需要的表,tb1,tb2,tb3这样。但是实际的工作中往往都是要求动态变化的,那总不能每次新增表了就去修改下代码,重启下服务吧!!
如当前分表策略是tb_20200106、tb_20200113、tb_20200120,当我新增表tb_20200127时,难度需要去修改代码,然后重启服务吗,这总感觉有点傻!
而使用官方提供的表达式配置方法:
ds 0..1. t o r d e r {0..1}.t_order 0..1.torder{0…1}
也是需要先创建好表,否则会报错!
通过自己摸索,最终实现了动态配置,当然方法有很多种,本人只是一只菜鸟,做做搬运工,希望对后面的兄弟姐妹们提供一点点帮助吧!
定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
db1,db2是分库后的两个业务数据库,db2中涉及分表操作,所以用ShardingJDBC进行封装,实现对db2的分页,排序查询。
按数据产生的时间分区,分区字段:
表后缀日期为当周周一的日期,一周的数据存入本周的表中。新表在前一周周末晚上通过定时任务创建,这里实现方式有很多,我是写的存储过程(创建新表,因为不想保存很久之前的数据,所以只留半年的数据,因此当表超过24张,即24个周的数据,则还要备份后删除掉最早的表。),然后quartz起定时任务调用存储过程。这部分可以直接用JAVA实现建表删表逻辑,不需要和我一样写存储过程哈…
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>2.0.3</version>
</dependency>
## 业务数据库1
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false
jdbc.username=root
jdbc.password=root
## 业务数据库2
bs.jdbc.driverClassName=com.mysql.jdbc.Driver
bs.jdbc.url=jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false
bs.jdbc.username=root
bs.jdbc.password=root
<!-- 数据源1 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClassName}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 数据源2 -->
<bean id="deviceDataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass"
value="${bs.jdbc.driverClassName}" />
<property name="jdbcUrl" value="${bs.jdbc.url}" />
<property name="user" value="${bs.jdbc.username}" />
<property name="password" value="${bs.jdbc.password}" />
</bean>
<!-- ShardingJDBC在数据源2基础上封装的数据源 -->
<bean id="shardingDataSource" class="com.fpi.cloud.shardingjdbc.ShardingJDBCDataSourceFactory">
</bean>
<!-- 多数据源管理 -->
<bean id="myDataSource"
class="com.fpi.cloud.utils.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- 指定lookupKey和与之对应的数据源 -->
<entry key="ds1" value-ref="dataSource"></entry>
<entry key="ds2" value-ref="deviceDataSource"></entry>
<entry key="sharding" value-ref="shardingDataSource"></entry>
</map>
</property>
<!-- 这里可以指定默认的数据源 -->
<property name="defaultTargetDataSource" ref="dataSource" />
</bean>
<!-- sessionFactory 将spring和mybatis整合 -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 关联多数据源,用于动态切换数据源 -->
<property name="dataSource" ref="myDataSource" />
<property name="configLocation"
value="classpath:mybatis-config.xml" />
<property name="mapperLocations"
value="classpath*:/Mapper/*.xml" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.fpi.cloud.dao" />
<property name="sqlSessionFactoryBeanName"
value="sqlSessionFactory" />
</bean>
通过ShardingJDBC封装原来的数据源2(因为数据源1没有分表,只有数据源2有分表),返回ShardingDataSource,再交给Spring进行多数据源管理。
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import com.fpi.cloud.utils.RedisUtils;
import io.shardingjdbc.core.api.config.ShardingRuleConfiguration;
import io.shardingjdbc.core.api.config.strategy.StandardShardingStrategyConfiguration;
import io.shardingjdbc.core.jdbc.core.datasource.ShardingDataSource;
/**
*
* @ClassName: ShardingJDBCDataSourceFactory
* @Description: shardingJDBCSource Config
* @author: luchenxi
* @date: 2019年11月12日 下午7:14:02
*
*
*/
public class ShardingJDBCDataSourceFactory implements FactoryBean<ShardingDataSource>
{
private static Logger logger = LogManager.getLogger(ShardingJDBCDataSourceFactory.class);
// 注入数据源2
@ Autowired
DataSource deviceDataSource;
/*@ Autowired
TableRuleConfiguration table9000SuperRuleConfiguration;
@ Autowired
TableRuleConfiguration table8000SuperRuleConfiguration;*/
// 创建ShardingDataSource
@ Override
public ShardingDataSource getObject() throws Exception
{
logger.info("ShardingJDBC DataSource 初始化 ...");
// 配置真实数据源
Map<String , DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("deviceDataSource", deviceDataSource);
// 配置shardingJDBC
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
// 分表规则
shardingRuleConfig.getTableRuleConfigs().add(TablRuleConfigFactory.getRuleConfig());
// 分表绑定
shardingRuleConfig.getBindingTableGroups().add("paramsampling");
shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(
"gmt_create", SuperTableShardingAlgorithm.class.getName()));
ShardingDataSource rDataSource = new ShardingDataSource(shardingRuleConfig.build(dataSourceMap));
return rDataSource;
}
@ Override
public Class<ShardingDataSource> getObjectType()
{
return ShardingDataSource.class;
}
@ Override
public boolean isSingleton()
{
return true;
}
/**
*
* @Title: getActualTables @Description: 获取redis缓存的表名 @param: @param
* key @param: @return @return: String @throws
*/
public static String getActualTables(String key)
{
// 获取目前存在的对应分表的后缀
List<String> at = RedisUtils.range(key);
StringBuilder tBuilder = new StringBuilder();
at.forEach(t -> {
tBuilder.append(t);
tBuilder.append(",");
});
if(tBuilder.length() > 0)
{
return tBuilder.substring(0, tBuilder.length() - 1);
}
return tBuilder.toString();
}
}
实际的表名日期后缀(20191111,20191118)存在Redis中,配置的时候取出组合成策略。
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.fpi.cloud.contants.RedisKey;
import io.shardingjdbc.core.api.config.TableRuleConfiguration;
public class TableRuleConfigFactory
{
private static Logger logger = LogManager.getLogger(TableRuleConfigFactory.class);
/**
*
* @Title: getRuleConfig @Description:
* 分表策略 @param: @return @return:
* TableRuleConfiguration @throws
*/
public static TableRuleConfiguration getRuleConfig()
{
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration();
// 期望的基础表名
orderTableRuleConfig.setLogicTable("paramsampling");
// 配置实际的表
String actualTbs = ShardingJDBCDataSourceFactory
.getActualTables(RedisKey.SHARDINGDBNAME);
// String actualTbs = "20191111,20191118,20191125";
logger.info("分表策略:" + actualTbs);
orderTableRuleConfig
.setActualDataNodes("deviceDataSource.paramsampling_${[" + actualTbs + "]}");
// 分表依据字段
orderTableRuleConfig.setKeyGeneratorColumnName("gmt_create");
// logger.info("初始化:" + orderTableRuleConfig.getLogicTable());
return orderTableRuleConfig;
}
import java.util.Collection;
import java.util.Date;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.fpi.cloud.utils.DateUtils;
import io.shardingjdbc.core.api.algorithm.sharding.PreciseShardingValue;
import io.shardingjdbc.core.api.algorithm.sharding.standard.PreciseShardingAlgorithm;
/**
*
* @ClassName: Super9000TableShardingAlgorithm
* @Description:自定义分片算法
* @author:luchenxi
* @date: 2019年11月13日 上午10:02:30
*
*/
public class SuperTableShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
private Logger logger = LogManager.getLogger(SuperTableShardingAlgorithm.class);
/**
*
*
* Title: doSharding
*
*
* Description: 配置分区规则,查询指定时间的需要去所在周周一的日期后缀表中查询
*
*
* @param availableTargetNames
* @param shardingValue
* @return
* @see io.shardingjdbc.core.api.algorithm.sharding.standard.PreciseShardingAlgorithm#doSharding(java.util.Collection,
* io.shardingjdbc.core.api.algorithm.sharding.PreciseShardingValue)
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
logger.info("分表策略生效...");
Date creatTime = shardingValue.getValue();
if (creatTime != null) {
// 查询所在周的周一日期后缀
String mondayStr = DateUtils.getWeekMonday(creatTime);
logger.info("目标表后缀 : " + mondayStr);
for (String each : availableTargetNames) {
if (each.endsWith(mondayStr)) {
logger.info("实际表 : " + each);
return each;
}
}
}
throw new IllegalArgumentException();
}
}
<!-- 定时创建表 -->
<bean id="dbProcessJob"
class="com.fpi.cloud.shardingjdbc.DBProcessJob">
<!-- 礼拜天建新表 -->
<property name="weekDay" value="7" />
<!-- 默认保留24张表-->
<property name="saveNum" value="24" />
</bean>
<bean id="dbProcessJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 指定任务类 -->
<property name="targetObject" ref="dbProcessJob" />
<!-- 指定任务执行的方法 -->
<property name="targetMethod" value="execute" />
</bean>
<bean id="dbProcessJobTrigger"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="dbProcessJobDetail" />
<!-- 每天晚上10点开始跑 -->
<property name="cronExpression" value="0 0 22 * * ?" />
<!-- <property name="startDelay" value="60000" /> -->
</bean>
package com.fpi.cloud.shardingjdbc;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.fpi.cloud.contants.RedisKey;
import com.fpi.cloud.service.SystemCfgService;
import com.fpi.cloud.utils.DateUtils;
import com.fpi.cloud.utils.DynamicDataSource;
import com.fpi.cloud.utils.MailUtils;
import com.fpi.cloud.utils.RedisUtils;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import io.shardingjdbc.core.api.config.ShardingRuleConfiguration;
import io.shardingjdbc.core.api.config.strategy.StandardShardingStrategyConfiguration;
import io.shardingjdbc.core.jdbc.core.datasource.ShardingDataSource;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* @ClassName: DBProcessJob
* @Description:定时创建新表,删除旧表
* @author: luchenxi
* @date: 2019年11月12日 上午10:29:05
*
*/
@ Component
@ Data
@ NoArgsConstructor
@ AllArgsConstructor
public class DBProcessJob
{
// 定时任务周几执行
public String weekDay;
// 保留表个数
public int saveNum;
private Logger logger = LogManager.getLogger(DBProcessJob.class);
@ Autowired
SystemCfgService systemCfgService
@ Autowired
ShardingDataSource shardingDataSource;
@ Autowired
ComboPooledDataSource deviceDataSource;
@ Autowired
BasicDataSource dataSource2;
@ Autowired
ComboPooledDataSource dataSource;
@ Autowired
DynamicDataSource myDataSource;
@ Autowired
SqlSessionFactoryBean sqlSessionFactory;
/**
*
* @Title: execute @Description:
* 每天晚上定时执行,判断是周天的话,调用存储过程,创建新表,删除旧表 @param: @return:
* void @throws
*/
public void execute()
{
try
{
logger.info("数据库分表处理定时任务启动...");
// 计算今天是否是周天
if(DateUtils.currentDayForWeek().equals(weekDay))
{
String oldestDB = RedisKey.SHARDINGDBNAME;
// 从缓存中获取已存在表个数
int currentNum = (int) (RedisUtils.llen(RedisKey.SHARDINGDBNAME) +
1);
if(currentNum >= saveNum)
{
// 从缓存中移除最早的表 20191111
String oldestDBSuffix = RedisUtils
.rpop(RedisKey.SHARDINGDBNAME);
if(StringUtils.isNotEmpty(oldestDBSuffix))
{
// dbname_20191111
oldestDB = oldestDB + oldestDBSuffix;
logger.info("需要删除旧表:" + oldestDB);
}
}
// 调用存储过程,建新表,删旧表
systemCfgService.deviceDBProcess(oldest9000DB, oldest8000DB);
logger.info("dbProcess 存储过程调用完成!");
// 生成下周的新表名日期后缀 20191113
String dateSuffix = DateUtils
.formatShortDate(DateUtils.getStandardDateNext(new Date()));
// 新表加入缓存
RedisUtils.lpush(RedisKey.SHARDINGDBNAME, dateSuffix);
logger.info("添加新表:" + dateSuffix);
// 关键: 分表变了后需要动态修改数据源配置
//applyShardingActTableChange();
// 刷新DataSource
refreshShardingDataSource();
}
}
catch (Exception e)
{
logger.error("仪表数据库处理定时任务执行失败!", e);
MailUtils.sendExceptRemainEmail("云平台定时分表任务执行失败!请尽快检查错误原因!");
}
}
/**
*
* @Title: getShardingDataSource @Description:
* 刷新数据源 @param: @return @return: DataSource @throws
*/
public void refreshShardingDataSource()
{
try
{
logger.info("ShardingJDBC DataSource 刷新 ...");
// 配置真实数据源
Map<String , DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("deviceDataSource", deviceDataSource);
// 配置shardingJDBC
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(TableRuleConfigFactory.getRuleConfig());
// 分表绑定
shardingRuleConfig.getBindingTableGroups().add("paramsampling");
// 分片算法
shardingRuleConfig.setDefaultTableShardingStrategyConfig(
new StandardShardingStrategyConfiguration("gmt_create",
SuperTableShardingAlgorithm.class.getName()));
// 刷新策略 这里非常重要
shardingDataSource.renew(shardingRuleConfig.build(dataSourceMap), new Properties());
logger.info("ShardingJDBC数据源已刷新...");
}
catch (Exception e)
{
logger.error("刷新数据源失败!", e);
MailUtils.sendExceptRemainEmail("刷新ShardingJDBC数据源失败:" + e.getMessage());
}
}
}
定时任务逻辑也很简单,就是到指定时间了,建个新表,删个旧表(非必须),然后重新配置数据源。
但是在我实际做的过程中,我是直接新创建一个shardingDataSource对象,然后替换掉旧的,但是一直都没办法刷新配置,无法将新建的表加入策略!!!百度也一直查不到原因,一度怀疑是不是源码中什么单例啊,什么Spring对bean的管理啊影响了!
实在是没有查到官方提供的这个renew方法,还是我自己实在是走头无路了,偶然间通过ide的代码提示功能看到这个方法,尝试了一下居然成了!!!!柳暗花明又一村。
重要的事情再说一遍:刷新分表数据源配置时一定要用renew方法!
shardingDataSource.renew(shardingRuleConfig.build(dataSourceMap), new Properties());
千万别new一个数据源
// 这个是错误的!!!!!
shardingDataSource =new ShardingDataSource(shardingRuleConfig.build(dataSourceMap));
就这个问题,我弄了整一天,查也没查到,很难受!!!睡了一觉,第二天Eclipse代码提示时突然看到了这个方法,应该是之前也提示过,被我忽视了!也没查到哪里介绍这个方法,点进去看了下源码说明:
/**
* Renew sharding data source.
*
* @param newShardingRule new sharding rule
* @param newProps new sharding properties
* @throws SQLException SQL exception
*/
public void renew(final ShardingRule newShardingRule, final Properties newProps) throws SQLException {
ShardingProperties newShardingProperties = new ShardingProperties(null == newProps ? new Properties() : newProps);
int originalExecutorSize = shardingProperties.getValue(ShardingPropertiesConstant.EXECUTOR_SIZE);
int newExecutorSize = newShardingProperties.getValue(ShardingPropertiesConstant.EXECUTOR_SIZE);
if (originalExecutorSize != newExecutorSize) {
executorEngine.close();
executorEngine = new ExecutorEngine(newExecutorSize);
}
boolean newShowSQL = newShardingProperties.getValue(ShardingPropertiesConstant.SQL_SHOW);
shardingProperties = newShardingProperties;
shardingContext = new ShardingContext(newShardingRule, getDatabaseType(), executorEngine, newShowSQL);
}
就他了!!!!
如果是只有一个数据源则不需要则一步!这里涉及到AOP的东西,身为菜鸟的我不敢乱说,相关理论概念自行百度吧,这里就直接应用了。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* @ClassName: DataSource
* @Description:自定义注解
* @author: luchenxi
* @date: 2019年6月28日 上午8:37:10
*
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
// 默认数据源为ds1
String value() default "ds1";
}
package com.fpi.cloud.aspect;
import java.lang.reflect.Method;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.fpi.cloud.utils.DynamicDataSource;
import com.fpi.cloud.utils.DynamicDataSourceHolder;
/**
*
* @ClassName: DataSourceAspect
* @Description:AOP配置
* @author: luchenxi
* @date: 2019年6月28日 上午8:37:53
*/
@ Aspect
@ Component
public class DataSourceAspect
{
private static Logger logger = LogManager.getLogger(DataSourceAspect.class);
@ Pointcut ("@annotation(com.fpi.cloud.aspect.DataSource)")
public void dataSource()
{
logger.info("多数据源切入点");
}
/**
*
* @Title: intercept @Description:
* 拦截目标方法,获取由@DataSource指定的数据源标识,设置到线程存储中以便切换数据源 @param: @param
* point @param: @throws Exception @return: void @throws
*/
@ Before ("dataSource()")
public void intercept(JoinPoint point) throws Exception
{
// logger.info("before...");
Class<?> target = point.getTarget().getClass();
MethodSignature signature = (MethodSignature) point.getSignature();
// 默认使用目标类型的注解,如果没有则使用其实现接口的注解
for(Class<?> clazz : target.getInterfaces())
{
// logger.info("need change db : " + signature.getMethod());
resolveDataSource(clazz, signature.getMethod());
}
resolveDataSource(target, signature.getMethod());
}
/**
*
* @Title: doAfter @Description: 清除数据源 @param: @param joinPoint @return:
* void @throws
*/
@ AfterReturning (pointcut = "dataSource()")
public void doAfter(JoinPoint joinPoint)
{
DynamicDataSource.clearDataSource();
}
/**
*
* @Title: resolveDataSource @Description:
* 提取目标对象方法注解和类型注解中的数据源标识 @param: @param clazz @param: @param
* method @return: void @throws
*/
private void resolveDataSource(Class<?> clazz , Method method)
{
try
{
Class<?>[] types = method.getParameterTypes();
// 默认使用类型注解
if(clazz.isAnnotationPresent(DataSource.class))
{
DataSource source = clazz.getAnnotation(DataSource.class);
DynamicDataSourceHolder.setDataSource(source.value());
// logger.info("数据源切换至:" + DynamicDataSourceHolder.getDataSource());
}
// 方法注解可以覆盖类型注解
Method m = clazz.getMethod(method.getName(), types);
if(m != null && m.isAnnotationPresent(DataSource.class))
{
DataSource source = m.getAnnotation(DataSource.class);
DynamicDataSourceHolder.setDataSource(source.value());
// logger.info("数据源切换至:" + DynamicDataSourceHolder.getDataSource());
}
}
catch (Exception e)
{
logger.error("数据源切换失败!", e);
}
}
}
<bean class="com.fpi.cloud.aspect.DataSourceAspect" id="dataSourceService"></bean>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@ DataSource ("ds2")
在对应的ServiceImpl中添加此注解即可,在需要切换数据源的方法上,或者在整个实现类上。
这里如果你能实现在Dao层切换可能更方便,身为菜鸟的我弄了好久也没搞出来,所以在划分业务的时候,我是有意将相同数据源的业务划分到一个service中,方便切换。
ShardingJDBC还是比较好用的,不需要去额外的维护什么数据库中间件,引入对应依赖就可以用了,但是呢个人觉得文档可以再完善一点,当然也可能是我对度娘的使用还不太熟练。
有什么错误的地方欢迎指出,谢谢!