sharding-jdbc-按日分表 解决它不支持部分sql

2024-02-04 04:59

本文主要是介绍sharding-jdbc-按日分表 解决它不支持部分sql,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

场景需求:

会话详情表(t_session_detail),单日数据非常大,所以我对会话详情表按日分表,今日的操作只入今日表,即按日分表.

实战:

1.导入sharding-jdbc依赖:

        <!--shading-jdbc--><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>sharding-jdbc-spring-boot-starter</artifactId><version>4.0.0-RC1</version></dependency>

2.配置sharding-jdbc:

sharding-jdbc代码不需要写很多,几乎全部在配置上。

1. 让sharding-jdbc接管数据源

2. 指定要分那个表

3. 指定分表的策略(如按照这个表的哪个字段来分)

这是yaml配置方式:

# 定义sharding-jdbc数据源
spring.shardingsphere.datasource.names = m1
spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost/test_shading_jdbc
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 333# 指定t_session_detail表的数据分布情况,配置数据节点. (由于是按日分表,所以表个数不固定,不能写死,于是采用表达式代替)
spring.shardingsphere.sharding.tables.t_session_detail.actual-data-nodes = m1.t_session_detail_$->{2020..2099}$->{(1..12).collect{t ->t.toString().padLeft(2,'0')}}$->{(1..31).collect{t ->t.toString().padLeft(2,'0')}}# 指定t_session_detail表的主键生成策略为SNOWFLAKE (由于t_session_detail是一种水平按日分表,所以不能采用自增等策略,这样会多表主键冲突。)
spring.shardingsphere.sharding.tables.t_session_detail.key-generator.column=id
spring.shardingsphere.sharding.tables.t_session_detail.key-generator.type=SNOWFLAKE# 指定t_session_detail表的分片策略,分片策略包括分片键和分片算法 (当指定分片键后,执行sql的时候sharding-jdbc会截取sql,判断本次sql是否有带分片键,如果带了就截取,按照您指定的分片策略算法重写sql)
spring.shardingsphere.sharding.tables.t_session_detail.table-strategy.standard.sharding-column = create_time
spring.shardingsphere.sharding.tables.t_session_detail.table-strategy.standard.precise-algorithm-class-name= com.dzh.shardingJdbc.config.DatabaseShardingAlgorithm# 打开sql输出日志
spring.shardingsphere.props.sql.show = true

我们也可以用代码来配置,这样更加的灵活,可以再配置类上@Condition,动态开关。

@Configuration
# 满足这个条件才注入sharding-jdbc 的功能,所以这个condition可以自定义。
@Conditional(value = {ShardingJdbcCondition.class})
public class ShardingJdbcAutoConfiguration {@AutowiredDbproperty dbProperty;  // 把你自己的数据库配置弄进来// 定义数据源public Map<String, DataSource> createDataSourceMap() {DruidDataSource dataSource1 = new DruidDataSource();dataSource1.setDriverClassName(dbProperty.getDriverClassName());dataSource1.setUrl(System.getProperty("spring.datasource.url"));dataSource1.setUsername(dbProperty.getUsername());dataSource1.setPassword(dbProperty.getPassword());Map<String, DataSource> result = new HashMap<>();result.put("给你的这个数据源起个名字", dataSource1);return result;}// 定义主键生成策略private static KeyGeneratorConfiguration getKeyGeneratorConfiguration() {KeyGeneratorConfiguration result = newKeyGeneratorConfiguration("SNOWFLAKE","表的主键名");return result;}// 指定表的分片策略TableRuleConfiguration getOrderTableRuleConfiguration() {TableRuleConfiguration result = new TableRuleConfiguration("逻辑表明,假设是session_detail","上明定义的数据源名.session_detail_$->{2022..2099}$->{(1..12).collect{t ->t.toString().padLeft(2,'0')}}$->{(1..31).collect{t ->t.toString().padLeft(2,'0')}}");result.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("分片键,就是按照表中哪个字段来分。如:crete_time",精确分片算法,范围分片算法[可为空,额外提供的,一般用来做范围查询时用到]);result.setKeyGeneratorConfig(getKeyGeneratorConfiguration());return result;}// 定义sharding‐Jdbc数据源@BeanDataSource getShardingDataSource() throws SQLException {ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();// 给sharding-jdbc添加 分表策略(如果有多个,可以添加多个)shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());//spring.shardingsphere.props.sql.show = trueProperties properties = new Properties();//properties.put("sql.show","true");return ShardingDataSourceFactory.createDataSource(createDataSourceMap(),shardingRuleConfig,properties);}
}

