目录

sharding-jdbc数据分片

水平分表

前面已经介绍过,水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中。在快速入门里,我 们已经对水平分库进行实现,这里不再重复介绍。

水平分库

前面已经介绍过,水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器 上。接下来看一下如何使用Sharding-JDBC实现水平分库,在分库的同时进行分表。咱们继续对快速入门中的例子进行完善。

将原有order_db库拆分为order_db_1、order_db_2

库表如下:

http://img.cana.space/picStore/20201123003411.png

分片规则修改

由于数据库拆分了两个,这里需要配置两个数据源。

分库需要配置分库的策略,和分表策略的意义类似,通过分库策略实现数据操作针对分库的数据库进行操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
debug=true
server.port=8001
spring.application.name=sharding-jdbc-simple
# 使用bean定义覆盖,sharding-jdbc的datasource覆盖druid定义的datasource,如果不覆盖会报
# Invalid bean definition with name 'dataSource' defined in class path resource
spring.main.allow-bean-definition-overriding = true
# 以下是分片规则配置
# 定义数据源
# 数据源名称
spring.shardingsphere.datasource.names=m1,m2
# 给m1数据源设置连接属性
spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=333
# 给m2数据源设置连接属性
spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://localhost:3306/order_db_2?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=333

# 指定分库策略
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=m$->{user_id % 2 + 1}

# 指定t_order表的数据分布情况,配置数据节点
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=m$->{1..2}.t_order_$->{1..2}
# 指定t_order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
# 指定t_order表的分片策略,分片策略包括分片键和分片算法
# 分片键
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
# 分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=\
  t_order_$->{order_id % 2 + 1}
# 打开sql输出日志
spring.shardingsphere.props.sql.show=true

测试类:

1
2
3
4
5
6
@Test
    public void insertOrder() {
        for (int i = 1; i < 20; i++) {
            orderMapper.insertOrder(new BigDecimal(i), (long) (Math.random() * 100 + 1), "SUCCESS");
        }
    }

日志:

1
2
3
4
5
6
7
8
...
m1 ::: insert into t_order_1 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [12, 76, SUCCESS, 537430056809529344]
...
m2 ::: insert into t_order_2 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [13, 51, SUCCESS, 537430056851472385]
...
m2 ::: insert into t_order_1 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [14, 69, SUCCESS, 537430056885026816]
...
m1 ::: insert into t_order_1 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [4, 56, SUCCESS, 537430056515928064]

可以看到四个数据节点都插入了相应的数据

查看数据库:以数据节点order_db_1.t_order_1为例,插入的都是user_id为偶数且order_id也为偶数的数据

http://img.cana.space/picStore/20201123003933.png

分库策略定义方式如下:

1
2
3
4
5
#分库策略,如何将一个逻辑表映射到多个数据源
spring.shardingsphere.sharding.tables.<逻辑表名称>.database‐strategy.<分片策略>.<分片策略属性名>= # 分片策略属性值

#分表策略,如何将一个逻辑表映射为多个实际表
spring.shardingsphere.sharding.tables.<逻辑表名称>.table‐strategy.<分片策略>.<分片策略属性名>= #分 片策略属性值

Sharding-JDBC支持以下几种分片策略:

不管理分库还是分表,策略基本一样。

  • standard :标准分片策略,对应StandardShardingStrategy。提供对SQL语句中的=, IN和BETWEEN AND的 分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和 RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。 RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置 RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
  • complex :符合分片策略,对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, IN和 BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复 杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发 者实现,提供最大的灵活度。
  • inline :行表达式分片策略,对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和 IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java 代码开发,如: t_user_$ ->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为 t_user_0 到 t_user_7 。
  • hint :Hint分片策略,对应HintShardingStrategy。通过Hint而非SQL解析的方式分片的策略。对于分片字段 非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工 登录主键分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释(待实现)两种方式使用。
  • none :不分片策略,对应NoneShardingStrategy。不分片的策略。

目前例子中都使用inline分片策略,若对其他分片策略细节若感兴趣,请查阅官方文档: https://shardingsphere.apache.org

查询测试

单个订单,id是偶数

1
2
3
4
5
6
7
8
9
@Test
    public void testSelectOrderByIds() {
        List<Long> ids = new ArrayList<>();
        ids.add(537337422749368320L);
//        ids.add(537337421923090433L);

        List<Map> maps = orderMapper.selectOrderByIds(ids);
        System.out.println(maps);
    }

日志:

http://img.cana.space/picStore/20201123004854.png

两个订单,id分别是奇数和偶数

1
2
3
4
5
6
7
8
9
@Test
public void testSelectOrderByIds() {
    List<Long> ids = new ArrayList<>();
    ids.add(537337422749368320L);
    ids.add(537337421923090433L);

    List<Map> maps = orderMapper.selectOrderByIds(ids);
    System.out.println(maps);
}

日志:

http://img.cana.space/picStore/20201123004938.png

由于查询语句中并没有使用分片键user_id,所以sharding-jdbc将广播路由到每个物理库。

下边我们在sql中添加userId分片键进行查询。

OrderMapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Select("<script>" +
        "select" +
        " * " +
        " from t_order t " +
        " where t.order_id in " +
        " <foreach collection='orderIds' open='(' separator=',' close=')' item='id'>" +
        " #{id} " +
        " </foreach>" +
        "and t.user_id = #{userId}" +
        "</script>")
List<Map> selectOrderByUserIdAndIds(@Param("userId") Integer userId, @Param("orderIds") List<Long> orderIds);

