一、分库分表的缘由
????随着数据量的增大,或者一些特殊业务需求,单表的性能已经不能满足系统的要求,虽然可以对代码,sql的查询做一些优化,或者做一些数据预热,热点数据捞到缓存中,但是不能从根本上解决单库单表的性能问题,这些都只能作为一些优化手段,此时就需要我们的分库分表,由原来的单库-单表演变为我们的多库-多表。
常见的拆表:
- 垂直拆分 ??-将单表中不经常用到的字段,拆到一张附属表中,拆完后,俩张表结构不同
- 水平拆分?? -将单表存放的数据,横切分出一半放到另外一张表中,俩表结构相同
如图: ????这只是对于表的拆分,不管垂直拆分还是水平拆分,都是可以在单库内进行的,但是单库或单台机器所提供的性能毕竟是有限的,所以一般在分表之上,又会进行一个分库,多台机器,多个库,每个库中多张表,这样就是整个机器集群在提供数据查询服务,性能大大提高。 ? ????最终我们整个db服务就演变成这样:
????说完我们的db集群,再来看我们上图中的分片键和路由规则,当我们做了分库分表之后,对于每一条数据的读写操作,我们应该去哪个库哪张表操作呢?数据的插入好说,我们可以随便插入某个库某张表,但是查询、删除、修改呢?总不能每个库每张表遍历,遍历完才能知道对应的数据在哪个库哪张表吧,这样我们分库分表的意义还何在,所以就有了分片键和路由规则。
分片键:
????举个列子:我们将表分片以后,当执行一条SQL时,通过对字段 order_id 取模的方式来决定,这条数据该在哪个数据库中的哪个表中执行,此时 order_id 字段就是表的分片健,不管是增删改查中哪种操作,都需要携带order_id,我们再通过我们的路由规则计算这条数据应该插入哪个库的哪张表,查询也就不再需要我们遍历所有的数据库,直接定位这条数据在哪个库哪张表,直接去查询,效率大大提升。
路由规则:
????路由规则又可以称之为分片算法,是通过对分片键计算,来得到这条SQL语句应该到哪个库表执行的算法,从执行 SQL 的角度来看,分库分表可以看作是一种路由机制,把 SQL 语句路由到我们期望的数据库或数据表中并获取数据,分片算法可以理解成一种路由规则。
二、分库分表实现
技术选型:
????市面上有很多不错的分库分表的技术框架,当然,当我们对分库分表的实现原理已经熟悉的情况下,也可以手写一个,自己实现分片算法,这里选了俩个市面上常用的开源框架进行对比:mycat和shardingJdbc mycat:
是一个需要单独部署的第三方组件,对项目代码没有入侵 客户端所有的 jdbc 请求都必须要先交给 mycat ,再由 mycat 转发到具本的真实服务器中 由于jdbc请求都需要经过mycat,经过mycat内部进行分片路由计算再转到mysql,所以有额外的性能消耗 支持事务
shardingJdbc:
作为一个组件,需要在项目中集成,项目开发成本增加 在项目中解析sql,路由到具体的mysql库-表,相比mycat,性能较高 有语言限制,目前只支持Java语言 4.0支持事务
这里我说下我为什么选用shardingJdbc,相比于mycat,shardingJdbc目前只支持Java,同时需要在项目代码中集成,但是他的性能较高,同时在代码中集成,能让我们的开发人员更加清楚分库分表的一个底层实现原理,而且不需要额外部署组件,也不需要担心像mycat宕机的风险,而且shardingJdbc4.0也支持事务,和seata整合也比较简单。
准备工作
1、准备一个springboot工程 2、准备好俩个数据库,每个库中5张订单表
3、导入shardingJdbc相关依赖
<!-- sharding-sphere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
编写配置文件
先编写我们的配置文件,由于考虑到后续还要和nacos,seata整合。此处采用yml写法,后续从nacos上拉去配置文件:application.yml
server:
port: 4010
spring:
application:
name: learn_shardingsphere
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource:
default:
driver-class-name: com.mysql.cj.jdbc.Driver
password: root
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://ip:3306/[默认库]?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
ds1:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://ip:3306/taobao1?serverTimezone=GMT%2B8&useSSL=false
password: root
type: com.zaxxer.hikari.HikariDataSource
username: root
ds2:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://ip:3306/taobao2?serverTimezone=GMT%2B8&useSSL=false
password: root
type: com.zaxxer.hikari.HikariDataSource
username: root
names: default,ds1,ds2
props:
sql:
show: true
sharding:
default-data-source-name: default
tables:
order:
actual-data-nodes: ds1.order_$->{1..5},ds2.order_$->{6..9}
database-strategy:
standard:
precise-algorithm-class-name: org.example.config.PreciseDatabaseShardingAlgorithm
sharding-column: id
table-strategy:
standard:
precise-algorithm-class-name: org.example.config.PreciseTableShardingAlgorithm
sharding-column: id
logging:
level:
com:
sharding:
demo:
mapper: DEBUG
mybatis-plus:
configuration:
auto-mapping-behavior: full
map-underscore-to-camel-case: true
mapper-locations: classpath:/mapper/*Mapper.xml
库分片规则实现: 我们这里简单规定,取订单id的个位数,小于5则放在ds1库中,大于5放在ds2库中
public class PreciseDatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> id) {
long value = id.getValue();
String number;
String str = String.valueOf(value).substring(String.valueOf(value).length() - 1);
if (Integer.parseInt(str) <= 5) {
number = "1";
} else {
number = "2";
}
if (value <= 0) {
throw new UnsupportedOperationException("preciseShardingValue is null");
}
for (String availableTargetName : collection) {
if (availableTargetName.endsWith(number)) {
return availableTargetName;
}
}
throw new UnsupportedOperationException();
}
}
表分片规则实现: 取订单id个位数,拼接表:order{订单id个位数}
public class PreciseTableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> id) {
long value = id.getValue();
if (value <= 0) {
throw new UnsupportedOperationException("preciseShardingValue is null");
}
String str = String.valueOf(value).substring(String.valueOf(value).length() - 1);
for (String availableTargetName : collection) {
if (availableTargetName.endsWith(str)) {
return availableTargetName;
}
}
throw new UnsupportedOperationException();
}
}
这里再贴出OrderMpper.xml的写法: 实体类,service实现,文章中就不具体贴出了 这里在xml中订单表都是取的逻辑表:order
<insert id="insert" parameterType="org.example.entity.Order">
insert into order(id,name,count,description) values(#{id},#{name},#{count},#{description})
</insert>
<update id="update" parameterType="org.example.entity.Order">
update order set
name=#{name},
count=#{count},
description=#{count}
where id = #{id}
</update>
<delete id="delete">
delete from order where id = #{id}
</delete>
<select id="select" resultType="org.example.entity.Order" parameterType="org.example.entity.Order">
select * from order where id = #{id}
</select>
|