使用ShardingSphere实现分库分表以及事务回滚含性能测试(附代码)

ShardingSphere的官网

1.简介

Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的数据水平扩展、分布式事务和分布式治理等功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。

Apache ShardingSphere 旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。 关系型数据库当今依然占有巨大市场份额,是企业核心系统的基石,未来也难于撼动,我们更加注重在原有基础上提供增量,而非颠覆。

Apache ShardingSphere 5.x 版本开始致力于可插拔架构,项目的功能组件能够灵活的以可插拔的方式进行扩展。 目前,数据分片、读写分离、数据加密、影子库压测等功能,以及 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 与协议的支持,均通过插件的方式织入项目。 开发者能够像使用积木一样定制属于自己的独特系统。Apache ShardingSphere 目前已提供数十个 SPI 作为系统的扩展点,仍在不断增加中。

在这里插入图片描述

2.ShardingSphere的核心概念

  • 分片
    一般我们在提到分库分表的时候,大多是以水平切分模式(水平分库、分表)为基础来说的,数据分片将原本一张数据量较大的表 t_order 拆分生成数个表结构完全一致的小数据量表 t_order_0、t_order_1、···、t_order_n,每张表只存储原大表中的一部分数据,当执行一条SQL时会通过 分库策略、分片策略 将数据分散到不同的数据库、表内。
  • 数据节点
    数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成,例如上图中 order_db_1.t_order_0、order_db_2.t_order_1 就表示一个数据节点。
  • 逻辑表
    逻辑表是指一组具有相同逻辑和数据结构表的总称。比如我们将订单表t_order 拆分成 t_order_0 ··· t_order_9 等 10张表。此时我们会发现分库分表以后数据库中已不在有 t_order 这张表,取而代之的是 t_order_n,但我们在代码中写 SQL 依然按 t_order 来写。此时 t_order 就是这些拆分表的逻辑表。
  • 真实表
    真实表也就是上边提到的 t_order_n 数据库中真实存在的物理表。
  • 分片键
    用于分片的数据库字段。我们将 t_order 表分片以后,当执行一条SQL时,通过对字段 order_id 取模的方式来决定,这条数据该在哪个数据库中的哪个表中执行,此时 order_id 字段就是 t_order 表的分片健。

分布式主键

数据分⽚后,不同数据节点⽣成全局唯⼀主键是⾮常棘⼿的问题,同⼀个逻辑表(t_order)内的不同真实表(t_order_n)之间的⾃增键由于⽆法互相感知而产⽣重复主键。

尽管可通过设置⾃增主键 初始值 和 步⻓ 的⽅式避免ID碰撞,但这样会使维护成本加大,乏完整性和可扩展性。如果后去需要增加分片表的数量,要逐一修改分片表的步长,运维成本非常高,所以不建议这种方式。

建议使用分布式主键⽣成器等开源项目。

3.使用ShardingSphere进行数据库分库分表

pom

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis驱动-->
        <!-- mybatis plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--druid数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--shardingsphere最新版本-->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-core-common</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-namespace</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>
        <!--lombok实体工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

application.yaml

server:
  port: 10001

# mybatis-plus相关配置
mybatis-plus:
  # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
  mapper-locations: classpath:mapper/*.xml
  typeAliasesPackage: com.whxd.sharding.web.entity
  configuration:
    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: true
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
    call-setters-on-nulls: true
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      #数据源名称,多数据源以逗号分隔
      names: logs0,logs1
      #配置数据源具体内容,包含连接池,驱动,地址,用户名和密码
      logs0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/logs0?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=true&&serverTimezone=Asia/Shanghai
        username: xxxxxx
        password: xxxxxx
      logs1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/logs1?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=true&&serverTimezone=Asia/Shanghai
        username: xxxxxx
        password: xxxxxx
    sharding:
      tables:
        operator_log:
          actual-data-nodes: logs$->{0..1}.operator_log_$->{0..5}
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: operator_log_$->{id % 5}
          #分表策略,同分库策略
          key-generator:
            column: id
            #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
            type: SNOWFLAKE
      default-database-strategy:
        inline:
          sharding-column: id
          algorithm-expression: logs$->{id % 2}
    props:
      sql:
        show: true

启动类

@SpringBootApplication
@MapperScan("com.whxd.sharding.web.mapper")
public class ShardingSphereApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShardingSphereApplication.class, args);
    }

    @Bean
    @ConditionalOnMissingBean
    public PaginationInterceptor paginationInterceptor() {
        // 开启 count 的 join 优化,只针对 left join !!!
        return new PaginationInterceptor();
    }
}

controller层

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author KimWu
 * @since 2021-08-09
 */
