因爲噹噹發佈了最新的Sharding-Sphere,因此本文已通過時,不日將推出新的版本java
項目中遇到了分庫分表的問題,找到了shrding-jdbc,因而就搞了一個springboot+sharding-jdbc+mybatis的增量分片的應用。今天寫博客總結一下遇到的坑。其實,我本身寫了一個increament-jdbc組件的,當我讀了sharding-jdbc的源碼以後,發現思路和原理差很少,sharding這個各方面要比個人強,畢竟我是一天以內趕出來的東東。mysql
示例代碼地址:https://gitee.com/spartajet/s...git
demo沒有寫日誌,也沒有各類異常判斷,只是說明問題web
個人項目背景就不說了,如今舉一個例子吧:A,B兩支股票都在上海,深圳上市,須要實時記錄這兩支股票的交易tick(不懂tick也沒有關係)。如今的分片策略是:上海、深圳分別建庫,每一個庫都存各自交易所的兩支股票的ticktick,且按照月分表。如圖:算法
db_shspring
db_szsql
tick_a_2017_01數據庫
分庫分表就是這樣的。根據這個建庫。 **千萬不要討論這樣分庫分表是否合適,這裏這樣分片只是舉個栗子,說明分庫分表這個事情。** **Sharding-jdbc是不支持建庫的SQL,若是像我這樣增量的數據庫和數據表,那就要一次性把一段時期的數據庫和數據表都要建好。**
考慮到表確實多,因此我就只建1,2月份的表。語句見demo文件。apache
mvn配置pom以下:json
<groupId>com.spartajet</groupId> <artifactId>springboot-sharding-jdbc-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot-sharding-jdbc-demo</name> <description>Springboot integrate Sharding-jdbc Demo</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.locales>zh_CN</project.build.locales> <java.version>1.8</java.version> <project.build.jdk>${java.version}</project.build.jdk> <spring.boot.version>1.4.1.RELEASE</spring.boot.version> <com.alibaba.druid.version>1.0.13</com.alibaba.druid.version> <mysql-connector-java.version>5.1.36</mysql-connector-java.version> <sharding-jdbc.version>1.4.1</sharding-jdbc.version> <com.google.code.gson.version>2.8.0</com.google.code.gson.version> <joda-trade.version>2.9.7</joda-trade.version> <commons-dbcp.version>1.4</commons-dbcp.version> <commons-io.version>2.5</commons-io.version> <mybatis-spring-boot-starter.version>1.2.0</mybatis-spring-boot-starter.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis-spring-boot-starter.version}</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>${commons-dbcp.version}</version> </dependency> <dependency> <groupId>com.dangdang</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>${sharding-jdbc.version}</version> </dependency> <dependency> <groupId>com.dangdang</groupId> <artifactId>sharding-jdbc-config-spring</artifactId> <version>${sharding-jdbc.version}</version> </dependency> <dependency> <groupId>com.dangdang</groupId> <artifactId>sharding-jdbc-self-id-generator</artifactId> <version>${sharding-jdbc.version}</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>${com.google.code.gson.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring.boot.version}</version> <exclusions> <exclusion> <artifactId>org.springframework.boot</artifactId> <groupId>spring-boot-start-logging</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring.boot.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> <version>${spring.boot.version}</version> <exclusions> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>${spring.boot.version}</version> <exclusions> <exclusion> <artifactId>org.springframework.boot</artifactId> <groupId>spring-boot-start-logging</groupId> </exclusion> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> <exclusion> <artifactId>log4j-over-slf4j</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>${project.build.jdk}</source> <target>${project.build.jdk}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.4</version> </plugin> </plugins> </build>
其實這個和sharding-jdbc的官網差很少。其實我想寫一個sharding-jdbc-spring-boot-starter
的pom的,等項目業務都作完再說吧。
我想將數據庫作成可配置的,因此我沒有在application.properties
文件中直接配置數據庫,而是寫在了database.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文件,加載方式以下:
@Value("classpath:database.json") private Resource databaseFile; @Bean public List<Database> databases() throws IOException { String databasesString = IOUtils.toString(databaseFile.getInputStream(), Charset.forName("UTF-8")); List<Database> databases = new Gson().fromJson(databasesString, new TypeToken<List<Database>>() { }.getType()); return databases; }
加載完database信息以後,能夠經過工廠方法配置邏輯數據庫:
@Bean public HashMap<String, DataSource> dataSourceMap(List<Database> databases) { Map<String, DataSource> 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; }
這樣就把各個邏輯數據庫就加載好了。
在這個實例中,數據庫的分庫就是根據上海(sh)和深圳(sz)來分的,在sharding-jdbc中是單鍵分片。根據官方文檔實現接口SingleKeyDatabaseShardingAlgorithm
就能夠
@service public class DatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<String> { /** * 根據分片值和SQL的=運算符計算分片結果名稱集合. * * @param availableTargetNames 全部的可用目標名稱集合, 通常是數據源或表名稱 * @param shardingValue 分片值 * * @return 分片後指向的目標名稱, 通常是數據源或表名稱 */ @Override public String doEqualSharding(Collection<String> availableTargetNames, ShardingValue<String> shardingValue) { String databaseName = ""; for (String targetName : availableTargetNames) { if (targetName.endsWith(shardingValue.getValue())) { databaseName = targetName; break; } } return databaseName; } }
此接口還有另外兩個方法,doInSharding
和doBetweenSharding
,由於我暫時不用IN和BETWEEN方法,因此就沒有寫,直接返回null。
數據表的分片策略是根據股票和時間共同決定的,在sharding-jdbc中是多鍵分片。根據官方文檔,實現MultipleKeysTableShardingAlgorithm
接口就OK了
@service public class TableShardingAlgorithm implements MultipleKeysTableShardingAlgorithm { /** * 根據分片值計算分片結果名稱集合. * * @param availableTargetNames 全部的可用目標名稱集合, 通常是數據源或表名稱 * @param shardingValues 分片值集合 * * @return 分片後指向的目標名稱集合, 通常是數據源或表名稱 */ @Override public Collection<String> doSharding(Collection<String> availableTargetNames, Collection<ShardingValue<?>> shardingValues) { String name = null; Date time = null; for (ShardingValue<?> shardingValue : shardingValues) { if (shardingValue.getColumnName().equals("name")) { name = ((ShardingValue<String>) shardingValue).getValue(); } if (shardingValue.getColumnName().equals("time")) { time = ((ShardingValue<Date>) shardingValue).getValue(); } if (name != null && time != null) { break; } } String timeString = new SimpleDateFormat("yyyy_MM").format(time); String suffix = name + "_" + timeString; Collection<String> result = new LinkedHashSet<>(); for (String targetName : availableTargetNames) { if (targetName.endsWith(suffix)) { result.add(targetName); } } return result; } }
這些方法的使用能夠查官方文檔。
以上只是定義了分片算法,尚未造成策略,尚未告訴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<String> columns = new LinkedList<>(); columns.add("name"); columns.add("time"); TableShardingStrategy tableShardingStrategy = new TableShardingStrategy(columns, tableShardingAlgorithm); return tableShardingStrategy; } }
這樣才能造成完成的分片策略。
sharding-jdbc的原理其實很簡單,就是本身作一個DataSource給上層應用使用,這個DataSource包含全部的邏輯庫和邏輯表,應用增刪改查時,他本身再修改sql,而後選擇合適的數據庫繼續操做。因此這個DataSource建立很重要。
@Bean @Primary public DataSource shardingDataSource(HashMap<String, DataSource> 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的多數據源配置
public class Tick { private long id; private String name; private String exchange; private int ask; private int bid; private Date time; }
很簡單,只實現一個插入方法
@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); }
還要設置一下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); } }
成功實現增量分庫分表!!!