定义刚刚定义分片键的分片算法:如,我上面定create_time来分。那么我就会按照这个create_time重写表名.

精确分片算法(一般用来做insert):

public class DatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {@Overridepublic String doSharding(Collection<String> collection, PreciseShardingValue<Date> preciseShardingValue) {String db_name = preciseShardingValue.getLogicTableName(); // t_session_detailtry {// 获取到分片键中的值(create_time), 重改表名Date date = preciseShardingValue.getValue();String year = String.format("%tY", date);String mon = String.format("%tm",date);String day = String.format("%td",date);db_name = db_name+"_"+year+mon+day;return db_name;} catch (Exception e) {e.printStackTrace();}throw new IllegalArgumentException();}
}

范围分片算法(一般用来做查询):比如,需要查询会话的所有操作详情,那么这个会话的详情可能在1表中有,2表中有....这个时候sql上就需要create_time between  xxx and xxx. 就会走范围分片算法,你就可以拿到范围时间,筛选出所有可能存在详情的表。

public class SessionDetailRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {@Overridepublic Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Date> rangeShardingValue) {String db_name = rangeShardingValue.getLogicTableName(); // ogw_session_detailRange<Date> valueRange = rangeShardingValue.getValueRange();Date startDay = valueRange.lowerEndpoint();Date endDay = valueRange.upperEndpoint();Set<String> res = new HashSet<>();// 计算出这【startDay~endDay】日期之间的所有日期,同时转换成对应数据库表名Calendar calendar = Calendar.getInstance();while (startDay.getTime()<=endDay.getTime()){String year = String.format("%tY", startDay);String mon = String.format("%tm",startDay);String day = String.format("%td",startDay);res.add(db_name+"_"+year+mon+day);calendar.setTime(startDay);calendar.add(Calendar.DATE, 1);// 获取增加后的日期startDay = calendar.getTime();}/*为了确保,再次补充最后一天时间表名.比如: 会话开始于6月1日 晚上9点,持续到6月2日早上9点。此时上述while循环只收集了6月1日的表名,所以需要再次检测最后一天的表名。又比如:会话开始于6月1日晚上9点,持续到6月2日晚上10点。此时上述while循环收集了6月1日的和2日的表名,虽然此时会再次补充一次,也无需紧要。我们用的set集合。*/String day = String.format("%td",startDay);String ed = String.format("%td",endDay);if(day.equals(ed)){String year = String.format("%tY", startDay);String mon = String.format("%tm",startDay);res.add(db_name+"_"+year+mon+day);}return res;}
}

那么以上就是sharding-jdbc 的配置,主要就是

        1. 配置一下数据源,让sharding-jdbc接收数据源

        2. 配置被分表的主键策略 & 分片策略(精确和范围等等)

数据库:

        模拟建了两张日期表,当然可以写一个定时任务,凌晨时候将明天或者后天的创建出来。

        

测试代码:

        1. 今日新增条会话详情:由于create_time字段是自动填充,填充今日的时间,所以会走分片策略,重写sql语句表名。

@Testpublic void testInsertSessionDetail() throws ParseException {SessionDetailDTO detailDTO = new SessionDetailDTO();detailDTO.setContent("会话详情asdsadsadsa");detailDTO.setSessionId(2L);sessionDetailMapper.insert(detailDTO);}

逻辑sql是这样:

INSERT INTO t_session_detail  ( create_time,content,session_id)  VALUES  ( ?,?,?)

sharding-jdbc重写:

Actual SQL: m1 ::: INSERT INTO t_session_detail_20220529   (create_time, content, session_id, id) VALUES (?, ?, ?, ?) ::: [2022-05-29 16:41:13.749, 会话详情asdsadsadsa, 2, 737711275903025153]

此时就实现了今日操作全入今日表,当然如果你这个时间是昨天的,肯定就入昨天的表。查询也是一样,sharding-jdbc 看是否命中分片,如果命中,按照分片策略重写sql。

当然,如果您的sql要是没有带分片键,那么sharding-jdbc就会根据你配置文件配置的所有数据表节点都执行一次这个sql语句,最终在汇总结果给你。这样就效率大大降低,所以使用sharding-jdbc 一定要带上这个分片键。

项目中运用分表照成的问题记录:

背景:

