背景
我们为什么要进行分库分表?
在电商系统中,当一张表的数据达到几千万时,查询一次所花的时间会变长。这时候,如果有联合查询的话,可能会卡死在那儿,甚至把系统给拖垮。而分库分表的目的就在于此:减小数据库的负担,提高数据库的效率,缩短查询时间。权衡过多个框架的利弊后,我们最终选择使用Sharding-JDBC来进行分库分表。
Sharding-JDBC简介
Sharding-jdbc定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据。
从官网的架构图中,可以看出我们只需要将Sharding-JDBC引入到项目中就好了。
Sharding-JDBC的使用
接下来,以b2b2c电商系统Javashop为例,具体说明sharding jdbc的应用:
一、分库分表的准备
在修改配置文件之前,我们应该想好我们的分片策略,包括:
1、要用几个数据库来分片
2、相应的表要分几张表
在本次分库分表中,以es_order表为例,将原始数据库javashop拆分为javashop0,javashop1两个数据库。将原始es_order表拆分为es_order0,es_order1两张表。
表中分片字段如下:
CREATE TABLE `es_order0` ( `order_id` bigint(20) NOT NULL COMMENT '主键ID', `trade_sn` varchar(20) DEFAULT NULL COMMENT '交易编号', 其他字段略)
CREATE TABLE `es_order1` ( `order_id` bigint(20) NOT NULL COMMENT '主键ID', `trade_sn` varchar(20) DEFAULT NULL COMMENT '交易编号', 其他字段略)
二、引入maven依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
三、配置分片策略
定义数据库的分片策略application.yaml:
spring:
profiles:
include: order
#分库分表配置
shardingsphere:
#配置sql是否显示输出,调试用,生产环境需关闭
props:
sql:
show: true
sharding:
#定义默认数据源为:ds0
default-data-source-name: ds0
#定义分库的数据源
datasource:
#这里配置所有数据源的名字
names: ds0,ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://ip:3306/default_database?useUnicode=true&characterEncoding=utf8&autoReconnect=true
username: root
password: 123456
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.2.110:3306/javashop02?useUnicode=true&characterEncoding=utf8&autoReconnect=true
username: root
password: 123456
定义表分片策略修改application-order.yml:
spring:
#分库分表配置
shardingsphere:
sharding:
tables:
#交易表(用trade sn 分库,用trade_id分表)
es_trade:
actual-data-nodes: ds$->{0..1}.es_trade$->{0..1}
database-strategy:
inline:
sharding-column: trade_sn
algorithm-expression: ds$->{ new Long(trade_sn) % 2}
table-strategy:
inline:
sharding-column: trade_id
algorithm-expression: es_trade$->{trade_id % 2}
通过上述方式我们分别将商品,会员,订单进行了分库分表。
问题与解决方案
如果您以为就是这么简单就完成了那么就大错特错了。我们遇到的问题如下:
主键自增问题
因为当使用分库分表等功能之后,就不能再依赖数据库自带的主键生成机制了,一方面主键ID不能重复,另外需要在新增之前就知道主键ID,才能保证ID能够均匀分布到不同的数据库或数据表中,所以要使用一个合理的主键生成策略。
我们的解决方案就是在做insert语句的时候,使用snowflake发号器生成主键。snowflake 是常见的id(编号)生成算法,由时间戳+业务id+机器id+序列号组合而成,在电商系统中,用于订单号的生成、支付单号的生成等等。本发号器主要解决在容器化的部署情况时,自动扩容时保持机器id的唯一性。关于snowflake详细说明请看《java 商城系统源码分享-snowflake发号器》的文章 统一封装的insert方法如下:
public void insert(String table, Object po) {
Long id = snCreator.create(getSubCode(table));
//设置刚刚主键值到 thread local
lastIdThreadLocal.set(id);
Map poMap = new HashedMap();
ColumnMeta columnMeta = ReflectionUtil.getColumnMeta(po);
poMap.put(columnMeta.getPrimaryKeyName(), id);
//这里就是将插入的时候使用雪花发号器作为主键,您们可以根据自己的业务进行修改。
如下略:
}
使用snowflake发号器带来的类型问题
因为雪花发号器的类型为Long类型我们在未分库分表之前数据库使用的id类型为int(11),所以我们需要将所有涉及到分库分表id类型类型修改为bigint(20)。当然还有java代码中接收数据的实体类。
使用snowflake号器带来的精度丢失问题
因为js支持数字的最大精度为16位以下,那么我们的雪花发号器的位数为17为。好尴尬啊,后来我们统一将Long类型的数据转为String类型。代码如下:
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customJackson() {
return new Jackson2ObjectMapperBuilderCustomizer() {
@Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
//定义Long型的Json 值转换,转换为String型
//为了适配javascript 不支持Long型的精度
jacksonObjectMapperBuilder.serializerByType(Long.class,new JsonSerializer(){
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(String.valueOf(value));
}
});
}
};
}
}
使用相同字段进行分库分表的问题
相同字段进行分库分表,就是我们用主键id进行分库,然后再用主键id进行分表。如果我插入100条订单数据的话,那么javashop0数据库中es_order0表有50条id为奇数的数据es_order1表中的数据为0条。javashop1数据库中的es_order0表0条数据es_order1表中50条数据。这显然不是我们所希望看到的。所以我们在分库的时候使用与2取模的形式分库,分表的时候我们将id右移2位在此进行与2取模。那么得到的结果才是我们想要的。具体配置如下:
spring:
#分库分表配置
shardingsphere:
sharding:
tables:
#商品表(用member_id分库,用member_id分表)
es_member:
actual-data-nodes: ds$->{0..1}.es_member$->{0..1}
database-strategy:
inline:
sharding-column: member_id
algorithm-expression: ds$->{member_id% 2}
table-strategy:
inline:
sharding-column: member_id
#因为都是使用同一个字段进行分库分表的,
#所以要右移两位之后取模以保证每个表中的数据平均
algorithm-expression: es_member$->{(member_id>>2)% 2}
Sharding-jdbc不支持的sql 官网提供不支持的sql如下:
INSERT INTO tbl_name (col1, col2, …) VALUES(1+2, ?, …) | VALUES语句不支持运算表达式 |
INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? | INSERT .. SELECT |
SELECT COUNT(col1) as count_alias FROM tbl_name GROUP BY col1 HAVING count_alias > ? | HAVING |
SELECT * FROM tbl_name1 UNION SELECT * FROM tbl_name2 | UNION |
SELECT * FROM tbl_name1 UNION ALL SELECT * FROM tbl_name2 | UNION ALL |
SELECT * FROM ds.tbl_name1 | 包含schema |
SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name | 详见DISTINCT支持情况详细说明 |
SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ? | 会导致全路由 |
SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name | 同时使用普通聚合函数和DISTINCT聚合函数 |
这个问题的话我们的解决方案是优化我们的sql,将这些不支持的sql语句使用其他的逻辑实现在这里就不一一阐述了。 易族智汇(javashop)原创文章