@RestController
@RequestMapping("/operatorLog")
public class OperatorLogController {

    @Autowired
    private IOperatorLogService operatorLogService;

    @PostMapping("/insert")
    public String insert(){
        try {
            for (int i = 1; i<100;i++){
                operatorLogService.save(build());
            }
            return "成功";
        } catch (Exception e) {
            e.printStackTrace();
            return "失败";
        }
    }

    @PostMapping("/page")
    public List<OperatorLog> page(){
        List<OperatorLog> list = operatorLogService.list();
        return list;
    }

    private OperatorLog build(){
        return new OperatorLog(
            null,System.currentTimeMillis()+"",null,null,null,
                null,null, null,null, CreatName.getName(),null,null,
                //随机生成姓名
                CreatName.getName(),null,null,null,null,null,null
        );
    }

}

数据库表的sql

DROP TABLE IF EXISTS `operator_log`;
CREATE TABLE `operator_log`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `excute_time` mediumtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '接口執行耗時(毫秒)',
  `remote_addr` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求IP',
  `request_method` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'HTTP方法',
  `system` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '所属系统',
  `module` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '請求模塊',
  `api` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '接口功能描述',
  `uri` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求URL',
  `params` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求参数',
  `user_name` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作人名称',
  `user_id` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作人id',
  `session_id` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '会话ID',
  `cur_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '当前用户(預留字段)',
  `bean_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '类名',
  `result` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求结果',
  `time` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求时间',
  `exc_name` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '异常名称',
  `exc_message` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '异常内容',
  `created` datetime(0) NULL DEFAULT NULL COMMENT '創建時間',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

如何测试是否已经完成分库分表:

创建两个数据库log0,log1,分别创建operator_log_0至4的表

调用新增的接口,看是否每张表都插入了数据,

而后调用查询接口,看是否能够都查询出来。

ShardingSphere分库分表如何实现事务回滚

启动类上添加注解

@EnableTransactionManagement
@SpringBootApplication
@MapperScan("com.whxd.sharding.web.mapper")
@EnableTransactionManagement
public class ShardingSphereApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShardingSphereApplication.class, args);
    }

    @Bean
    @ConditionalOnMissingBean
    public PaginationInterceptor paginationInterceptor() {
        // 开启 count 的 join 优化,只针对 left join !!!
        return new PaginationInterceptor();
    }
}

执行方法上加上

@ShardingTransactionType(TransactionType.XA)
@Transactional(rollbackFor = Exception.class)
    @PostMapping("/insert")
    @ShardingTransactionType(TransactionType.XA)
    @Transactional(rollbackFor = Exception.class)
    public String insert() {
        for (int i = 1; i < 100; i++) {
            operatorLogService.save(build());
            if (i == 90) {
                throw new RuntimeException("系统异常");
            }
        }
        return "成功";
    }

测试执行完成后,已插入数据进行了事务回滚。保证了分库分表时的一致性。

在这里插入图片描述

ShardingSphere百万级数量分库分表后性能测试

测试案例使用环境:

①两个数据库 logs0和logs1
②分别5张数据表,共10张数据表。operator_log_0 - 4
③数据库未建立索引

插入一百万条数据

    @PostMapping("/insert")
    @ShardingTransactionType(TransactionType.XA)
    @Transactional(rollbackFor = Exception.class)
    public String insert() {
        for (int i = 1; i <= 1000000; i++) {
            operatorLogService.save(build());
            log.info("正在执行数据库插入 {}", i);
        }
        return "成功";
    }

方法执行时间耗时较长,建议空闲时段执行,避免cpu飙升。

查询方法

    @PostMapping("/page")
    public List<OperatorLog> page() {
        long startTime = System.currentTimeMillis();
        List<OperatorLog> list = operatorLogService.list(new QueryWrapper<OperatorLog>().gt("excute_time",1629774891192L));
        log.info("耗时 : {} 秒 ",(System.currentTimeMillis()-startTime)/1000D);
        return list;
    }

在这里插入图片描述
当数据库的数据量过大,大到一定的程度,我们就可以进行分库分表。
分库分表在单表数据量很大的情况下,使用该种方法是一种不错的选择(常用的可以使用日志记录场景)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值