        由于项目需要适配Oracle数据库,故本人在测试过程中,发现项目中许多涉及到数据库的sql都会报错,报一些Sharding-jdbc的sql语法解析错误。这个问题困扰了我大半天。一直在想mysql都没错,为什么换成oracle就不行。并且抛出的报错中,我把sql截取下来,丢到oracle数据库中执行,是可行的。说明sql本身是没有问题的。后来,我百度了下,发现shardingJdbc对许多sql不支持,比如left join xxx on (条件) , 对于on 后面不能有(),而我使用MybatisPlus框架,生成的sql会加(),就矛盾了。所以他在解析过程就会抛出错误,都到不了连接数据库流程。

        如下图错误:

        

        后来我就思考,shardingJdbc 既然需要先解析sql,如果不满足它框架的SQL语法,就要抛出错误,那我直接去修改源码,我将哪些语法限制全部注释,于是,我就直接重写了TableReferecesClauseParse类的parseJoinCondition().  不让他校验。

        就在我松了一口气后,过了一天,小组群内又抛出错误了,还是sql解析异常,但不是连表错误,是其他的语法union。我又照葫芦画瓢,将UNION语法限制也注释掉。如下:

   

        但是好景不长,越点系统,越多sql解析错误,等不知名异常。我直接奔溃了,大骂shardingJdbc。为什么mysql数据库没有问题,适配oracle这么多。这马上要发包了,给我来这一出。就在我实在无奈时,我直接百度了shardingJdbc 原理。发现它代理了JDBC所有流程。当我们使用shardingJdbc时,其实是使用了sharding数据源,mybatis框架在获取数据源时,其实获取的是shardingjdbc数据源,从这个数据源获取到的connection连接对象就是sharding连接对象了,当然后续Statement等JDBC对象都是sharding的。所以才会出现这么多问题。

        于是,我大开杀戒,将sharding数据源 和 原生数据源 包装成Spring动态数据源。最终注入容器中就只有一个数据源,就是Spring的动态数据源。然后我利用Mybatis的Plus拦截插件机制,拦截Mybatis获取数据源流程,去判断本次流程的sql语句,如果是分表逻辑的sql,我直接给它sharding数据源,否则都采用原生数据源。后来就没有那么多七七八八的sql解析异常了。

代码如下:

1. 注入Spring动态数据源。

在项目的ShardingJdbc自动配置类中,之前这里是直接return sharding数据源,现在我return 动态数据源。 

 以下是 自定义动态数据源 (Spring 会回调 determineCurrentLookupKey(), 通过key 去map中找对应的数据源给你用。所以通常我们都是利用ThreadLocal去让当前线程选择使用key选择使用)

/*** 自定义路由数据源*      在ShardingJdbcAutoConfiguration中像ioc容器中注入了 MyDynamicDataSource*      MyDynamicDataSource.targetDataSources中持有shardingDataSource & 普通DataSource*      最终Spring中就只有一个路由数据源MyDynamicDataSource。**     Mybatis采用哪个数据源去连接数据库全靠DataSourceKeyHolder里面的ThreadLocal的value是什么。*     如果是originalDataSource---》就选择普通数据源*     如果是shardingDataSource---》就选择shardingDatasource*     从而去避免只使用shardingDataSource导致的一些sql语句不兼容问题。**     而DataSourceKeyHolder.ThreadLocal.value 我们是借助mybatis提供的Pluin插件机制来设置值。*     我们只需要在Mybatis获取数据源前,设置一下DataSourceKeyHolder.ThreadLocal.value即可。*     而这个时机,Mybatis提供的Pluin插件机制正好满足我们。** @author zihao.deng*/
public class MyDynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceKeyHolder.getKey();}
}
public class DataSourceKeyHolder {private static final ThreadLocal<String> keyHolder = new ThreadLocal<>();public static void setKey(String key) {keyHolder.set(key);}public static String getKey() {String s = keyHolder.get();return s;}public static void clear(){keyHolder.remove();}
}

2. 编写Mybatis 插件类

        直接拦截Mybatis中的Executor体系,把CRUD都拦截主。判断sql语句是否走分表逻辑。如果走,就将ThreadLocal中的值变成 sharding数据源的key。

