# springboot-sharding-jdbc-demo **Repository Path**: spartajet/springboot-sharding-jdbc-demo ## Basic Information - **Project Name**: springboot-sharding-jdbc-demo - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 10 - **Forks**: 7 - **Created**: 2018-08-31 - **Last Updated**: 2022-05-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README > 项目中遇到了分库分表的问题,找到了shrding-jdbc,于是就搞了一个springboot+sharding-jdbc+mybatis的增量分片的应用。今天写博客总结一下遇到的坑。 > > 其实,我自己写了一个increament-jdbc组件的,当我读了sharding-jdbc的源码之后,发现思路和原理差不多,sharding这个各方面要比我的强,毕竟我是一天之内赶出来的东东。 > > 示例代码地址:https://gitee.com/spartajet/springboot-sharding-jdbc-demo.git > > demo没有写日志,也没有各种异常判断,只是说明问题 ## 一、需求背景 我的项目背景就不说了,现在举一个例子吧:A,B两支股票都在上海,深圳上市,需要实时记录这两支股票的交易tick(不懂tick也没有关系)。现在的分片策略是:上海、深圳分别建库,每个库都存各自交易所的两支股票的ticktick,且按照月分表。如图: * db_sh * tick_a_2017_01 * tick_b_2017_01 * ........ * tick_a_2017_12 * tick_b_2017_12 * db_sz * tick_a_2017_01 * tick_b_2017_01 * ........ * tick_a_2017_12 * tick_b_2017_12 分库分表就是这样的。根据这个建库。 **千万不要讨论这样分库分表是否合适,这里这样分片只是举个栗子,说明分库分表这个事情。** **Sharding-jdbc是不支持建库的SQL,如果像我这样增量的数据库和数据表,那就要一次性把一段时期的数据库和数据表都要建好。** ## 二、建库 考虑到表确实多,所以我就只建1,2月份的表。语句见demo文件。 ## 三、springboot集成sharding-jdbc mvn配置pom如下: ```xml com.spartajet springboot-sharding-jdbc-demo 0.0.1-SNAPSHOT jar springboot-sharding-jdbc-demo Springboot integrate Sharding-jdbc Demo UTF-8 UTF-8 UTF-8 zh_CN 1.8 ${java.version} 1.4.1.RELEASE 1.0.13 5.1.36 1.4.1 2.8.0 2.9.7 1.4 2.5 1.2.0 org.springframework.boot spring-boot-starter-jdbc ${spring.boot.version} org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis-spring-boot-starter.version} commons-dbcp commons-dbcp ${commons-dbcp.version} com.dangdang sharding-jdbc-core ${sharding-jdbc.version} com.dangdang sharding-jdbc-config-spring ${sharding-jdbc.version} com.dangdang sharding-jdbc-self-id-generator ${sharding-jdbc.version} com.google.code.gson gson ${com.google.code.gson.version} org.springframework.boot spring-boot-starter-web ${spring.boot.version} org.springframework.boot spring-boot-start-logging org.springframework.boot spring-boot-starter-test ${spring.boot.version} test org.springframework.boot spring-boot-starter-log4j2 ${spring.boot.version} log4j log4j org.springframework.boot spring-boot-starter ${spring.boot.version} org.springframework.boot spring-boot-start-logging logback-classic ch.qos.logback log4j-over-slf4j org.slf4j mysql mysql-connector-java ${mysql-connector-java.version} org.springframework.boot spring-boot-maven-plugin ${spring.boot.version} org.apache.maven.plugins maven-compiler-plugin 3.1 ${project.build.jdk} ${project.build.jdk} ${project.build.sourceEncoding} org.apache.maven.plugins maven-jar-plugin 2.4 ``` 其实这个和sharding-jdbc的官网差不多。其实我想写一个`sharding-jdbc-spring-boot-starter`的pom的,等项目业务都做完再说吧。 ## 四、配置数据源 我想将数据库做成可配置的,所以我没有在`application.properties`文件中直接配置数据库,而是写在了`database.json`文件中。 ```json [ { "name": "db_sh", "url": "jdbc:mysql://localhost:3306/db_sh", "username": "root", "password": "root", "driveClassName":"com.mysql.jdbc.Driver" }, { "name": "db_sz", "url": "jdbc:mysql://localhost:3306/db_sz", "username": "root", "password": "root", "driveClassName":"com.mysql.jdbc.Driver" } ] ``` 然后在springboot读取database文件,加载方式如下: ```java @Value("classpath:database.json") private Resource databaseFile; @Bean public List databases() throws IOException { String databasesString = IOUtils.toString(databaseFile.getInputStream(), Charset.forName("UTF-8")); List databases = new Gson().fromJson(databasesString, new TypeToken>() { }.getType()); return databases; } ``` 加载完database信息之后,可以通过工厂方法配置逻辑数据库: ```java @Bean public HashMap dataSourceMap(List databases) { Map dataSourceMap = new HashMap<>(); for (Database database : databases) { DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); dataSourceBuilder.url(database.getUrl()); dataSourceBuilder.driverClassName(database.getDriveClassName()); dataSourceBuilder.username(database.getUsername()); dataSourceBuilder.password(database.getPassword()); DataSource dataSource = dataSourceBuilder.build(); dataSourceMap.put(database.getName(), dataSource); } return dataSourceMap; } ``` 这样就把各个逻辑数据库就加载好了。 ## 五、配置分片策略 ### 5.1数据库分片策略 在这个实例中,数据库的分库就是根据上海(sh)和深圳(sz)来分的,在sharding-jdbc中是单键分片。根据官方文档实现接口`SingleKeyDatabaseShardingAlgorithm`就可以 ```java @service public class DatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm { /** * 根据分片值和SQL的=运算符计算分片结果名称集合. * * @param availableTargetNames 所有的可用目标名称集合, 一般是数据源或表名称 * @param shardingValue 分片值 * * @return 分片后指向的目标名称, 一般是数据源或表名称 */ @Override public String doEqualSharding(Collection availableTargetNames, ShardingValue shardingValue) { String databaseName = ""; for (String targetName : availableTargetNames) { if (targetName.endsWith(shardingValue.getValue())) { databaseName = targetName; break; } } return databaseName; } } ``` 此接口还有另外两个方法,`doInSharding`和`doBetweenSharding`,因为我暂时不用IN和BETWEEN方法,所以就没有写,直接返回null。 ### 5.2数据表分片策略 数据表的分片策略是根据股票和时间共同决定的,在sharding-jdbc中是多键分片。根据官方文档,实现`MultipleKeysTableShardingAlgorithm`接口就OK了 ```java @service public class TableShardingAlgorithm implements MultipleKeysTableShardingAlgorithm { /** * 根据分片值计算分片结果名称集合. * * @param availableTargetNames 所有的可用目标名称集合, 一般是数据源或表名称 * @param shardingValues 分片值集合 * * @return 分片后指向的目标名称集合, 一般是数据源或表名称 */ @Override public Collection doSharding(Collection availableTargetNames, Collection> shardingValues) { String name = null; Date time = null; for (ShardingValue shardingValue : shardingValues) { if (shardingValue.getColumnName().equals("name")) { name = ((ShardingValue) shardingValue).getValue(); } if (shardingValue.getColumnName().equals("time")) { time = ((ShardingValue) shardingValue).getValue(); } if (name != null && time != null) { break; } } String timeString = new SimpleDateFormat("yyyy_MM").format(time); String suffix = name + "_" + timeString; Collection result = new LinkedHashSet<>(); for (String targetName : availableTargetNames) { if (targetName.endsWith(suffix)) { result.add(targetName); } } return result; } } ``` 这些方法的使用可以查官方文档。 ### 5.3注入分片策略 以上只是定义了分片算法,还没有形成策略,还没有告诉shrding将哪个字段给分片算法: ``` @Configuration public class ShardingStrategyConfig { @Bean public DatabaseShardingStrategy databaseShardingStrategy(DatabaseShardingAlgorithm databaseShardingAlgorithm) { DatabaseShardingStrategy databaseShardingStrategy = new DatabaseShardingStrategy("exchange", databaseShardingAlgorithm); return databaseShardingStrategy; } @Bean public TableShardingStrategy tableShardingStrategy(TableShardingAlgorithm tableShardingAlgorithm) { Collection columns = new LinkedList<>(); columns.add("name"); columns.add("time"); TableShardingStrategy tableShardingStrategy = new TableShardingStrategy(columns, tableShardingAlgorithm); return tableShardingStrategy; } } ``` 这样才能形成完成的分片策略。 ## 六、配置Sharding-jdbc的DataSource sharding-jdbc的原理其实很简单,就是自己做一个DataSource给上层应用使用,这个DataSource包含所有的逻辑库和逻辑表,应用增删改查时,他自己再修改sql,然后选择合适的数据库继续操作。所以这个DataSource创建很重要。 ```java @Bean @Primary public DataSource shardingDataSource(HashMap dataSourceMap, DatabaseShardingStrategy databaseShardingStrategy, TableShardingStrategy tableShardingStrategy) { DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap); TableRule tableRule = TableRule.builder("tick").actualTables(Arrays.asList("db_sh.tick_a_2017_01", "db_sh.tick_a_2017_02", "db_sh.tick_b_2017_01", "db_sh.tick_b_2017_02", "db_sz.tick_a_2017_01", "db_sz.tick_a_2017_02", "db_sz.tick_b_2017_01", "db_sz.tick_a_2017_02")).dataSourceRule(dataSourceRule).build(); ShardingRule shardingRule = ShardingRule.builder().dataSourceRule(dataSourceRule).tableRules(Arrays.asList(tableRule)).databaseShardingStrategy(databaseShardingStrategy).tableShardingStrategy(tableShardingStrategy).build(); DataSource shardingDataSource = ShardingDataSourceFactory.createDataSource(shardingRule); return shardingDataSource; } ``` **这里要着重说一下为什么要用@Primary这个注解,没有这个注解是会报错的,错误大致意思就是DataSource太多了,mybatis不知道用哪个。加上这个mybatis就知道用sharding的DataSource了。这里参考的是jpa的多数据源配置** ## 七、配置mybatis ### 7.1 Bean ```java public class Tick { private long id; private String name; private String exchange; private int ask; private int bid; private Date time; } ``` ### 7.2 Mapper 很简单,只实现一个插入方法 ```java @Mapper public interface TickMapper { @Insert("insert into tick (id,name,exchange,ask,bid,time) values (#{id},#{name},#{exchange},#{ask},#{bid},#{time})") void insertTick(Tick tick); } ``` ### 7.3 SessionFactory配置 还要设置一下tick的SessionFactory: ``` @Configuration @MapperScan(basePackages = "com.spartajet.shardingboot.mapper", sqlSessionFactoryRef = "sessionFactory") public class TickSessionFactoryConfig { @Bean public SqlSessionFactory sessionFactory(DataSource shardingDataSource) throws Exception { final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(shardingDataSource); return sessionFactory.getObject(); } @Bean public CommonSelfIdGenerator commonSelfIdGenerator() { CommonSelfIdGenerator.setClock(AbstractClock.systemClock()); CommonSelfIdGenerator commonSelfIdGenerator = new CommonSelfIdGenerator(); return commonSelfIdGenerator; } } ``` 这里添加了一个`CommonSelfIdGenerator`,sharding自带的id生成器,看了下代码和`facebook`的`snowflake`类似。我又不想把数据库的主键设置成自增的,否则数据双向同步会死的很惨的。 ### ## 八、测试写入 ``` @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest public class SpringbootShardingJdbcDemoApplicationTests { @Autowired private TickMapper tickMapper; @Autowired private CommonSelfIdGenerator commonSelfIdGenerator; @Test public void contextLoads() { Tick tick = new Tick(commonSelfIdGenerator.generateId().longValue(), "a", "sh", 100, 200, new Date()); this.tickMapper.insertTick(tick); } } ``` 成功实现增量分库分表!!!