前言
随着业务的快速发展,业务系统数据表记录也随着急剧增长,带来的明显结果就是当用户访问某些表时性能显著下降,通过分析后决定来拆分大的数据表以降低单表数据量,提高查询性能。
分库分表又分为垂直拆分和水平拆分,这里简单介绍下:
-
垂直分库
即按照业务模块进行拆分,比如将订单模块独立为一个数据库,商品模块独立为一个数据库
-
垂直分表
即将宽表拆分为窄表,所有记录都能在单表中找到,通过将一些字段拆分出去建立副表来降低单表的数据量;比如订单表中,通过垂直拆分我们可以将低频、占用空间大的字段拆分出去
-
水平分库
即将相同的表拆分到不同的数据库,以来降低单库的压力;比如我们可以通过用户id来拆分,用户id为奇数的存储到order_1库的订单表,用户id为偶数的订单存储到order_2库的订单表;
-
水平分表
即将相同的表复制出来多份,以来降低单表的数据量,提升单表查询性能;比如在order数据库中创建user_order_1、user_order_2表,通过订单id求余来存储到不同的表中
一、Sharding-JDBC是什么?
在Java的JDBC层提供的额外服务, 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
其主要支持功能有数据分片、读写分离、分布式事务等;本篇文章主要介绍使用其数据分片功能来实现分库分表
二、使用步骤
这里主要以springboot+Mybaits-Plus+来演示使用方式
1.引包
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
2.配置
application.yml
server:
port: 8099
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.none.sharding.domain.entity
spring:
application:
name: sharding
shardingsphere:
datasource:
names: ds0
ds0:
url: jdbc:mysql://localhost:3306/order
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
sharding:
defaultDataSourceName: ds0 #默认数据源,即不需要分库分表的表查询时会默认使用ds0数据库
tables:
t_address: #t_address
key-generator-column-name: id #主键 通过雪花算法生成
actual-data-nodes: ds0.t_address${0..1} #数据节点,数据库中t_address0,t_address1
table-strategy: #分表策略
inline: #行表达式
sharding-column: id #分表字段为id
algorithm-expression: t_address${id % 2} #分表算法为求余
t_user_order:
key-generator-column-name: id #主键
actual-data-nodes: ds0.t_user_order${0..2} #数据节点,均匀分布
table-strategy: #分表策略
inline:
sharding-column: id
algorithm-expression: t_user_order${id % 3}
props:
sql.show: true #显示原sql以及解析生成的实际sql
allow.range.query.with.inline.sharding: true #支持范围查询例如大于、小于、between;
2. 使用
由于Sharding-JDBC定义为增强版jdbc所以使用方式和jdbc相同,对于业务代码无侵入,原有查询怎么写接入Sharding-JDBC后基本不用进行任何改变;
这里我们使用Springboot+mybatis-plus+Sharding-JDBC做个简单示例:
userOrderDO实体
@Data
@TableName("t_user_order")
public class UserOrderDO
extends Model<UserOrderDO> {
private Long id;
private String orderNo;
private Long userId;
private Integer amount;
}
UserOrderMapper
@Mapper
public interface UserOrderMapper
extends BaseMapper<UserOrderDO> {
}
UserOrderService
public interface UserOrderService
extends IService<UserOrderDO> {
}
UserOrderServiceImpl
@Service
public class UserOrderServiceImpl extends ServiceImpl<UserOrderMapper, UserOrderDO>
implements UserOrderService {
}
UserOrderController
@RestController
@RequestMapping("/userOrder/")
public class UserOrderController {
@Resource
private UserOrderService userOrderService;
/**
* 生成订单
* @return
*/
@RequestMapping("save")
public String save() {
UserOrderDO userOrderDO = new UserOrderDO();
userOrderDO.setOrderNo(String.valueOf(System.currentTimeMillis()));
userOrderDO.setUserId((long)123);
userOrderDO.setAmount(500);
userOrderService.save(userOrderDO);
return "success";
}
/**
* 通过订单id查询订单
* @param id
* @return
*/
@RequestMapping("get")
public UserOrderDO getOrderById(@RequestParam Long id) {
return userOrderService.getById(id);
}
}
调用/userOrder/save
,控制台可以看到如下两条sql:
Logic SQL: INSERT INTO t_user_order ( id, order_no, user_id,amount ) VALUES ( ?, ?, ?, ? )
Actual SQL: ds0 ::: INSERT INTO t_user_order0 ( id, order_no, user_id, amount ) VALUES (?, ?, ?, ?) ::: [1340265974084612097, 1608379283781, 123, 500]
可以看到,我们对t_user_order进行插入的时候,Sharding-JDBC最终会根据分表规则进行替换,1340265974084612097%3=0
,所以最终sql会路由到t_user_order0这个数据表中;
那我们再看下查询,
Logic SQL: SELECT id,order_no,user_id,amount FROM t_user_order WHERE id=?
Actual SQL: ds0 ::: SELECT id,order_no,user_id,amount FROM t_user_order2 WHERE id=? ::: [2]
因为2%3=2
所以这条sql最终会去查询t_user_order2表
3. 注意
虽然Sharding-JDBC与传统jdbc的使用方式基本无异,但是在分表场景下仍然有些sql不能支持,或者支持不完善;
- 5.x之前不支持子查询;5.0.0-alpha内测中,5.0.0版本已经支持
- 5.x之前不支持更新操作 set column=column 操作
- 对分库分表的表进行curd时,一定要使用分片字段;SQL 中如果无分片字段,将执行全路由,性能较差;比如:对t_user_order表拆分了40张表,分片字段为id,当使用order_no进行查询时,将会去查询40张表然后聚合结果集返回
- 慎用范围查询,包括>=、<=、between;范围查询通常也会走全路由 (除非特殊情况可以在算法中进行拆解)
- join操作;对于两张进行join操作的表可以进行绑定关系设置分片键、分片规则一致;避免join操作变为笛卡尔积式查询
- 对分片键函数操作则会全路由,比如contact(‘id’,‘a’)
- 禁止在sql中使用case when、if else等业务查询;
- mybatis-plus使用getById、updateById、updateBatchById等操作时要考虑分表键是否为id,如果不是id字段则需要重写这些方法,在where条件中要增加相关分片键的条件;否则将会全路由;如果更新操作中要更新分片键的值,则会报错不能修改分片键;
总结
以上即为Springboot+MyBatis-plus+Sharding-JDBC进行拆分表的简单demo,后续会更新拆分表的几个策略:标准分片策略、复合分片策略、行表达式分片策略、Hint分片策略
Demo 地址 https://github.com/nangge/sharding.git