浅谈订单重构之 MySQL 分库分表实战篇

目录
  • 一、目标
  • 二、环境准备
    • 1、基本信息
    • 2、数据库环境准备
    • 3、建库 & 导入分表
  • 三、配置&实践
    • 1、pom文件  
    • 2、常量配置
    • 3、yml 配置
    • 4、分库分表策略
    • 5、dao层编写
    • 6、单元测试
  • 四、总结

    一、目标

    本文将完成如下目标:

    • 分表数量: 256    分库数量: 4
    • 以用户id(user_id) 为数据库分片key
    • 最后测试订单创建,更新,删除, 单订单号查询,根据user_id查询列表操作。

    架构图:

    表结构如下:

    create table `order_xxx` (
      `order_id` bigint(20) unsigned not null,
      `user_id` int(11) default '0' comment '订单id',
      `status` int(11) default '0' comment '订单状态',
      `booking_date` datetime default null,
      `create_time` datetime default null,
      `update_time` datetime default null,
      primary key (`order_id`),
      key `idx_user_id` (`user_id`),
      key `idx_bdate` (`booking_date`),
      key `idx_ctime` (`create_time`),
      key `idx_utime` (`update_time`)
    ) engine=innodb default charset=utf8;
    
    

    注:  000<= xxx <= 255, 本文重点在于分库分表实践, 只保留具有代表性字段,其它场景可以在此基础上做改进。

    全局唯一id设计

    要求:1.全局唯一 2:粗略有序 3:可反解出库编号

    • 1bit + 39bit时间差 + 8bit机器号 + 8bit用户编号(库号) + 8bit自增序列
    订单号组成项 保留字段 毫秒级时间差 机器数 用户编号(表编号) 自增序列
    所占字节(单位bit) 1 39 8 8 8

    单机最大qps: 256000 使用寿命: 17年

    二、环境准备

    1、基本信息

    版本 备注
    springboot 2.1.10.release
    mango 1.6.16 wiki地址:https://github.com/jfaster/mango
    hikaricp 3.2.0
    mysql 5.7 测试使用docker一键搭建

    2、数据库环境准备

    进入mysql:

    #主库
     mysql -h 172.30.1.21 -uroot -pbytearch
    
    #从库
     mysql -h 172.30.1.31 -uroot -pbytearch
    
    
    

    进入容器

    #主
    docker exec -it db_1_master /bin/bash
    
    #从
    docker exec -it db_1_slave /bin/bash
    
    
    

    查看运行状态

    #主
    docker exec db_1_master sh -c 'mysql -u root -pbytearch -e "show master status \g"'
    #从
    docker exec db_1_slave sh -c 'mysql -u root -pbytearch -e "show slave status \g"' 

    3、建库 & 导入分表

    (1)在mysql master实例分别建库

    172.30.1.21(   o rder_db_ 1) ,  172.30.1.22( order_db_2) ,

    172.30.1.23( ord er_db_3) ,   172.30.1.24( order_db_4 )

    (2)依次导入建表sql 命令为

    mysql -uroot -pbytearch -h172.30.1.21 order_db_1<fast-cloud-mysql-sharding/doc/sql/order_db_1.sql;
    mysql -uroot -pbytearch -h172.30.1.22 order_db_2<fast-cloud-mysql-sharding/doc/sql/order_db_2.sql;
    mysql -uroot -pbytearch -h172.30.1.23 order_db_3<fast-cloud-mysql-sharding/doc/sql/order_db_3.sql;
    mysql -uroot -pbytearch -h172.30.1.24 order_db_4<fast-cloud-mysql-sharding/doc/sql/order_db_4.sql;  
    
    

    三、配置&实践

    1、pom文件  

         <!-- mango 分库分表中间件 --> 
                <dependency>
                    <groupid>org.jfaster</groupid>
                    <artifactid>mango-spring-boot-starter</artifactid>
                    <version>2.0.1</version>
                </dependency>
             
                 <!-- 分布式id生成器 -->
                <dependency>
                    <groupid>com.bytearch</groupid>
                    <artifactid>fast-cloud-id-generator</artifactid>
                    <version>${version}</version>
                </dependency>
    
                <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
                <dependency>
                    <groupid>mysql</groupid>
                    <artifactid>mysql-connector-java</artifactid>
                    <version>6.0.6</version>
                </dependency>
    

    2、常量配置

    package com.bytearch.fast.cloud.mysql.sharding.common;
    
    /**
     * 分库分表策略常用常量
     */
    public class shardingstrategyconstant {
        /**
         * database 逻辑名称 ,真实库名为 order_db_xxx
         */
        public static final string logic_order_database_name = "order_db";
        /**
         * 分表数 256,一旦确定不可更改
         */
        public static final int sharding_table_num = 256;
    
        /**
         * 分库数, 不建议更改, 可以更改,但是需要dba迁移数据
         */
        public static final int sharding_database_node_num = 4;
    }
    

    3、yml 配置

    4主4从数据库配置, 这里仅测试默认使用root用户密码,生产环境不建议使用root用户。

    mango:
      scan-package: com.bytearch.fast.cloud.mysql.sharding.dao
      datasources:
        - name: order_db_1
          master:
            driver-class-name: com.mysql.cj.jdbc.driver
            jdbc-url: jdbc:mysql://172.30.1.21:3306/order_db_1?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
            user-name: root
            password: bytearch
            maximum-pool-size: 10
            connection-timeout: 3000
          slaves:
            - driver-class-name: com.mysql.cj.jdbc.driver
              jdbc-url: jdbc:mysql://172.30.1.31:3306/order_db_1?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
              user-name: root
              password: bytearch
              maximum-pool-size: 10
              connection-timeout: 3000
        - name: order_db_2
          master:
            driver-class-name: com.mysql.cj.jdbc.driver
            jdbc-url: jdbc:mysql://172.30.1.22:3306/order_db_2?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
            user-name: root
            password: bytearch
            maximum-pool-size: 10
            connection-timeout: 3000
          slaves:
            - driver-class-name: com.mysql.cj.jdbc.driver
              jdbc-url: jdbc:mysql://172.30.1.32:3306/order_db_2?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
              user-name: root
              password: bytearch
              maximum-pool-size: 10
              connection-timeout: 3000
        - name: order_db_3
          master:
            driver-class-name: com.mysql.cj.jdbc.driver
            jdbc-url: jdbc:mysql://172.30.1.23:3306/order_db_3?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
            user-name: root
            password: bytearch
            maximum-pool-size: 10
            connection-timeout: 3000
          slaves:
            - driver-class-name: com.mysql.cj.jdbc.driver
              jdbc-url: jdbc:mysql://172.30.1.33:3306/order_db_3?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
              user-name: root
              password: bytearch
              maximum-pool-size: 10
              connection-timeout: 3000
        - name: order_db_4
          master:
            driver-class-name: com.mysql.cj.jdbc.driver
            jdbc-url: jdbc:mysql://172.30.1.24:3306/order_db_4?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
            user-name: root
            password: bytearch
            maximum-pool-size: 10
            connection-timeout: 3000
          slaves:
            - driver-class-name: com.mysql.cj.jdbc.driver
              jdbc-url: jdbc:mysql://172.30.1.34:3306/order_db_4?useunicode=true&characterencoding=utf8&autoreconnect=true&rewritebatchedstate&connecttimeout=1000&sockettimeout=5000&usessl=false
              user-name: root
              password: bytearch
              maximum-pool-size: 10
              connection-timeout: 300
    

    4、分库分表策略

    1). 根据order_id为shardkey分库分表策略

    package com.bytearch.fast.cloud.mysql.sharding.strategy;
    
    import com.bytearch.fast.cloud.mysql.sharding.common.shardingstrategyconstant;
    import com.bytearch.id.generator.identity;
    import com.bytearch.id.generator.seqidutil;
    import org.jfaster.mango.sharding.shardingstrategy;
    
    /**
     * 订单号分库分表策略
     */
    public class orderidshardingstrategy implements shardingstrategy<long, long> {
        @override
        public string getdatasourcefactoryname(long orderid) {
            if (orderid == null || orderid < 0l) {
                throw new illegalargumentexception("order_id is invalid!");
            }
            identity identity = seqidutil.decodeid(orderid);
            if (identity.getextraid() >= shardingstrategyconstant.sharding_table_num) {
                throw new illegalargumentexception("sharding table num is invalid, tablenum:" + identity.getextraid());
            }
            //1. 计算步长
            int step = shardingstrategyconstant.sharding_table_num / shardingstrategyconstant.sharding_database_node_num;
            //2. 计算出库编号
            long dbno = math.floordiv(identity.getextraid(), step) + 1;
            //3. 返回数据源名
            return string.format("%s_%s", shardingstrategyconstant.logic_order_database_name, dbno);
        }
    
        @override
        public string gettargettable(string logictablename, long orderid) {
            if (orderid == null || orderid < 0l) {
                throw new illegalargumentexception("order_id is invalid!");
            }
            identity identity = seqidutil.decodeid(orderid);
            if (identity.getextraid() >= shardingstrategyconstant.sharding_table_num) {
                throw new illegalargumentexception("sharding table num is invalid, tablenum:" + identity.getextraid());
            }
            // 基于约定,真实表名为 logictablename_xxx, xxx不足三位补0
            return string.format("%s_%03d", logictablename, identity.getextraid());
        }
    }
    
    

    2). 根据user_id 为shardkey分库分表策略

    package com.bytearch.fast.cloud.mysql.sharding.strategy;
    
    import com.bytearch.fast.cloud.mysql.sharding.common.shardingstrategyconstant;
    import org.jfaster.mango.sharding.shardingstrategy;
    
    /**
     * 指定分片key 分库分表策略
     */
    public class useridshardingstrategy implements shardingstrategy<integer, integer> {
    
        @override
        public string getdatasourcefactoryname(integer userid) {
            //1. 计算步长 即单库放得表数量
            int step = shardingstrategyconstant.sharding_table_num / shardingstrategyconstant.sharding_database_node_num;
            //2. 计算出库编号
            long dbno = math.floordiv(userid % shardingstrategyconstant.sharding_table_num, step) + 1;
            //3. 返回数据源名
            return string.format("%s_%s", shardingstrategyconstant.logic_order_database_name, dbno);
        }
    
        @override
        public string gettargettable(string logictablename, integer userid) {
            // 基于约定,真实表名为 logictablename_xxx, xxx不足三位补0
            return string.format("%s_%03d", logictablename, userid % shardingstrategyconstant.sharding_table_num);
        }
    }
    
    

    5、dao层编写

    1). orderpartitionbyiddao

    package com.bytearch.fast.cloud.mysql.sharding.dao;
    
    import com.bytearch.fast.cloud.mysql.sharding.common.shardingstrategyconstant;
    import com.bytearch.fast.cloud.mysql.sharding.pojo.entity.orderentity;
    import com.bytearch.fast.cloud.mysql.sharding.strategy.orderidshardingstrategy;
    import org.jfaster.mango.annotation.*;
    
    @db(name = shardingstrategyconstant.logic_order_database_name, table = "order")
    @sharding(shardingstrategy = orderidshardingstrategy.class)
    public interface orderpartitionbyiddao {
    
        @sql("insert into #table (order_id, user_id, status, booking_date, create_time, update_time) values" +
                "(:orderid,:userid,:status,:bookingdate,:createtime,:updatetime)"
        )
        int insertorder(@tableshardingby("orderid") @databaseshardingby("orderid") orderentity orderentity);
    
        @sql("update #table set update_time = now()" +
                "#if(:bookingdate != null),booking_date = :bookingdate #end " +
                "#if (:status != null), status = :status #end" +
                "where order_id = :orderid"
        )
        int updateorderbyorderid(@tableshardingby("orderid") @databaseshardingby("orderid") orderentity orderentity);
    
    
        @sql("select * from #table where order_id = :1")
        orderentity getorderbyid(@tableshardingby @databaseshardingby long orderid);
    
        @sql("select * from #table where order_id = :1")
        @usemaster
        orderentity getorderbyidfrommaster(@tableshardingby @databaseshardingby long orderid);
    
    

    6、单元测试

    @springboottest(classes = {application.class})
    @runwith(springjunit4classrunner.class)
    public class shardingtest {
        @autowired
        orderpartitionbyiddao orderpartitionbyiddao;
    
        @autowired
        orderpartitionbyuseriddao orderpartitionbyuseriddao;
    
        @test
        public void testcreateorderrandom() {
            for (int i = 0; i < 20; i++) {
                int userid = threadlocalrandom.current().nextint(1000,1000000);
                orderentity orderentity = new orderentity();
                orderentity.setorderid(seqidutil.nextid(userid % shardingstrategyconstant.sharding_table_num));
                orderentity.setstatus(1);
                orderentity.setuserid(userid);
                orderentity.setcreatetime(new date());
                orderentity.setupdatetime(new date());
                orderentity.setbookingdate(new date());
                int ret = orderpartitionbyiddao.insertorder(orderentity);
                assert.assertequals(1, ret);
            }
        }
    
        @test
        public void testorderall() {
            //insert
            int userid = threadlocalrandom.current().nextint(1000,1000000);
            orderentity orderentity = new orderentity();
            orderentity.setorderid(seqidutil.nextid(userid % shardingstrategyconstant.sharding_table_num));
            orderentity.setstatus(1);
            orderentity.setuserid(userid);
            orderentity.setcreatetime(new date());
            orderentity.setupdatetime(new date());
            orderentity.setbookingdate(new date());
            int i = orderpartitionbyiddao.insertorder(orderentity);
            assert.assertequals(1, i);
    
            //get from master
            orderentity orderinfo = orderpartitionbyiddao.getorderbyidfrommaster(orderentity.getorderid());
            assert.assertnotnull(orderinfo);
            assert.assertequals(orderinfo.getorderid(), orderentity.getorderid());
    
            //get from slave
            orderentity slaveorderinfo = orderpartitionbyiddao.getorderbyid(orderentity.getorderid());
            assert.assertnotnull(slaveorderinfo);
            //update
            orderentity updateentity = new orderentity();
            updateentity.setorderid(orderinfo.getorderid());
            updateentity.setstatus(2);
            updateentity.setupdatetime(new date());
            int affectrows = orderpartitionbyiddao.updateorderbyorderid(updateentity);
            assert.asserttrue( affectrows > 0);
        }
    
        @test
        public void testgetlistbyuserid() {
            int userid = threadlocalrandom.current().nextint(1000,1000000);
            for (int i = 0; i < 5; i++) {
                orderentity orderentity = new orderentity();
                orderentity.setorderid(seqidutil.nextid(userid % shardingstrategyconstant.sharding_table_num));
                orderentity.setstatus(1);
                orderentity.setuserid(userid);
                orderentity.setcreatetime(new date());
                orderentity.setupdatetime(new date());
                orderentity.setbookingdate(new date());
                orderpartitionbyiddao.insertorder(orderentity);
            }
            try {
                //防止主从延迟引起的校验错误
                thread.sleep(1000);
            } catch (interruptedexception e) {
                e.printstacktrace();
            }
            list<orderentity> orderlistbyuserid = orderpartitionbyuseriddao.getorderlistbyuserid(userid);
            assert.assertnotnull(orderlistbyuserid);
            assert.asserttrue(orderlistbyuserid.size() == 5);
        }
    }
    
    

    大功告成:

    四、总结

    本篇主要介绍java版使用mango框架实现mysql分库分表实战,分库分表中间件也可以使用类似于shardingjdbc,或者自研。

    以上分库分表数量仅供演示参考,实际工作中分表数量、分库数量、是根据公司实际业务数据增长速度, 高峰期qps,物理机器配置等等因素计算。

    到此这篇关于浅谈订单重构之 mysql 分库分表实战篇的文章就介绍到这了,更多相关mysql 分库分表内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

    (0)
    上一篇 2022年3月21日
    下一篇 2022年3月21日

    相关推荐