测试程序:

1
2
3
4
5
6
7
8
9
@Test
    public void testSelectOrderByIds() {
        List<Long> ids = new ArrayList<>();
        ids.add(537337422749368320L);
//        ids.add(537337421923090433L);

        List<Map> maps = orderMapper.selectOrderByUserIdAndIds(1, ids);
        System.out.println(maps);
    }

日志:

http://img.cana.space/picStore/20201123005616.png

跟我们预想的一样,只会发出一条真实sql

综上,在使用查询条件的时候尽量指定完整的分片键,这样缩小路由的数据节点范围。

垂直分库

前面已经介绍过,垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器 上,它的核心理念是专库专用。接下来看一下如何使用Sharding-JDBC实现垂直分库。

数据准备

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- 创建数据库user_db
CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

-- 在user_db中创建t_user表
DROP TABLE
IF
	EXISTS `t_user`;
CREATE TABLE `t_user` (
	`user_id` BIGINT ( 20 ) NOT NULL COMMENT '用户id',
	`fullname` VARCHAR ( 255 ) CHARACTER 
	SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户姓名',
	`user_type` CHAR ( 1 ) DEFAULT NULL COMMENT '用户类型',
	PRIMARY KEY ( `user_id` ) USING BTREE 
) ENGINE = INNODB CHARACTER 
SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

sharding-jdbc配置

由于没有分库分表,只需要配置真实数据节点节点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 数据源名称
spring.shardingsphere.datasource.names=m0,m1,m2
...
# 给m0数据源设置连接属性
spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url=jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.m0.username=root
spring.shardingsphere.datasource.m0.password=333
...
# 逻辑表t_user只对应一个真实表t_user
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes=m0.t_user

数据操作,新增UserMapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.eh.shardingjdbc.order.dao;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;
import java.util.Map;

public interface UserMapper {
    /**
     * 新增用户 * @param userId 用户id * @param fullname 用户姓名 * @return
     */
    @Insert("insert into t_user(user_id, fullname) value(#{userId},#{fullname})")
    int insertUser(@Param("userId") Long userId, @Param("fullname") String fullname);

    /**
     * 根据id列表查询多个用户 * @param userIds 用户id列表 * @return
     */
    @Select({"<script>",
            " select",
            " * ",
            " from t_user t ",
            " where t.user_id in",
            "<foreach collection='userIds' item='id' open='(' separator=',' close=')'>",
            "#{id}",
            "</foreach>",
            "</script>"
    })
    List<Map> selectUserByIds(@Param("userIds") List<Long> userIds);
}

测试,新增单元测试方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Test
public void testInsertUser() {
    for (int i = 0; i < 10; i++) {
        Long id = i + 1L;
        userMapper.insertUser(id, "姓名" + id);
    }
}

@Test
public void testSelectUserByIds() {
    List<Long> userIds = new ArrayList<>();
    userIds.add(1L);
    userIds.add(2L);
    List<Map> users = userMapper.selectUserByIds(userIds);
    System.out.println(users);
}

插入:

http://img.cana.space/picStore/20201123104417.png

通过日志可以看出t_user表的数据被落在了m0数据源,达到目标。

查询:

http://img.cana.space/picStore/20201123104504.png

通过日志可以看出t_user表的查询操作被落在了m0数据源,达到目标。

公共表

公共表属于系统中数据量较小,变动少,而且属于高频联合查询的依赖表。参数表、数据字典表等属于此类型。可 以将这类表在每个数据库都保存一份,所有更新操作都同时发送到所有分库执行。接下来看一下如何使用 Sharding-JDBC实现公共表。

数据准备

分别在user_db、order_db_1、order_db_2中创建t_dict表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CREATE TABLE `t_dict` (
	`dict_id` BIGINT ( 20 ) NOT NULL COMMENT '字典id',
	`type` VARCHAR ( 50 ) CHARACTER 
	SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典类型',
	`code` VARCHAR ( 50 ) CHARACTER 
	SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典编码',
	`value` VARCHAR ( 50 ) CHARACTER 
	SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典值',
	PRIMARY KEY ( `dict_id` ) USING BTREE 
) ENGINE = INNODB CHARACTER 
SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

sharding-jdbc配置

1
2
# 指定t_dict为公共表
spring.shardingsphere.sharding.broadcast‐tables=t_dict

数据操作,新增DictMapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.eh.shardingjdbc.order.dao;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;

public interface DictMapper {
    /**
     * 新增字典
     *
     * @param type 字典类型 * @param code 字典编码 * @param value 字典值 * @return
     */
    @Insert("insert into t_dict(dict_id,type,code,value) value(#{dictId},#{type},#{code},# {value})")
    int insertDict(@Param("dictId") Long dictId, @Param("type") String type, @Param("code") String code, @Param("value") String value);

    /**
     * 删除字典
     *
     * @param dictId 字典id * @return
     */
    @Delete("delete from t_dict where dict_id = #{dictId}")
    int deleteDict(@Param("dictId") Long dictId);
}

测试,新增单元测试方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void testInsertDict() {
    dictMapper.insertDict(1L, "user_type", "0", "管理员");
    dictMapper.insertDict(2L, "user_type", "1", "操作员");
}

@Test
public void testDeleteDict() {
    dictMapper.deleteDict(1L);
}

执行testInsertDict

http://img.cana.space/picStore/20201123110028.png

通过日志可以看出,对t_dict的表的操作被广播至所有数据源。

测试testDeleteDict

http://img.cana.space/picStore/20201123110127.png