SpringBoot分表

利用shardingsphere可以在springboot下很简单的完成分库分表操作,这里说一下其实现以及在分表过程中个人遇到的几个问题。

orm使用的JPA

创建工程

利用SpringInitializer创建一个工程,引入ShardingSphere的依赖:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.2.1</version>
</dependency>
<!-- 如果不引入,shardingsphere内置依赖的yaml版本过低,启动会报错 -->
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.33</version>
</dependency>

定义一个Order实体,并编写相应的repository或者mapper,用于测试,dao层代码与平常一样,不在赘述,实体定义如下:

package ink.labrador.shardingdemo.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;

@Entity
@Data
@Table(name = "t_order")
public class Order {
    @Id
    private Integer id;
    @Column
    private String name;
    @Column
    private Integer userId;
}

这里示例将order水平拆分为两个表,t_order_0t_order_1,依据userId2取模进行差分,因此建立两个相同的表:

CREATE TABLE `ShardingTest`.`t_order_0`  (
  `id` int(0) NOT NULL,
  `user_id` int(0) NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `order0_user_id`(`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

CREATE TABLE `ShardingTest`.`t_order_1`  (
  `id` int(0) NOT NULL,
  `user_id` int(0) NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `order1_user_id`(`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

分库分表配置

shardingsphere可以开箱即用,只需要在配置文件中配置分表策略即可,如下:

spring:
  main:
    allow-bean-definition-overriding: true
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false
  shardingsphere:
    datasource:
      names: ds0 # 是用的数据源列表,每个数据源即是一个数据库,若分库,多个数据源逗号隔开,如ds0,ds1
      ds0: # 数据源ds0配置
        driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动名称
        type: com.zaxxer.hikari.HikariDataSource # 数据源类型
        jdbc-url: jdbc:mysql://192.168.18.226:3306/ShardingTest # 数据库连接
        username: root # 用户名
        password: root # 密码

    rules: # 分片规则设置
      sharding:
        tables: # 表格分片设置
          t_order: # t_order表分片规则
            # t_order表实际是用的节点名称,ds0.t_order_0以及ds0.t_order_1。
            actual-data-nodes: ds0.t_order_$->{0..1}
#            database-strategy: # 数据库分片策略
#              standard:
#                sharding-column: id # 数据库分片依据的字段
#                sharding-algorithm-name: database_inline # 数据库分片是用的算法名称
            table-strategy: # 是用的分片策略
              standard:
                sharding-column: user_id # 分片依赖的字段
                sharding-algorithm-name: order_inline  # 使用的分片算法名称
        sharding-algorithms: # 分片算法定义
#          database_inline
#            type: INLINE
#            props:
#              allow-range-query-with-inline-sharding: true # 允许范围查询
#              algorithm-expression=ds_${id % 2}

          order_inline: # 自定义分配算法名称
            type: INLINE # 分片类型
            props:
              allow-range-query-with-inline-sharding: true # 允许范围查询
              # 分片表达式,user_id与2取模,即user_id为奇数的会插入t_order_1,偶数的插入t_order_0
              algorithm-expression: t_order_${user_id % 2}

这是只依据user_id进行了分表,注释部分即为分库策略,若同时分库分表,依旧实际情况进行类似配置即可。

测试

插入测试:

@Test
public void testSharding() {
    List<Order> orders = new ArrayList<>();
    for (int i = 1; i < 6; i ++) {
        Order order = new Order();
        order.setId(i);
        order.setUserId(i);
        order.setName("NO_" + i);
        orderRep.save(order);
        orders.add(order);
    }
    orderRep.saveAll(orders);
}

执行结果如下:

数据库ds0会插入user_id为1和2的两条记录:

Snipaste20230224140037jpg

数据库ds1会插入user_id为1、3、5的三条记录:

Snipaste20230224140041jpg

说明分片成功。

问题

找不到方法错误

启动报错找不到方法:

 org.apache.shardingsphere.infra.util.yaml.constructor.ShardingSphereYamlConstructor$1.setCodePointLimit

这是shardingsphere内置依赖的yaml版本过低,在pom.xml中显示加入高版本依赖即可:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.33</version>
</dependency>

批量插入时同一session中唯一标志不能相同错误

错误信息如下:

A different object with the same identifier value was already associated with the session

这是因为批量保存时,存在主键相同的实体,在一次操作事务的会话中,hibernate是不允许出现具有两个主键一样的对象的,具有自增主键批量保存时会出现这个问题,详细的看下边避免自增主键中的说明。

避免自增主键

当分片的表中具有自增主键时,例如将t_orderid设成自增,则会出现两个问题,一是上边提到的批量插入问题,二是批量查询时查出的数据会是错误的。

对于第一个问题,以测试order插入的示例为例,若id为自增主键,则不必为其显示赋值,假如数据库中没有任何数据,因为自增id是表级控制的,则插入时,user_id1的记录会插入到表t_order_1中,因为是表t_order_1的第一条数据,会被分配id1user_id2的记录会插入到表t_order_0中,因为是表t_order_0的第一条数据,也会被分配id1,这样,两者就具有相同的id了,而在一次操作事务的会话中,hibernate是不允许出现具有两个主键一样的对象的,所以会插入失败,事务回滚。单独插入时就可以成功,因为虽然id相同,但不是在一次事务中执行的。

对于第二个问题,若通过单个插入操作成功了,则数据库中记录如下:

t_order_0:

Snipaste20230223165934jpg

t_order_1:

Snipaste20230223165938jpg

两个表中前两条记录的id是相同的,这时若是用findAll进行查询,会发现查询出来的数据是错误的,结果如下:

Order(id = 1, name = "NO_2", userId = 2)
Order(id = 2, name = "NO_4", userId = 4)
Order(id = 1, name = "NO_2", userId = 2)
Order(id = 2, name = "NO_4", userId = 4)
Order(id = 3, name = "NO_5", userId = 5)

会发现userId24的记录出现了两次。这是因为查询时,ShardingSpere数据源会在两个表中同时进行查询,然后将查询结果汇总后提交到上层orm框架,而orm框架在序列化组织数据时,实体是依据id进行组装的,当组装到第二条id1的数据时,依据id在结果集中进行筛选,就直接筛选到第一条id1的数据进行组装了,因而封装成实体后,相同的id的实体的值都会变成第一次出现的相同id的记录的值。

综上,在分片的表中,应该避免是用自增id,像订单这种可以直接是用订单号作为主键,而没有唯一属性的表,可以使用雪花算法或者uuid等策略生成id

范围查询报错

默认情况下,分片的表会禁止范围查询,可以在分片算法配置中进行设置:

sharding-algorithms: # 分片算法定义
  order_inline: # 自定义分配算法名称
    type: INLINE # 分片类型
    props:
      allow-range-query-with-inline-sharding: true # 允许范围查询
      algorithm-expression: t_order_${user_id % 2}