zoukankan      html  css  js  c++  java
  • 分库表在电商系统中的应用

    背景

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

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

    通过上述方式我们分别将商品,会员,订单进行了分库分表。

    问题与解决方案

    如果您以为就是这么简单就完成了那么就大错特错了。我们遇到的问题如下:

    1. 主键自增问题

    因为当使用分库分表等功能之后,就不能再依赖数据库自带的主键生成机制了,一方面主键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);
       //这里就是将插入的时候使用雪花发号器作为主键,您们可以根据自己的业务进行修改。
       如下略:
       }
    1. 使用snowflake发号器带来的类型问题

    因为雪花发号器的类型为Long类型我们在未分库分表之前数据库使用的id类型为int(11),所以我们需要将所有涉及到分库分表id类型类型修改为bigint(20)。当然还有java代码中接收数据的实体类。

    1. 使用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));
                        }
                    });
                }
    
            };
        }
    
    }
    1. 使用相同字段进行分库分表的问题

    相同字段进行分库分表,就是我们用主键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}
    1. Sharding-jdbc不支持的sql

    官网提供不支持的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)原创文章 

  • 相关阅读:
    JQuery Ajax 在asp.net中使用总结
    直接拿来用!最火的Android开源项目(一)
    专访邓凡平:Android开发路上的快速学习之道
    C/C++使用心得:enum与int的相互转换
    学习汇编的第一步
    《汇编程序》王爽实验9的解法(显示的问题)
    《汇编程序》王爽实验10.2的解法
    GDB调试精粹及使用实例
    aptget 使用详解
    《汇编程序》王爽实验10.3的解法
  • 原文地址:https://www.cnblogs.com/javashop-docs/p/13384677.html
Copyright © 2011-2022 走看看