随着业务数据量的变大,单库单表已经不能满足需求了。当单表数据量超过五百万行,查询性能急剧下降。分库分表迫在眉睫,寻找一个简单实用的解决方案相信是很多小伙伴的想法。
我在看了好多的博客之后遇到了开源数据库中间件mycat和shardingsphere(前身是sharding-jdbc),经过一番比较之后,我选了京东开源的shardingsphere作为我的解决方案。
写这篇文章的目的有两个,一来是帮助刚入门学习shardingsphere的童鞋快速上手,减少时间成本,先看下怎么用再去看官方文档可以达到事半功倍的效果;二来是记录自己在学习过程中遇到的问题,方便以后在项目中的使用。
(1)版本信息:
(2)用于演示的代码和重要的参考链接已经放到文章的末尾,有需要的童鞋可直接下载查看。其中
sharding_proxy文件夹存放代码,sharding-proxy-server存放proxy服务端文件。
太多的理论知识我就不赘述了,麻烦自己到官网去看。
sharding-jdbc 和 sharding-proxy对比
配置好之后可作为独立的数据源使用,一个逻辑数据库代理着几个真实数据库,可以用客户端软件比如Navicat Premium 直接去连接和操作。很好的帮助我们处理分库分表的问题,基本不需要对现有的业务代码修改,减少时间成本。
目前生产环境已使用的公司
(1)其实大部分的工作都是在编写conf目录下的yml文件,配置好之后应用还是跟以前的方式一样连接和操作,基本不需要对代码进行修改。
(2)逻辑数据源可以配置多个,每个yml文件代表一个逻辑数据源,可手动创建yml文件和编写规则实现多个逻辑数据源的使用。
(3)本文演示的是单个逻辑数据源的配置和使用。
首先你要对业务需要用到的表有一个清晰的认识。哪些表不需要拆分,哪些表需要拆分,表跟表之间是否存在关联。通过阅读官网和我的理解,我觉得主要分为这几种表:
这种表数据量不大,小于十万这样,而且跟其他表没有关联。这样的表不需要拆分,放在一个默认库中即可。比如:配置表,地区编码表。
这种表数据量不大,没有必要拆分;但是跟其他表有关联关系。在每个库都保存一个完整表,当读取数据的时候随机路由到任一库,当写入数据时每个库下的表都写入。
数据量较大需要拆分的表。比如说订单数据根据主键尾数拆分为10张表,分别是t_order_0到t_order_9,他们的逻辑表名为t_order。
按我的理解就是父子表,常见的就是订单表和订单详情表,通过订单id关联。这种类型的表数据量大也是需要拆分的。
为了加深对sharding-proxy的理解,我在这里模拟了一个场景,基本涵盖了常见的情况,顺便把实现步骤和使用过程的问题也提一提。
按照前面表的关系图,我们可以划分一个默认库(存放单库单表和广播表)和三个库(存放逻辑表和广播表);如下所示。sql文件放在git地址的sql目录下。
data_source
--area
--config
--factory
--warehouse
data_source0
--code_relate0
--code_relate1
--customer0
--customer1
--factory
--indent_detail0
--indent_detail1
--indent0
--indent1
--task_upload0
--task_upload1
--task0
--task1
--warehouse
data_source1
--code_relate0
--code_relate1
--customer0
--customer1
--factory
--indent_detail0
--indent_detail1
--indent0
--indent1
--task_upload0
--task_upload1
--task0
--task1
--warehouse
data_source2
--code_relate0
--code_relate1
--customer0
--customer1
--factory
--indent_detail0
--indent_detail1
--indent0
--indent1
--task_upload0
--task_upload1
--task0
--task1
--warehouse
注意事项:
(1)解压工具最好选择winrar,不然解压出来的lib文件夹下的jar包由于命名太长被截断,后面将会导致文件找不到,系统启动失败异常。
(2)解压后的文件的路径下最好不要有中文,否则也会导致启动失败。
MySQL Connector/J,下载之后放到lib文件夹下。
(1)首先要编写的是server.yaml,配置全局信息
##治理中心
# orchestration:
# name: orchestration_ds
# overwrite: true
# registry:
# type: zookeeper
# serverLists: localhost:2181
# namespace: orchestration
#权限配置
authentication:
users:
root: #用户名
password: root #密码
sharding:
password: sharding
authorizedSchemas: sharding_db #只能访问的逻辑数据库
#Proxy属性
props:
max.connections.size.per.query: 1
acceptor.size: 16 #用于设置接收客户端请求的工作线程个数,默认为CPU核数*2
executor.size: 16 # Infinite by default.
proxy.frontend.flush.threshold: 128 # The default value is 128.
# LOCAL: Proxy will run with LOCAL transaction.
# XA: Proxy will run with XA transaction.
# BASE: Proxy will run with B.A.S.E transaction.
proxy.transaction.type: LOCAL #默认为LOCAL事务
proxy.opentracing.enabled: false #是否开启链路追踪功能,默认为不开启。
query.with.cipher.column: true
sql.show: true #SQL打印
check.table.metadata.enabled: true #是否在启动时检查分表元数据一致性,默认值: false
# proxy.frontend.flush.threshold: # 对于单个大查询,每多少个网络包返回一次
注意事项:
(1)当你把注释的示例内容复制粘贴,然后用快捷键去掉‘#’注释符的时候,注意父子层的距离不要改变,比如下方父子层之间是两个空格。
props:
max.connections.size.per.query: 1
(2) 在编写的时候不要用‘Tab’键拉开距离,应该使用空格键,否则启动的时候会报错。
小插曲
当你编写yml文件的时候,里面都会有这句话,一开始我以为还要在哪里引用这些要用的文件,后面才发现直接去掉注释编写就好,文件都是自动引用的。
If you want to configure orchestration, authorization and proxy properties, please refer to this file.
其实编写这个文件和sharding-jdbc的yml配置文件基本相同,区别就是对于组合单词jdbc用的是驼峰,proxy用的是“-”,如果之前玩过sharding-jdbc的话可以直接把配置文件复制过来,对应修改即可。
schemaName: sharding_db #逻辑数据库名配置
dataSources: #真实数据源配置
db:
url: jdbc:mysql://127.0.0.1:3306/data_source?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
db0:
url: jdbc:mysql://127.0.0.1:3306/data_source0?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
db1:
url: jdbc:mysql://127.0.0.1:3306/data_source1?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
db2:
url: jdbc:mysql://127.0.0.1:3306/data_source2?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
shardingRule: ##分库分表规则
defaultDataSourceName: db #默认数据源,放置不需要分片的表和广播表
broadcastTables:
- factory
- warehouse
bindingTables:
- indent,indent_detail
- task_upload,code_relate ##绑定表配置
defaultDatabaseStrategy: #默认的分库规则,如果逻辑表没单独配置则使用这个
inline:
shardingColumn: customer_id #默认按照customer_id分库
algorithmExpression: db$->{customer_id % 3}
tables: #逻辑表配置
config: ###单库单表,使用UUID作为主键
actualDataNodes: db.config
keyGenerator:
column: code
type: UUID
customer:
actualDataNodes: db$->{0..2}.customer$->{0..1} #具体的数据节点
tableStrategy: ##分表策略
inline:
shardingColumn: customer_name #根据hash值取模确定落在哪张表
algorithmExpression: customer$->{Math.abs(customer_name.hashCode() % 2)}
keyGenerator: #配置主键生成策略,默认使用SNOWFLAKE
column: customer_id
type: SNOWFLAKE
props:
worker:
id: 20200422
indent:
actualDataNodes: db$->{0..2}.indent$->{0..1}
tableStrategy:
inline:
shardingColumn: indent_id
algorithmExpression: indent$->{indent_id % 2}
keyGenerator:
column: indent_id
type: SNOWFLAKE
indent_detail:
actualDataNodes: db$->{0..2}.indent_detail$->{0..1}
tableStrategy:
inline:
shardingColumn: indent_id
algorithmExpression: indent_detail$->{indent_id % 2}
keyGenerator:
column: detail_id
type: SNOWFLAKE
task:
actualDataNodes: db$->{0..2}.task$->{0..1} #具体的数据节点
databaseStrategy: #分库规则
inline:
shardingColumn: task_id
algorithmExpression: db$->{task_id % 3}
tableStrategy:
inline:
shardingColumn: task_id
algorithmExpression: task$->{task_id % 2}
task_upload:
actualDataNodes: db$->{0..2}.task_upload$->{0..1} #具体的数据节点
databaseStrategy: #分库规则
inline:
shardingColumn: task_id
algorithmExpression: db$->{task_id % 3}
tableStrategy:
inline:
shardingColumn: stack_code
algorithmExpression: task_upload$->{Math.abs(stack_code.hashCode() % 2)}
keyGenerator:
column: upload_id
type: SNOWFLAKE
code_relate:
actualDataNodes: db$->{0..2}.code_relate$->{0..1} #具体的数据节点
databaseStrategy: #分库规则
inline:
shardingColumn: task_id
algorithmExpression: db$->{task_id % 3}
tableStrategy:
inline:
shardingColumn: stack_code
algorithmExpression: code_relate$->{Math.abs(stack_code.hashCode() % 2)}
keyGenerator:
column: relate_id
type: SNOWFLAKE
注意事项:
(1)数组的写法
broadcastTables:
- factory
- warehouse
bindingTables:
- indent,indent_detail
- task_upload,code_relate ##绑定表配置
(2)分片键分为分库键和分表键。
(3)主键生成默认使用SNOWFLAKE算法,使用UUID主键的话需要配置。
(4)如果分片键的值为long型,分片规则为分片字段取模即可;如果是String型,分片规则为分片字段的哈希值取模再求绝对值,因为哈希值取模之后也许会出现负数。
(5)逻辑表和绑定表配置建议,尽可能的让同一类型的数据落在同一个库中。比如用户的信息和他产生的订单以及订单详情,可以通过consumer_id作为分库键,indent_id作为分表键存放,这样如果查询命中分片键的话可以提高查询效率(少查了不必要的表)。
(6)绑定表建表的时候,子表最好增加分库键字段便于新增数据时确定落到哪个库中。比如用户表、订单表和订单详情表,consumer_id作为分库键,订单表需要有这个字段,订单详情表也需要这个字段,否则订单详情新增数据的时候会在每个库都新增数据,很明显是不合理的情况。
日志路径默认在启动方式start.bat下创建个logs文件夹,所以只要填写相对路径就好。下面设置的是按级别按天输出日志,基本符合常规需求。
<?xml version="1.0"?>
>
<!-- 控制台输出 -->
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %highlight([%-5level] %logger{50} - %msg%n)
UTF-8
<!-- 系统正常日志文件 -->
INFO
ACCEPT
DENY
../logs/%d{yyyy-MM-dd}/info.%i.log
10MB
true
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n
UTF-8
<!-- 系统警告日志文件,记录用户输入不符合规则的数据 -->
WARN
ACCEPT
DENY
../logs/%d{yyyy-MM-dd}/warn.%i.log
10MB
true
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n
UTF-8
<!-- 系统错误日志文件 -->
ERROR
ACCEPT
DENY
../logs/%d{yyyy-MM-dd}/error.%i.log
10MB
true
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n
UTF-8
>
-ref ref="SYSTEM_ERROR"/>
>
<!-- 默认纪录级别 -->
>
在cmd命令窗口中cd到bin目录下,然后直接运行start.bat,默认是3307端口;当然你也可以指定端口,在后面加上参数即可,比如start.bat 3309即可指定连接端口为3309。然后观察cmd窗口,如果没报出异常的话表示启动成功。
这里推荐使用Navicat Premium 11 去连接,使用Navicat Premium 12的话你会发现很多奇怪的问题,下面是官网的建议和成功连接的图。至此,proxy服务端搭建好了,接下来是编写代码。
通过示例代码的工具类连接generator库可以快速生成基础的CRUD代码
src/test/java/com/project/generator/MybatisGenerator.java
注意事项
(1)实体主键类型的选择
/**
* id
*/
@TableId(value = "id", type = IdType.ID_WORKER)
private Long id;
或者
/**
* id
*/
@TableId(value = "id", type = IdType.NONE)
private Long id;
使用type= ID.AUTO的异常信息
Caused by: java.sql.SQLException: Field ‘id’ doesn’t have a default value
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3978)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903)
at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2124)
at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java:1801)
… 89 common frames omitted
/**
* 编号
*/
@TableId(value = "code", type = IdType.AUTO)
private String code;
或者
/**
* 编号
*/
@TableId(value = "code", type = IdType.UUID)
private String code;
测试代码已经写到里面了,通过发起请求和观察cmd窗口或者日志文件你会发现逻辑SQL和真实SQL,从而发现他的查询规则:
1、如果表没配置规则,那么直接到默认库去访问
2、如果访问的是广播表,那么读的时候是随机路由到一个库,写的时候是全部库都写数据。
3、逻辑表查询,查询字段命中了分库键,那么路由到指定库下的所有表查询;命中了分表键,到所有库下指定表查询。如果都没命中,那么将发生笛卡尔积,进行全路由所有的库和表都查询一遍,效率不高。所以合理的配置分片规则是很重要的。
由于这里只配置了一个逻辑数据源(包含多个真实数据源),不大清楚算不算分布式事务。不过写法跟原来是一样的,加上注解即可。运行下面的代码发生异常,数据将不会写进数据库,表示事务控制成功。
/**
* 测试事务管理
*
* @date 2020年5月3日
* @author huangjg
*/
@Transactional(rollbackFor = Exception.class)
@PostMapping("/save")
public ResponseData<?> save() {
customerService.saveBatch(list);
int i = 5 / 0;
return ResponseData.out(CodeEnum.SUCCESS, null);
}
这个还没开始研究,估计在4.1.x版本会有这个功能。
目前我将zookeeper跑起来的时候不懂如何跟项目对接起来,如果有成功的同学麻烦将方法告知下。
官网的文档比较详细和社区都是很活跃的,这些可以减少我们的学习成本,快速用于项目。如果在学习的过程中遇到问题可以多看看官方文档或者直接到github上面提issues,官方人员会很快给予答复的。
演示代码地址
shardingsphere官网地址
shardingsphere github地址