Sharding-JDBC是当当开源的用于分库分表的基础类库。定位轻量级java框架,可以通过客户端直接连接数据库,只需要在增加额外的数据源配置就可以轻松实现完整的分库分表功能。
Sharding-JDBC是一个开源的适用于微服务的分布式数据访问基础类库,它始终以云原生的基础开发套件为目标。
目前Sharding-JDBC已经实现的功能包括(最新稳定版本为 2.0.0.M1
):
下面的例子都是基于Spring Boot开发: Spring Boot + Mybatis + Druid + Sharding-Jdbc
Application
是项目的启动入口
DataSourceConfig
是数据源配置,包括如何结合Sharding-Jdbc设置分库分表
algorithm
下面是设置的分库分表策略,比如当 city_id % 2
为偶数放在库A,否则放在库B
UserMapper
是Mybatis的接口,由于采用了全注解配置,所以没有Mapper文件
druid
下面是druid的监控页面配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-boot-learning-examples</artifactId> <groupId>com.rhwayfun</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>spring-boot-mybatis-sharding-jdbc</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>com.dangdang</groupId> <artifactId>sharding-jdbc-core</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
使用Sharding-Jdbc需要依赖 sharding-jdbc-core
,其他都是Spring Boot相关的依赖。
/** * 数据源配置 * * @author happyxiaofan * @since 0.0.1 */ @Configuration @ConfigurationProperties(prefix = DataSourceConstants.DATASOURCE_PREFIX) @MapperScan(basePackages = { DataSourceConstants.MAPPER_PACKAGE }, sqlSessionFactoryRef = "mybatisSqlSessionFactory") public class DataSourceConfig { private String url; private String username; private String password; @Bean(name = "mybatisDataSource") public DataSource getDataSource() throws SQLException { //设置分库映射 Map<String, DataSource> dataSourceMap = new HashMap<>(2); dataSourceMap.put("springboot_0", mybatisDataSource("springboot")); dataSourceMap.put("springboot_1", mybatisDataSource("springboot2")); //设置默认库,两个库以上时必须设置默认库。默认库的数据源名称必须是dataSourceMap的key之一 DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "springboot_0"); //设置分表映射 TableRule userTableRule = TableRule.builder("user") .generateKeyColumn("user_id") //将user_id作为分布式主键 .actualTables(Arrays.asList("user_0", "user_1")) .dataSourceRule(dataSourceRule) .build(); //具体分库分表策略 ShardingRule shardingRule = ShardingRule.builder() .dataSourceRule(dataSourceRule) .tableRules(Collections.singletonList(userTableRule)) .databaseShardingStrategy(new DatabaseShardingStrategy("city_id", new ModuloDatabaseShardingAlgorithm())) .tableShardingStrategy(new TableShardingStrategy("user_id", new ModuloTableShardingAlgorithm())).build(); DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule); //return new ShardingDataSource(shardingRule); return dataSource; } private DataSource mybatisDataSource(final String dataSourceName) throws SQLException { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(DataSourceConstants.DRIVER_CLASS); dataSource.setUrl(String.format(url, dataSourceName)); dataSource.setUsername(username); dataSource.setPassword(password); /* 配置初始化大小、最小、最大 */ dataSource.setInitialSize(1); dataSource.setMinIdle(1); dataSource.setMaxActive(20); /* 配置获取连接等待超时的时间 */ dataSource.setMaxWait(60000); /* 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ dataSource.setTimeBetweenEvictionRunsMillis(60000); /* 配置一个连接在池中最小生存的时间,单位是毫秒 */ dataSource.setMinEvictableIdleTimeMillis(300000); dataSource.setValidationQuery("SELECT 'x'"); dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(false); dataSource.setTestOnReturn(false); /* 打开PSCache,并且指定每个连接上PSCache的大小。 如果用Oracle,则把poolPreparedStatements配置为true, mysql可以配置为false。分库分表较多的数据库,建议配置为false */ dataSource.setPoolPreparedStatements(false); dataSource.setMaxPoolPreparedStatementPerConnectionSize(20); /* 配置监控统计拦截的filters */ dataSource.setFilters("stat,wall,log4j"); return dataSource; } /** * Sharding-jdbc的事务支持 * * @return */ @Bean(name = "mybatisTransactionManager") public DataSourceTransactionManager mybatisTransactionManager(@Qualifier("mybatisDataSource") DataSource dataSource) throws SQLException { return new DataSourceTransactionManager(dataSource); } @Bean(name = "mybatisSqlSessionFactory") public SqlSessionFactory mybatisSqlSessionFactory(@Qualifier("mybatisDataSource") DataSource mybatisDataSource) throws Exception { final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(mybatisDataSource); return sessionFactory.getObject(); } // 省略setter、getter }
分库规则,按照 city_id
分库:
如果cityId mod 2 为0,则落在springboot_0,也即是springboot,
如果cityId mod 2 为1,则落在springboot_1,也即是springboot2
分表规则,按照 user_id
分表:
如果userId mod 2 为0,则落在表user_0,
如果userId mod 2 为1,则落在表user_1
上面指定了两个数据库 springboot
和 springboot2
,对应的key分别是 springboot_0
和 springboot_1
,在具体执行数据库写入的时候会先根据分库算法确定写入到哪个库(springboot或者springboot2),再根据分表算法确定写入最终哪个表( user_0或user_1
)。所以这里两个数据库都有两个表,这里以 user
表作为示例,表结构如下:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户id', `city_id` int(11) DEFAULT NULL COMMENT '城市id', `user_name` varchar(15) DEFAULT NULL, `age` int(11) DEFAULT NULL COMMENT '年龄', `birth` date DEFAULT NULL COMMENT '生日', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1
在设置分表映射的时候,我们将 user_id
作为分布式主键,但是我们在创建表的时候却将id作为了自增主键,这两个是什么关系呢,用MySQL的自增主键不行么。因为同一个逻辑表(这里的逻辑表是 user
)内的不同实际表(这里的实际表是 user_0
和 user_1
)之间的自增键是无法互相感知的,这样会造成重复Id的生成。而Sharding-Jdbc的分布式主键保证了数据库进行分库分表后主键(这里的 user_id
)一定是唯一不重复的,这样就解决了生成重复Id的问题。
注意下面这段代码:
ShardingRule shardingRule = ShardingRule.builder() .dataSourceRule(dataSourceRule) .tableRules(Collections.singletonList(userTableRule)) .databaseShardingStrategy(new DatabaseShardingStrategy("city_id", new ModuloDatabaseShardingAlgorithm())) .tableShardingStrategy(new TableShardingStrategy("user_id", new ModuloTableShardingAlgorithm())).build();
这里指定了具体的分库分表策略,我们要实现的是根据 city_id
分库,根据 user_id
分表。 ModuloDatabaseShardingAlgorithm
表示具体分库的算法, TableShardingStrategy
表示具体的分表的算法。
我们可以先假设下,如果插入下面这条数据,按照之前确定的分库分表规则,肯定是落在 springboot2
库,但是无法实现确定落在哪个表,因为我们将 user_id
作为了分布式主键,主键是由Sharding-Jdbc内部先生成的,所以可能会落在 user_0
或 user_1
:
@Test public void getOneSlave() throws Exception { UserEntity user = new UserEntity(); user.setCityId(1);//1 mod 2 = 1,所以会落在springboot2库中 user.setUserName("insertTest"); user.setAge(10); user.setBirth(new Date()); assertTrue(userMapper.insertSlave(user) > 0); Long userId = user.getUserId(); System.out.println("Generated Key--userId:" + userId + "mod:" + 1 % 2); UserEntity one = userMapper.getOne(userId); System.out.println("Generated User:" + one); assertEquals("insertTest", one.getUserName()); //assertTrue(userMapper.delete(userId) > 0); }
通过user.getUserId可以获取Sharding-Jdbc的生成的主键,运行这个测试用例,可以看到生成主键为 131004387576250368
,通过计算131004387576250368 mod 2 = 0,所以这条记录在表 user_0
。
验证如下:
以上例子所有的代码都在GitHub仓库中: https://github.com/rhwayfun/spring-boot-learning-examples