背景

我们为什么要进行分库分表?

电商系统中,当一张表的数据达到几千万时,查询一次所花的时间会变长。这时候,如果有联合查询的话,可能会卡死在那儿,甚至把系统给拖垮。而分库分表的目的就在于此:减小数据库的负担,提高数据库的效率,缩短查询时间。权衡过多个框架的利弊后,我们最终选择使用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)原创文章

04-06 09:27