Sharding-Jdbc+Mybatis+SpringBoot的分库分表实现
1、sharding jdbc简介
Sharding-JDBC定位为轻量级java框架,使用客户端直连数据库,以jar包形式提供服务,未使用中间层,无需额外
部署,无其他依赖,DBA也无需改变原有的运维方式,可理解为增强版的JDBC驱动,旧代码迁移成本几乎为零。
它主要的功能:分库分表;读写分离; 柔性事务;分布式主键;兼容性;灵活多样的配置;分布式治理能力 (2.0
新功能)。
前两个功能无需多说,柔性事务主要表现在:最大努力送达型事务,TCC型事务(TBD);分布式主键也不需要多
说,兼容性主要体现在:可适用于任何基于java的ORM框架,如:JPA,Hibernate,Mybatis,Spring JDBC
Template或直接使用JDBC,可基于任何第三方的数据库连接池,如:DBCP,C3P0,BoneCP, Druid等,理论上
可支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL;灵活多样的配置主要
支持这些配置Java,YAML,Inline表达式,Spring命名空间,Spring boot starter。
分布式治理能力: 配置集中化与动态化,可支持数据源、表与分片策略的动态切换(2.0.0.M1), 客户端的数据库
治理,数据源失效自动切换(2.0.0.M2),。
当当网的sharding jdbc抽离出来了,由Apache负责,所以我的依赖从dangdang的:
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>1.3.3</version>
</dependency>
变为了:
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>2.0.3</version>
</dependency>
2、开始动手
下面进行sharding jdbc
的分库分表。
2.1 建库建表
建立数据库和表(分别建了两个库两张表):
CREATE DATABASE `user_0`;
use `user_0`;
CREATE TABLE `user_info_1` (
`user_id` bigint(19) NOT NULL,
`user_name` varchar(45) DEFAULT NULL,
`account` varchar(45) NOT NULL,
`password` varchar(45) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_info_0` (
`user_id` bigint(19) NOT NULL,
`user_name` varchar(45) DEFAULT NULL,
`account` varchar(45) NOT NULL,
`password` varchar(45) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE DATABASE `user_1`;
use `user_1`;
CREATE TABLE `user_info_1` (
`user_id` bigint(19) NOT NULL,
`user_name` varchar(45) DEFAULT NULL,
`account` varchar(45) NOT NULL,
`password` varchar(45) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_info_0` (
`user_id` bigint(19) NOT NULL,
`user_name` varchar(45) DEFAULT NULL,
`account` varchar(45) NOT NULL,
`password` varchar(45) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2 pom依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-shardingjdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shardingjdbc</name>
<description>springboot-shardingjdbc</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.3 mybatis的配置
我创建一个entity包(实体包),里面有一个UserInfo
的类:
package com.example.entity;
public class UserInfo {
private Long userId;
private String userName;
private String account;
private String password;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public void setUserName(String userName) {
this.userName = userName == null ? null : userName.trim();
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account == null ? null : account.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
}
然后我有一个mapper包(映射包),里面有一个映射类UserInfoMapper
:
package com.example.mapper;
import com.example.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper {
int insert(UserInfo record);
int insertSelective(UserInfo record);
UserInfo selectByPrimaryKey(Long userId);
int updateByPrimaryKeySelective(UserInfo record);
int updateByPrimaryKey(UserInfo record);
}
还有再resources目录下的mapper目录中的UserInfoMapper.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserInfoMapper">
<resultMap id="BaseResultMap" type="com.example.entity.UserInfo">
<id column="user_id" jdbcType="BIGINT" property="userId" />
<result column="user_name" jdbcType="VARCHAR" property="userName" />
<result column="account" jdbcType="VARCHAR" property="account" />
<result column="password" jdbcType="VARCHAR" property="password" />
</resultMap>
<sql id="Base_Column_List">
user_id, user_name, account, password
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user_info
where user_id = #{userId,jdbcType=BIGINT}
</select>
<insert id="insert" parameterType="com.example.entity.UserInfo">
insert into user_info (user_id, user_name, account,
password)
values (#{userId,jdbcType=BIGINT}, #{userName,jdbcType=VARCHAR}, #{account,jdbcType=VARCHAR},
#{password,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" parameterType="com.example.entity.UserInfo">
insert into user_info
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">
user_id,
</if>
<if test="userName != null">
user_name,
</if>
<if test="account != null">
account,
</if>
<if test="password != null">
password,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">
#{userId,jdbcType=BIGINT},
</if>
<if test="userName != null">
#{userName,jdbcType=VARCHAR},
</if>
<if test="account != null">
#{account,jdbcType=VARCHAR},
</if>
<if test="password != null">
#{password,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.example.entity.UserInfo">
update user_info
<set>
<if test="userName != null">
user_name = #{userName,jdbcType=VARCHAR},
</if>
<if test="account != null">
account = #{account,jdbcType=VARCHAR},
</if>
<if test="password != null">
password = #{password,jdbcType=VARCHAR},
</if>
</set>
where user_id = #{userId,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="com.example.entity.UserInfo">
update user_info
set user_name = #{userName,jdbcType=VARCHAR},
account = #{account,jdbcType=VARCHAR},
password = #{password,jdbcType=VARCHAR}
where user_id = #{userId,jdbcType=BIGINT}
</update>
</mapper>
mybatis-config.xml
配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<mappers>
<mapper resource="mapper/UserInfoMapper.xml"/>
</mappers>
</configuration>
2.4 sharding jdbc的配置
首先,我们先实现我们的分库分表的策略分库策略的类,DemoDatabaseShardingAlgorithm
package com.example.config;
import io.shardingjdbc.core.api.algorithm.sharding.PreciseShardingValue;
import io.shardingjdbc.core.api.algorithm.sharding.standard.PreciseShardingAlgorithm;
import java.util.Collection;
public class DemoDatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
for (String each : collection) {
System.out.println("database:" + each);
if (each.endsWith(Long.parseLong(preciseShardingValue.getValue().toString()) % 2 + "")) {
return each;
}
}
throw new IllegalArgumentException();
}
}
使用io.shardingjdbc,就应该实现PreciseShardingAlgorithm
接口,然后实现doSharding
方法,对应SQL中
的=、IN,还有RangeShardingAlgorithm
接口中,对应SQL中的BETWEEN AND,因为我只需要=、in操作,所
以只实现了PreciseShardingAlgorithm接口,你如果都需要,你可以都实现。
如果你使用的当当网的sharding jdbc,那么你需要实现SingleKeyDatabaseShardingAlgorithm
这个接口,实
现其中的三个方法。
分表策略的类,DemoTableShardingAlgorithm
:
package com.example.config;
import io.shardingjdbc.core.api.algorithm.sharding.PreciseShardingValue;
import io.shardingjdbc.core.api.algorithm.sharding.standard.PreciseShardingAlgorithm;
import java.util.Collection;
public class DemoTableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
for (String each : collection) {
System.out.println("table:" + each);
if (each.endsWith(Long.parseLong(preciseShardingValue.getValue().toString()) % 2 + "")) {
return each;
}
}
throw new IllegalArgumentException();
}
}
与分库的步骤一致,也是需要实现PreciseShardingAlgorithm
和RangeShardingAlgorithm
两个接口的类。
剩下的就是最重要的部分,sharding jdbc的配置:
DataSourceConfig
:
package com.example.config;
import io.shardingjdbc.core.api.config.ShardingRuleConfiguration;
import io.shardingjdbc.core.api.config.TableRuleConfiguration;
import io.shardingjdbc.core.api.config.strategy.StandardShardingStrategyConfiguration;
import io.shardingjdbc.core.jdbc.core.datasource.ShardingDataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper", sqlSessionTemplateRef = "testSqlSessionTemplate")
public class DataSourceConfig {
@Bean(name = "shardingDataSource")
DataSource getShardingDataSource() throws SQLException {
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(getUserTableRuleConfiguration());
shardingRuleConfig.getBindingTableGroups().add("user_info");
shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("user_id", DemoDatabaseShardingAlgorithm.class.getName()));
shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("user_id", DemoTableShardingAlgorithm.class.getName()));
return new ShardingDataSource(shardingRuleConfig.build(createDataSourceMap()));
}
@Bean
TableRuleConfiguration getUserTableRuleConfiguration() {
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration();
orderTableRuleConfig.setLogicTable("user_info");
orderTableRuleConfig.setActualDataNodes("user_${0..1}.user_info_${0..1}");
orderTableRuleConfig.setKeyGeneratorColumnName("user_id");
return orderTableRuleConfig;
}
/**
* 需要手动配置事务管理器
*
* @param shardingDataSource
* @return
*/
@Bean
public DataSourceTransactionManager transactitonManager(DataSource shardingDataSource) {
return new DataSourceTransactionManager(shardingDataSource);
}
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory(DataSource shardingDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(shardingDataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return bean.getObject();
}
@Bean
@Primary
public SqlSessionTemplate testSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
private Map<String, DataSource> createDataSourceMap() {
Map<String, DataSource> result = new HashMap<>();
result.put("user_0", createDataSource("user_0"));
result.put("user_1", createDataSource("user_1"));
return result;
}
private DataSource createDataSource(final String dataSourceName) {
BasicDataSource result = new BasicDataSource();
result.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName));
result.setUsername("root");
result.setPassword("root");
return result;
}
}
2.5 启动类
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement(proxyTargetClass = true)
public class SpringbootShardingjdbcApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootShardingjdbcApplication.class, args);
}
}
2.6 配置文件
mybatis.config-location=classpath:mybatis-config.xml
mybatis.mapper-locations=classpath:mapper/*.xml
#datasource
spring.devtools.remote.restart.enabled=false
#data source1
spring.datasource.user0.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.user0.url=jdbc:mysql://localhost:3306/user_0
spring.datasource.user0.username=root
spring.datasource.user0.password=root
#data source2
spring.datasource.user1.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.user1.url=jdbc:mysql://localhost:3306/user_1
spring.datasource.user1.username=root
spring.datasource.user1.password=root
2.7 测试
我写了一个测试服务和一个测试类
测试服务:DemoService
package com.example.service;
import com.example.entity.UserInfo;
import com.example.mapper.UserInfoMapper;
import groovy.util.logging.Slf4j;
import io.shardingjdbc.core.api.HintManager;
import io.shardingjdbc.core.hint.HintManagerHolder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service
public class DemoService {
@Resource
UserInfoMapper userInfoMapper;
public static Long userId = 150L;
public void demo() {
for (int i = 1; i <= 40; i++) {
UserInfo userInfo = new UserInfo();
userInfo.setUserId(userId);
userInfo.setAccount("Account" + i);
userInfo.setPassword("pass" + i);
userInfo.setUserName("name" + i);
userId++;
if (i == 3) {
HintManagerHolder.clear();
HintManager hintManager = HintManager.getInstance();
hintManager.addDatabaseShardingValue("user_info", "user_id", 3L);
hintManager.addTableShardingValue("user_info", "user_id", 3L);
}
userInfoMapper.insert(userInfo);
}
}
}
测试类DemoApplicationTests
package com.example;
import com.example.mapper.UserInfoMapper;
import com.example.service.DemoService;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
class SpringbootShardingjdbcApplicationTests {
@Resource
UserInfoMapper userInfoMaper;
@Resource
DemoService demoService;
@Test
public void contextLoads() {
demoService.demo();
}
}
查询数据库查看结果: