纯干货 | Sharding-JDBC分库分表详细讲解,并有完整代码实现

Java
406
0
0
2023-12-04

一:数据库分片方案

  • 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
  • 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat、360的Atlas、网易的DDB等等都是这种架构的实现

二:Sharding-JDBC

Sharding-JDBC:

Sharding-JDBC是一个开源的适用于微服务的分布式数据访问基础类库,它始终以云原生的基础开发套件为目标。

Sharding-JDBC定位为轻量级java框架,使用客户端直连数据库,以jar包形式提供服务,未使用中间层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式,可理解为增强版的JDBC驱动,旧代码迁移成本几乎为零。

Sharding-JDBC完整的实现了分库分表,读写分离和分布式主键功能,并初步实现了柔性事务。从2016年开源至今,在经历了整体架构的数次精炼以及稳定性打磨后,如今它已积累了足够的底蕴,相信可以成为开发者选择技术组件时的一个参考。

1.分库分表

  • SQL 解析功能完善,支持聚合,分组,排序,LIMIT,TOP等查询,并且支持级联表以及笛卡尔积的表查询
  • 支持内、外连接查询
  • 分片策略灵活,可支持=,BETWEEN,IN等多维度分片,也可支持多分片键共用,以及自定义分片策略
  • 基于Hint的强制分库分表路由

2.读写分离

  • 一主多从的读写分离配置,可配合分库分表使用
  • 基于Hint的强制主库路由

3.柔性事务

  • 最大努力送达型事务
  • TCC型事务(TBD)

4.分布式主键

  • 统一的分布式基于时间序列的ID生成器

5.兼容性

  • 可适用于任何基于java的ORM框架,如:JPA, Hibernate, mybatis , Spring JDBC Template或直接使用JDBC
  • 可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid等
  • 理论上可支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL

6.灵活多样的配置

  • Java
  • YAML
  • Inline表达式
  • Spring命名空间
  • Spring boot starter

7.分布式治理能力 (2.0新功能)

  • 配置集中化与动态化,可支持数据源、表与分片策略的动态切换(2.0.0.M1)
  • 客户端的数据库治理,数据源失效自动切换(2.0.0.M2)
  • 基于Open Tracing协议的APM信息输出(2.0.0.M3)

架构图

三:sharding-jdbc + jpa + druid集成

1. 数据库准备

 -- 在db数据库上分别创建t_order_0、t_order_1表
USE db;
DROP TABLE IF EXISTS t_order_; 
CREATE TABLE t_order_ ( 
order_id bigint() NOT NULL, 
user_id bigint() NOT NULL, 
PRIMARY KEY (order_id) 
) ENGINE=InnoDB DEFAULT CHARSET=utf COLLATE=utf8_bin; 
DROP TABLE IF EXISTS t_order_; 
CREATE TABLE t_order_ ( 
order_id bigint() NOT NULL, 
user_id bigint() NOT NULL, 
PRIMARY KEY (order_id) 
) ENGINE=InnoDB DEFAULT CHARSET=utf COLLATE=utf8_bin; 


-- 在db数据库上分别创建t_order_0、t_order_1表
USE db;
DROP TABLE IF EXISTS t_order_; 
CREATE TABLE t_order_ ( 
order_id bigint() NOT NULL, 
user_id bigint() NOT NULL, 
PRIMARY KEY (order_id) 
) ENGINE=InnoDB DEFAULT CHARSET=utf COLLATE=utf8_bin; 

DROP TABLE IF EXISTS t_order_; 
CREATE TABLE t_order_ ( 
order_id bigint() NOT NULL, 
user_id bigint() NOT NULL, 
PRIMARY KEY (order_id) 
) ENGINE=InnoDB DEFAULT CHARSET=utf COLLATE=utf8_bin; 

2. 引入依赖

 <?xml version=".0" encoding="UTF-8"?>
< project  xmlns="#34; xmlns:xsi="#34;
         xsi:schemaLocation=" #;>
    <modelVersion>.0.0</modelVersion>

    <groupId>com.company</groupId>
    <artifactId>sharding-jdbc</artifactId>
    <version>.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>sharding-jdbc</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-</project.reporting.outputEncoding>
        <java.version>.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>.1.41</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>.1.10</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.dangdang</groupId>
            <artifactId>sharding-jdbc-core</artifactId>
            <version>.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>
        </plugins>
    </build>
</project>

注意mysql-connector-java的版本不要太高了