@Intercepts({@Signature(type = Executor.class, method = "update",args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Slf4j
public class DynamicallyDataSourcePlugIn implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object[] args = invocation.getArgs();if(args[0] instanceof MappedStatement) {MappedStatement ms = (MappedStatement) args[0];BoundSql boundSql = ms.getBoundSql(wrapCollection(args[1]));String sql = boundSql.getSql();if(sql!=null && sql.contains("ogw_session_detail")){log.info("走分表...选择shardingDataSource...");DataSourceKeyHolder.setKey("shardingDataSource");}else{DataSourceKeyHolder.setKey("originalDataSource");}}return invocation.proceed();}private Object wrapCollection(final Object object) {return ParamNameResolver.wrapToMapIfCollection(object, null);}
}

3. 补充下Mybatis插件机制原理:

        在mybatis体系中,所有的curd都是通过sqlSession去操作。而sqlSession其实是委托给了Executor体系去完成。而Executor体系的实现类中,通常都是使用SimpleExeucutor。所以直接定位到SimpleExecutor类中。如本次以selectList() 查询为例子:

         先看 prepareStatement(handler, ms.getStatementLog());

在prepareStatement() 中,会从Spring中获取一个数据源,在从数据源中获取connection连接对象,在通过Connection对象获取stamt对象。(里面逻辑不看了,就是这么个理)所以我们只需要在此处拦截,做一个获取数据源的判断即可。

在看 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

 正好满足我们的需求,在获取数据源前应用插件机制。

 其实插件机制的切入点,不止在这里,还有后续Statement执行时切入等,我都忘记了。只是突然想到了这里。

所以对于这些技术,原理真的非常重要,如果不去学习,出现这种问题,压根无从下手,学了源码等,就会知道很多扩展点,有多问题都是可以通过扩展点去解决的。

这篇关于sharding-jdbc-按日分表 解决它不支持部分sql的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/676416

相关文章

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Mysql表的简单操作(基本技能)

《Mysql表的简单操作(基本技能)》在数据库中,表的操作主要包括表的创建、查看、修改、删除等,了解如何操作这些表是数据库管理和开发的基本技能,本文给大家介绍Mysql表的简单操作,感兴趣的朋友一起看... 目录3.1 创建表 3.2 查看表结构3.3 修改表3.4 实践案例:修改表在数据库中,表的操作主要

SpringKafka消息发布之KafkaTemplate与事务支持功能

《SpringKafka消息发布之KafkaTemplate与事务支持功能》通过本文介绍的基本用法、序列化选项、事务支持、错误处理和性能优化技术,开发者可以构建高效可靠的Kafka消息发布系统,事务支... 目录引言一、KafkaTemplate基础二、消息序列化三、事务支持机制四、错误处理与重试五、性能优

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

mysql出现ERROR 2003 (HY000): Can‘t connect to MySQL server on ‘localhost‘ (10061)的解决方法

《mysql出现ERROR2003(HY000):Can‘tconnecttoMySQLserveron‘localhost‘(10061)的解决方法》本文主要介绍了mysql出现... 目录前言:第一步:第二步:第三步:总结:前言:当你想通过命令窗口想打开mysql时候发现提http://www.cpp

MySQL大表数据的分区与分库分表的实现

《MySQL大表数据的分区与分库分表的实现》数据库的分区和分库分表是两种常用的技术方案,本文主要介绍了MySQL大表数据的分区与分库分表的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. mysql大表数据的分区1.1 什么是分区?1.2 分区的类型1.3 分区的优点1.4 分

MySQL错误代码2058和2059的解决办法

《MySQL错误代码2058和2059的解决办法》:本文主要介绍MySQL错误代码2058和2059的解决办法,2058和2059的错误码核心都是你用的客户端工具和mysql版本的密码插件不匹配,... 目录1. 前置理解2.报错现象3.解决办法(敲重点!!!)1. php前置理解2058和2059的错误

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T

MySQL INSERT语句实现当记录不存在时插入的几种方法

《MySQLINSERT语句实现当记录不存在时插入的几种方法》MySQL的INSERT语句是用于向数据库表中插入新记录的关键命令,下面:本文主要介绍MySQLINSERT语句实现当记录不存在时... 目录使用 INSERT IGNORE使用 ON DUPLICATE KEY UPDATE使用 REPLACE