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_0
与t_order_1
,依据userId
对2
取模进行差分,因此建立两个相同的表:
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的两条记录:
数据库ds1
会插入user_id
为1、3、5的三条记录:
说明分片成功。
问题
找不到方法错误
启动报错找不到方法:
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_order
的id
设成自增,则会出现两个问题,一是上边提到的批量插入问题,二是批量查询时查出的数据会是错误的。
对于第一个问题,以测试order
插入的示例为例,若id
为自增主键,则不必为其显示赋值,假如数据库中没有任何数据,因为自增id
是表级控制的,则插入时,user_id
为1
的记录会插入到表t_order_1
中,因为是表t_order_1
的第一条数据,会被分配id
为1
,user_id
为2
的记录会插入到表t_order_0
中,因为是表t_order_0
的第一条数据,也会被分配id
为1
,这样,两者就具有相同的id
了,而在一次操作事务的会话中,hibernate
是不允许出现具有两个主键一样的对象的,所以会插入失败,事务回滚。单独插入时就可以成功,因为虽然id
相同,但不是在一次事务中执行的。
对于第二个问题,若通过单个插入操作成功了,则数据库中记录如下:
t_order_0
:
t_order_1
:
两个表中前两条记录的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)
会发现userId
为2
和4
的记录出现了两次。这是因为查询时,ShardingSpere
数据源会在两个表中同时进行查询,然后将查询结果汇总后提交到上层orm
框架,而orm
框架在序列化组织数据时,实体是依据id
进行组装的,当组装到第二条id
为1
的数据时,依据id
在结果集中进行筛选,就直接筛选到第一条id
为1
的数据进行组装了,因而封装成实体后,相同的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}