3. application.yml

 spring:
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: none

注意:hibernate.ddl-auto=none 是因为分表就会有多个表,例如torder0、torder1等,而ORM只能映射成一个,所以关闭自动的ddl语句。

4. domain

 @Entity
@Table(name = "t_order")
@Data
public class Order {
    @Id
    private  Long  orderId;

    private Long userId;
}

注意:orderId上使用@Id注解并没有使用@GeneratedValue(strategy = GenerationType.AUTO)的主键生成策略,原因是分表必须要保证所有表的主键id不重复,如果使用mysql的自动生成,那么id就会重复,这里的id一般要使用分布式主键id来通过代码来生成。

5. Repository

 import com.company.shardingjdbc.domain.Order;
import org.springframework.data.repository.CrudRepository;

public interface OrderRepository extends CrudRepository<Order, Long> {
}

6. Controller

 import com.company.shardingjdbc.domain.Order;
import com.company.shardingjdbc.repository.OrderRepository;
import com.dangdang.ddframe.rdb.sharding.keygen.KeyGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private KeyGenerator keyGenerator;

    @RequestMapping("/create")
    public Object add() {
        for (int i =; i < 10; i++) {
            Order order = new Order();
            order.setUserId((long) i);
            order.setOrderId((long) i);
            orderRepository.save(order);
        }
        for (int i =; i < 20; i++) {
            Order order = new Order();
            order.setUserId((long) i +);
            order.setOrderId((long) i);
            orderRepository.save(order);
        }

        //        for (int i =; i < 30; i++) {
        //            Order order = new Order();
        //            order.setOrderId(keyGenerator.generateKey().longValue());
        //            order.setUserId(keyGenerator.generateKey().longValue());
        //            orderRepository.save(order);
        //        }

        return "success";
    }

    @RequestMapping("query")
    private Object queryAll() {
        return orderRepository.findAll();
    }
}

7. Configuration

 package com.company.shardingjdbc.configuration;

import com.alibaba.druid.pool.DruidDataSource;
import com.company.shardingjdbc.common.ModuleDatabaseShardingAlgorithm;
import com.company.shardingjdbc.common.ModuleTableShardingAlgorithm;
import com.dangdang.ddframe.rdb.sharding.api.ShardingDataSourceFactory;
import com.dangdang.ddframe.rdb.sharding.api.rule.DataSourceRule;
import com.dangdang.ddframe.rdb.sharding.api.rule.ShardingRule;
import com.dangdang.ddframe.rdb.sharding.api.rule.TableRule;
import com.dangdang.ddframe.rdb.sharding.api.strategy.database.DatabaseShardingStrategy;
import com.dangdang.ddframe.rdb.sharding.api.strategy.table.TableShardingStrategy;
import com.dangdang.ddframe.rdb.sharding.keygen.DefaultKeyGenerator;
import com.dangdang.ddframe.rdb.sharding.keygen.KeyGenerator;
import com.mysql.jdbc.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;


@Configuration
public class DataSourceConfiguration {
    @Bean
    public DataSource getDataSource() throws SQLException {
        return buildDataSource();
    }

    private DataSource buildDataSource() throws SQLException {
        // 设置分库映射
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        // 添加两个数据库db,db1到map里
        dataSourceMap.put("db", createDataSource("db0"));
        dataSourceMap.put("db", createDataSource("db1"));
        // 设置默认db为db,也就是为那些没有配置分库分表策略的指定的默认库
        // 如果只有一个库,也就是不需要分库的话,map里只放一个映射就行了,只有一个库时不需要指定默认库,但个及以上时必须指定默认库,否则那些没有配置策略的表将无法操作数据
        DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "db");

        // 设置分表映射,将t_order_和t_order_1两个实际的表映射到t_order逻辑表
        //和1两个表是真实的表,t_order是个虚拟不存在的表,只是供使用。如查询所有数据就是 SELECT  * from t_order就能查完0和1表的
        TableRule orderTableRule = TableRule.builder("t_order")
                .actualTables(Arrays.asList("t_order_", "t_order_1"))
                .dataSourceRule(dataSourceRule)
                .build();

        // 具体分库分表策略,按什么规则来分
        ShardingRule shardingRule = ShardingRule.builder()
                .dataSourceRule(dataSourceRule)
                .tableRules(Arrays.asList(orderTableRule))
                .databaseShardingStrategy(new DatabaseShardingStrategy("user_id", new ModuleDatabaseShardingAlgorithm()))
                .tableShardingStrategy(new TableShardingStrategy("order_id", new ModuleTableShardingAlgorithm())).build();

        DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);

        return dataSource;
    }

    private static DataSource createDataSource(final String dataSourceName) {
        // 使用druid连接数据库
        DruidDataSource result = new DruidDataSource();
        result.setDriverClassName(Driver.class.getName());
        result.setUrl(String.format("jdbc:mysql://localhost:/%s", dataSourceName));
        result.setUsername("root");
        result.setPassword("root");
        return result;
    }

    @Bean
    public KeyGenerator keyGenerator() {
        return new DefaultKeyGenerator();
    }
}

ModuleDatabaseShardingAlgorithm

 package com.company.shardingjdbc.common;

import com.dangdang.ddframe.rdb.sharding.api.ShardingValue;
import com.dangdang.ddframe.rdb.sharding.api.strategy.database.SingleKeyDatabaseShardingAlgorithm;
import com.google.common.collect.Range;

import java.util.Collection;
import java.util.LinkedHashSet;

/**
 * 单键数据库分片算法.
 *
 * 支持单键和多键策略
 * <ul>
 *     <li>单键 SingleKeyDatabaseShardingAlgorithm</li>
 *     <li>多键 MultipleKeysDatabaseShardingAlgorithm</li>
 * </ul>
 *
 * 支持的分片策略
 * <ul>
 *     <li> = doEqualSharding 例如 where order_id = </li>
 *     <li> IN doInSharding 例如 where order_id in (, 2)</li>
 *     <li> BETWEEN doBetweenSharding 例如 where order_id between and 2 </li>
 * </ul>
 *
 * @author mengday
 */
public class ModuleDatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<Long> {

    /**
     * 分片策略 相等=
     * @param availableTargetNames 可用的目标名字(这里指数据名db、db1)
     * @param shardingValue 分片值[logicTableName="t_order" 逻辑表名, columnName="user_id" 分片的列名, value="" 分片的列名对应的值(user_id=20)]
     * @return
     */
    @Override
    public String doEqualSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
        for (String each : availableTargetNames) {
            if (each.endsWith(shardingValue.getValue() % + "")) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }

    @Override
    public Collection<String> doInSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        for (Long value : shardingValue.getValues()) {
            for (String tableName : availableTargetNames) {
                if (tableName.endsWith(value % + "")) {
                    result.add(tableName);
                }
            }
        }
        return result;
    }

    @Override
    public Collection<String> doBetweenSharding(Collection<String> availableTargetNames,
                                                ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        Range<Long> range = shardingValue.getValueRange();
        for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
            for (String each : availableTargetNames) {
                if (each.endsWith(i % + "")) {
                    result.add(each);
                }
            }
        }
        return result;
    }
}
 package com.company.shardingjdbc.common;

import com.dangdang.ddframe.rdb.sharding.api.ShardingValue;
import com.dangdang.ddframe.rdb.sharding.api.strategy.table.SingleKeyTableShardingAlgorithm;
import com.google.common.collect.Range;

import java.util.Collection;
import java.util.LinkedHashSet;

public final class ModuleTableShardingAlgorithm implements SingleKeyTableShardingAlgorithm<Long> {

    /**
     * doEqualSharding =
     * @param tableNames 实际物理表名
     * @param shardingValue [logicTableName="t_order", columnName="order_id", value=]
     * 
     *  select * from t_order from t_order where order_id =
     *          └── SELECT *  FROM t_order_ WHERE order_id = 11
     *  select * from t_order from t_order where order_id =
     *          └── SELECT *  FROM t_order_ WHERE order_id = 44
     */
     *  select * from t_order from t_order where order_id =
     *          └── SELECT *  FROM t_order_ WHERE order_id = 11
     *  select * from t_order from t_order where order_id =
     *          └── SELECT *  FROM t_order_ WHERE order_id = 44
     */
    public String doEqualSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
        for (String each : tableNames) {
            if (each.endsWith(shardingValue.getValue() % + "")) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }

    /**
     *  select * from t_order from t_order where order_id in (,44)
     *          ├── SELECT *  FROM t_order_ WHERE order_id IN (11,44)
     *          └── SELECT *  FROM t_order_ WHERE order_id IN (11,44)
     *  select * from t_order from t_order where order_id in (,13,15)
     *          └── SELECT *  FROM t_order_ WHERE order_id IN (11,13,15)
     *  select * from t_order from t_order where order_id in (,24,26)
     *          └──SELECT *  FROM t_order_ WHERE order_id IN (22,24,26)
     */
    public Collection<String> doInSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(tableNames.size());
        for (Long value : shardingValue.getValues()) {
            for (String tableName : tableNames) {
                if (tableName.endsWith(value % + "")) {
                    result.add(tableName);
                }
            }
        }
        return result;
    }
    /**
     *  select * from t_order from t_order where order_id between and 20
     *          ├── SELECT *  FROM t_order_ WHERE order_id BETWEEN 10 AND 20
     *          └── SELECT *  FROM t_order_ WHERE order_id BETWEEN 10 AND 20
     */
    public Collection<String> doBetweenSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(tableNames.size());
        Range<Long> range = shardingValue.getValueRange();
        for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
            for (String each : tableNames) {
                if (each.endsWith(i % + "")) {
                    result.add(each);
                }
            }
        }
        return result;
    }
}

8. localhost:8080/order/create

db0 ├── torder0 userid为偶数 orderid为偶数 ├── torder1 userid为偶数 orderid为奇数 db1 ├── torder0 userid为奇数 orderid为偶数 ├── torder1 userid为奇数 orderid为奇数

纯干货 | Sharding-JDBC分库分表详细讲解,并有完整代码实现

纯干货 | Sharding-JDBC分库分表详细讲解,并有完整代码实现

纯干货 | Sharding-JDBC分库分表详细讲解,并有完整代码实现

纯干货 | Sharding-JDBC分库分表详细讲解,并有完整代码实现

四:sharding-jdbc + mybatis + druid集成

此示例是在jap原有的集成上集成mybatis

1. 引入mybatis依赖
 <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>.3.2</version>
</dependency>
2. 在Application上添加注解@MapperScan
 @MapperScan("com.company.shardingjdbc.mapper")
@SpringBootApplication
public class ShardingJdbcApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShardingJdbcApplication.class, args);
    }
}
3. application.yml
 # Mybatis 配置
mybatis:
  typeAliasesPackage: com.company.shardingjdbc.domain
  mapperLocations: classpath:mapper/*.xml
  configuration.map-underscore-to-camel-case: true

# 打印mybatis中的sql语句和结果集
logging:
  level.com.company.shardingjdbc.mapper: TRACE
4. OrderMapper
 import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface OrderMapper {

    void insert(Order order);

    List<Order> queryById(@Param("orderIdList") List<Long> orderIdList);
}
5. OrderMapper.xml
 <?xml version=".0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper.0//EN" "#34; >
<mapper namespace="com.company.shardingjdbc.mapper.OrderMapper" >
    <select id="queryById" parameterType="Long" resultType="Order">
        SELECT * FROM t_order WHERE order_id IN
        <foreach collection="orderIdList" item="orderId" open="(" separator="," close=")">
            #{orderId}
        </foreach>
    </select>

    <insert id="insert" parameterType="Order">
        INSERT INTO t_order (order_id, user_id) VALUES (#{orderId}, #{userId})
    </insert>
</mapper>
6. OrderController
 import com.dangdang.ddframe.rdb.sharding.keygen.KeyGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderMapper orderMapper;

    @RequestMapping("/insert")
    public Object insert() {
        for (int i =; i < 30; i++) {
            Order order = new Order();
            order.setUserId((long) i);
            order.setOrderId((long) i);
            orderMapper.insert(order);
        }
        for (int i =; i < 40; i++) {
            Order order = new Order();
            order.setUserId((long) i +);
            order.setOrderId((long) i);
            orderMapper.insert(order);
        }

        return "success";
    }

    @RequestMapping("queryById")
    public List<Order> queryById(String orderIds) {
        List<String> strings = Arrays.asList(orderIds.split(","));
        List<Long> orderIdList = strings.stream().map(item -> Long.parseLong(item)).collect(Collectors.toList());
        return orderMapper.queryById(orderIdList);
    }
}
7. 插入数据

localhost:8080/order/insert

  • ModuleDatabaseShardingAlgorithm: 先根据分片键user_id及值来确定要操作的数据库是db0还是db1
  • ModuleTableShardingAlgorithm: 再根据分片键orderid及值来确定要操作的数据库对应的表是torder0还是torder_1
  • 当数据库名和表名都确定了就可以操作数据库了

localhost:8080/order/queryById?orderIds=20,31,30,21