ShardingDataSource动态数据源整合&分表方案
@ConfigurationProperties(prefix = "mysql.one") 后置处理,配置文件中的指定键值对映射到一个java实体类上
sharding-jdbc 分库分表
https://www.cnblogs.com/hongdada/p/9324473.html
https://blog.csdn.net/hy245120020/article/details/85335446
https://blog.51cto.com/14442094/2450074 http://shardingsphere.apache.org/index_zh.html
shrding是由 io/shardingsphere/shardingjdbc/spring/boot/SpringBootConfiguration.class 加载的数据源
io/shardingjdbc/spring/boot/SpringBootConfiguration.class
io.shardingsphere.shardingjdbc.jdbc.core.datasource.ShardingDataSource
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.12.RELEASE</version> com.alibaba.druid.pool.DruidDataSource 多数据源切换不用@Primary指定一个主数据源 ShardingDataSource DruidDataSource 整合 <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.12.RELEASE</version> <dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <!--<version>2.0.4.RELEASE</version>--> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.21</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> @Configuration public class MysDruidConfig { // @Primary @Bean(name="dOne") @ConfigurationProperties(prefix = "mysql.one") public DruidDataSource dataSourceOne(){ DruidDataSource dOne = new DruidDataSource(); dOne.setQueryTimeout(300); dOne.setTestWhileIdle(true); dOne.setTestOnBorrow(true); dOne.setTestOnReturn(true); dOne.setTimeBetweenEvictionRunsMillis(600000); dOne.setMinEvictableIdleTimeMillis(300000); dOne.setUrl("jdbc:mysql://127.0.0.1/xmh"); return dOne; } @Bean(name="dTwo") @ConfigurationProperties(prefix = "mysql.two") public DruidDataSource dataSourceTwo(){ DruidDataSource dTwo = new DruidDataSource(); dTwo.setQueryTimeout(300); dTwo.setTestWhileIdle(true); dTwo.setTestOnBorrow(true); dTwo.setTestOnReturn(true); dTwo.setTimeBetweenEvictionRunsMillis(600000); dTwo.setMinEvictableIdleTimeMillis(300000); dTwo.setUrl("jdbc:mysql://127.0.0.1/xmh2"); return dTwo; } @Autowired javax.sql.DataSource shardingDataSource; SpringBootConfiguration s; @Bean(name = "routeDataSource") public RouteDataSource dataSource(@Qualifier("dOne")DruidDataSource dOne, @Qualifier("dTwo")DruidDataSource dTwo){ System.out.println("this.shardingDataSource***************"+this.shardingDataSource); System.out.println("DruidDataSource************dOne"+dOne.toString()+" "+dOne.getUrl()+" "+dOne.getMaxActive()); System.out.println("DruidDataSource************dTwo"+dTwo.toString()+" "+dTwo.getUrl()+" "+dTwo.getMaxActive()); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("dataSourceOne",dOne); targetDataSources.put("dataSourceTwo",shardingDataSource); RouteDataSource dataSource = new RouteDataSource(); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(dTwo); System.out.println("routeDataSource******************"+dataSource); return dataSource; } } @Mapper public interface MyTestIbatisSharding extends BaseMapper<String> { @MyDataSource(dataSourceName="dataSourceTwo") @Select("select order_id from t_order ") List<String> selectStr(); } @Autowired cn.com.xmh.ibatisMapper.MyTestIbatisSharding MyTestIbatisSharding; System.out.println(" MyTestIbatisSharding*********** "+MyTestIbatisSharding.selectStr()); spring-boot2以上不能使用_ sharding.jdbc.datasource.names=ds0,ds1 sharding.jdbc.datasource.ds0.type=org.apache.commons.dbcp.BasicDataSource sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds0.url=jdbc:mysql://localhost:3306/xmh sharding.jdbc.datasource.ds0.username=root sharding.jdbc.datasource.ds0.password=admin sharding.jdbc.datasource.ds1.type=org.apache.commons.dbcp.BasicDataSource sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds1.url=jdbc:mysql://localhost:3306/xmh2 sharding.jdbc.datasource.ds1.username=root sharding.jdbc.datasource.ds1.password=admin sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds${user_id % 2} sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds${0..1}.t_order_${0..1} sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_${order_id % 2} sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=order_id sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds${0..1}.t_order_item_${0..1} sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.algorithm-expression=t_order_item_${order_id % 2} sharding.jdbc.config.sharding.tables.t_order_item.key-generator-column-name=order_item_id
public interface BaseMapper<T> extends Mapper<T> {//,InsertIgnoreMapper<T>
}
springboot引入mybatis的xml配置 commons-dbcp 使用默认数据源 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot</artifactId> <version>1.5.8.RELEASE</version> </parent> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> import org.apache.ibatis.annotations.Mapper; @Mapper public interface MyDaoMapperTest { public List<TUser> queryAll(); } F:\study-idea\space\myboot\src\main\resources\MyMappers\myDaoTestMapper.xml中namespace设置 <mapper namespace="cn.com.myBatisXml.MyDaoMapperTest"> @Resource MyDaoMapperTest myDaoMapperTest; System.out.println(myDaoMapperTest.queryAll()); springboot引入mybatis的xml配置 commons-dbcp 使用默认数据源 使用shardingjdbc 不与DruidDataSource一起使用 @Primary <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>io.shardingjdbc</groupId> <artifactId>sharding-jdbc-core-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <!--<version>2.0.4.RELEASE</version>--> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.21</version> </dependency> sharding.jdbc.datasource.names=ds_0,ds_1 sharding.jdbc.datasource.ds_0.type=org.apache.commons.dbcp.BasicDataSource sharding.jdbc.datasource.ds_0.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds_0.url=jdbc:mysql://localhost:3306/xmh sharding.jdbc.datasource.ds_0.username=root sharding.jdbc.datasource.ds_0.password=admin sharding.jdbc.datasource.ds_1.type=org.apache.commons.dbcp.BasicDataSource sharding.jdbc.datasource.ds_1.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds_1.url=jdbc:mysql://localhost:3306/xmh2 sharding.jdbc.datasource.ds_1.username=root sharding.jdbc.datasource.ds_1.password=admin sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds_${user_id % 2} sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds_${0..1}.t_order_${0..1} sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_${order_id % 2} sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=order_id sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds_${0..1}.t_order_item_${0..1} sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.algorithm-expression=t_order_item_${order_id % 2} sharding.jdbc.config.sharding.tables.t_order_item.key-generator-column-name=order_item_id for (int i = 0; i < 3; i++) { Order order = new Order(); order.setUserId(55); order.setStatus("INSERT_TEST"); order.setOrderId(3); orderRepository.insert(order); } <insert id="insert" useGeneratedKeys="true" keyProperty="orderId"> INSERT INTO t_order ( user_id, status ) VALUES ( #{userId,jdbcType=INTEGER}, #{status,jdbcType=VARCHAR} ) </insert>
分库分表
https://mp.weixin.qq.com/s/aAZH15d0KM-dD8pY9xeTow
考虑因素:
后续翻倍扩容法进行扩容时,会出现扩容前后数据不在同一个表中,从而无法实施
常见错误案例一:非互质关系导致的数据偏斜问题
上述方案是初次使用者特别容易进入的误区,用Hash值分别对分库数和分表数取余,得到库序号和表序号。其实稍微思索一下,我们就会发现,以10库100表为例,如果一个Hash值对100取余为0,那么它对10取余也必然为0。
这就意味着只有0库里面的0表才可能有数据,而其他库中的0表永远为空!
类似的我们还能推导到,0库里面的共100张表,只有10张表中(个位数为0的表序号)才可能有数据。这就带来了非常严重的数据偏斜问题,
优化1:我们把10库100表看成总共1000个逻辑表,将求得的Hash值对1000取余,得到一个介于[0,999)中的数,然后再将这个数二次均分到每个库和每个表中,大概逻辑代码如下:
public static ShardCfg shard(String userId) {
// ① 算Hash
int hash = userId.hashCode();
// ② 总分片数
int sumSlot = DB_CNT * TBL_CNT;
// ③ 分片序号
int slot = Math.abs(hash % sumSlot);
// ④ 计算库序号和表序号的错误案例
int dbIdx = slot % DB_CNT ;
int tblIdx = slot / DB_CNT ;
return new ShardCfg(dbIdx, tblIdx);
}
该方案确实很巧妙的解决了数据偏斜的问题,只要Hash值足够均匀,那么理论上分配序号也会足够平均
问题:例如扩容前Hash为1986的数据应该存放在6库98表,但是翻倍扩容成20库100表后,它分配到了6库99表,表序号发生了偏移
最终方案:事实上,我们只需要换种写法,就能得出一个比较大众化的分库分表方案。
public static ShardCfg shard2(String userId) {
// ① 算Hash
int hash = userId.hashCode();
// ② 总分片数
int sumSlot = DB_CNT * TBL_CNT;
// ③ 分片序号
int slot = Math.abs(hash % sumSlot);
// ④ 重新修改二次求值方案
int dbIdx = slot / TBL_CNT ;
int tblIdx = slot % TBL_CNT ;
return new ShardCfg(dbIdx, tblIdx);
}
【方案缺点】
1、翻倍扩容法前期操作性高,但是后续如果分库数已经是大几十的时候,每次扩容都非常耗费资源。
2、连续的分片键Hash值大概率会散落在相同的库中,某些业务可能容易存在库热点(例如新生成的用户Hash相邻且递增,且新增用户又是高概率的活跃用户,那么一段时间内生成的新用户都会集中在相邻的几个库中)。
常用姿势四:剔除公因数法
还是基于错误案例一启发,在很多场景下我们还是希望相邻的Hash能分到不同的库中。就像N库单表的时候,我们计算库序号一般直接用Hash值对库数量取余。
那么我们是不是可以有办法去除掉公因数的影响呢?下面为一个可以考虑的实现案例:
public static ShardCfg shard(String userId) {
int dbIdx = Math.abs(userId.hashCode() % DB_CNT);
// 计算表序号时先剔除掉公约数的影响
int tblIdx = Math.abs((userId.hashCode() / TBL_CNT) % TBL_CNT);
return new ShardCfg(dbIdx, tblIdx);
}
经过测算,该方案的最大数据偏斜度也比较小,针对不少业务从N库1表升级到N库M表下,需要维护库序号不变的场景下可以考虑。
常用姿势五:一致性Hash法
虚拟节点主要解决的痛点是节点数据搬迁过程中各个节点的负载不均衡问题,通过虚拟节点打散到各个节点中均摊压力进行处理。
常用姿势三:基因法
还是由错误案例一启发,我们发现案例一不合理的主要原因,就是因为库序号和表序号的计算逻辑中,有公约数这个因子在影响库表的独立性。
那么我们是否可以换一种思路呢?我们使用相对独立的Hash值来计算库序号和表序号。
public static ShardCfg shard(String userId) {
int dbIdx = Math.abs(userId.substring(0, 4).hashCode() % DB_CNT );
int tblIdx = Math.abs(userId.hashCode() % TBL_CNT);
return new ShardCfg(dbIdx, tblIdx);
}
如上所示,我们计算库序号的时候做了部分改动,我们使用分片键的前四位作为Hash值来计算库序号。
这也是一种常用的方案,我们称为基因法,即使用原分片键中的某些基因(例如前四位)作为库的计算因子,而使用另外一些基因作为表的计算因子。该方案也是网上不少的实践方案或者是其变种,看起来非常巧妙的解决了问题,然而在实际生成过程中还是需要慎重。
如果不是16库100表,而是8库100表,或者20库100表,数据偏斜率都能降低到了5%以下的可接受范围。所以该方案的隐藏的"坑"较多,我们不仅要估算上线初期的偏斜率,还需要测算若干次翻倍扩容后的数据偏斜率。
https://mp.weixin.qq.com/s/aAZH15d0KM-dD8pY9xeTow
除了partition key只有一个非partition key作为条件查询解决方案,映射法