文章目录

  • 前言
  • 一、框架搭建
  • 二、代码编写
    • 最麻烦的分页查询
  • 三、题外话
    • 源码下载

前言

之前在公司开发的一个产品,数据量巨大,疫情期间更是单月数据量增长超过100万,我们的单机MySQL数据库查询速度已经完全力不从心了,于是走上了分表的道路。我们的产品目标客户主要是学校,根据产品经理给的业务需求,决定按学期分表,即按主表数据入库时间按学期存放到对应分表,将原来超大的主表拆分成无数的小表,拆分后单表最大数据量为300万。
经过一段时间的考察研究决定使用ShardingJDBC来简化分表后的业务开发工作,ShardingJDBC的主要作用是根据设定好的路由规则将SQL语句路由到对应的数据库及分表上,由于我们使用的是单库分表,因此只涉及表的路由。在做业务开发的过程中踩过一些坑,也有了一些经验,于是在这里把代码抽取出来做成Demo来做一个分享。


提示:以下是本篇文章正文内容,下面案例可供参考

一、框架搭建

先介绍一下Demo使用的框架和版本,主要是SpringBoot全家桶2.2.6版,MyBatisPlus的2.3.3版以及我们今天的主角ShardingJDBC的4.0.1版本。
主要说下ShardingJDBC框架的集成与配置,首先在pom.xml引入依赖:

<dependency><groupId>org.apache.shardingsphere</groupId><artifactId>sharding-jdbc-spring-boot-starter</artifactId><version>${sharding-sphere.version}</version>
</dependency>

接下来在application.yml中加入配置:

spring:shardingsphere:datasource:names: dsds:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/shardingjdbc?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8&tinyInt1isBit=falseusername: rootpassword: admin123sharding:tables:t_order:actual-data-nodes: ds.t_order_$->{2020..2021}_$->{1..12}table-strategy:standard:sharding-column: create_timeprecise-algorithm-class-name: io.ian.demo.core.shardingjdbc.DatePreciseShardingAlgorithmrange-algorithm-class-name: io.ian.demo.core.shardingjdbc.DateRangeShardingAlgorithmt_order_item:actual-data-nodes: ds.t_order_item_$->{2020..2021}_$->{1..12}table-strategy:standard:sharding-column: order_create_timeprecise-algorithm-class-name: io.ian.demo.core.shardingjdbc.DatePreciseShardingAlgorithmrange-algorithm-class-name: io.ian.demo.core.shardingjdbc.DateRangeShardingAlgorithmbinding-tables:- t_order,t_order_itemdefaultDataSourceName: dsprops:sql:show: true

由于我们是单库分表,因此只配置一个数据源ds。主要说下表的配置,先看表结构:


t_order和t_order_item表以order_id字段关联,为一对多关系。t_order_item表的order_create_time字段值与关联的t_order表的create_time值相同,create_time字段和order_create_time字段分别为两表的分表字段。在ShardingJDBC中t_order和t_order_item表均为逻辑表,实际的表是例如t_order_2020_6这种带月后缀的表,这里我们为了简化分表逻辑进行了按月分表。具体的配置方式可以参考官方配置手册。

二、代码编写

下面我们着重看一下自定义的分表逻辑类DatePreciseShardingAlgorithm和DateRangeShardingAlgorithm:

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;import java.util.Collection;
import java.util.Date;public class DatePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {@Overridepublic String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> preciseShardingValue) {Date date = preciseShardingValue.getValue();String suffix = ShardingUtils.getSuffixByYearMonth(date);for (String tableName : availableTargetNames) {if (tableName.endsWith(suffix)) {return tableName;}}throw new IllegalArgumentException("未找到匹配的数据表");}
}

DatePreciseShardingAlgorithm实现了PreciseShardingAlgorithm接口,用于条件精确匹配时的表路由(=和in)。availableTargetNames就是我们在配置中配的actual-data-nodes,ds.t_order_$->{2020…2021}_$->{1…12}表示实际表为ds.t_order_2020_1到ds.t_order_2021_12共两年24张表。preciseShardingValue.getValue()获取到的是我们执行sql传递的时间参数,我们在这里用Date去做查询条件,不要使用String的日期。

import com.google.common.collect.Range;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;import java.util.*;@Slf4j
public class DateRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {@Overridepublic Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> rangeShardingValue) {List<String> list = new ArrayList<>();log.info("availableTargetNames : " + availableTargetNames);log.info(rangeShardingValue.toString());Range<Date> valueRange = rangeShardingValue.getValueRange();Date lowerDate = valueRange.lowerEndpoint();Date upperDate = valueRange.upperEndpoint();String lowerSuffix = ShardingUtils.getSuffixByYearMonth(lowerDate);String upperSuffix = ShardingUtils.getSuffixByYearMonth(upperDate);TreeSet<String> suffixList = ShardingUtils.getSuffixListForRange(lowerSuffix, upperSuffix);for (String tableName : availableTargetNames) {if (containTableName(suffixList, tableName)) {list.add(tableName);}}log.info("match tableNames-----------------------" + list.toString());return list;}private boolean containTableName(Set<String> suffixList, String tableName) {boolean flag = false;for (String s : suffixList) {if (tableName.endsWith(s)) {flag = true;break;}}return flag;}
}

DateRangeShardingAlgorithm 实现了RangeShardingAlgorithm接口,用于条件范围匹配时的路由(between、<、>等)。valueRange.lowerEndpoint()和valueRange.upperEndpoint()分别获取范围的上下边接,我们这两个时间的跨度去计算目标表的后缀,然后在可用的表中选取路由的目标表。

下面我们来看下增删改查的业务代码与不分表时有哪些变化。

新增t_order数据时我们给createTime赋值,在执行sql时会触发DatePreciseShardingAlgorithm中的精确匹配,直接把数据插入到createTime对应的分表,在代码上完全没有任何改动。
修改、单条删除、单条查询在分表前查询条件只需要id主键即可,但在分表后我们应该加上时间参数,可以是精确时间也可以是时间段,这样才能快速找到目标表,否则会在所有分表执行sql导致性能十分低下。

最麻烦的分页查询

首先希望大家能看下官方文档针对分页的章节。
我们为了获得总数据条数使用的count语句将会对所有分表进行查询然后归并结果,根据笔者在产品开发中的实际经验,当数据量到百万千万级别时,这个查询耗费的时间是不可接受的,因此在数据量巨大时,传统的分页查询方式是不可取的。为了解决这个问题从业务上我们可以不展示总数据量和总页数,而改用无限下拉滚动的方式来展示分页数据,但如果产品一定要展示呢?由于我们是按时间分表,因此从业务上看,除了当前月,其他的分表数据总量应该已经固定不再增长了,因此可以把当前月之前月份的数据总数做统计并存入中间表,这个数据再加上当前月的分表数据量就可以算出总数据量了,有了总数计算总页数也就简单了。但事实上我们的需求一般不会是对所有数据进行分页查询,而是有条件的对数据进行分页查询,比如本例中就是根据用户名进行订单的分页查询,那么我们的总数统计就应该按用户名去统计存入中间表。

下面简单介绍一下无限下拉滚动分页查询的实现:

/*** 从第二页开始lastCreateTime和lastRowNum为必选参数*/
@ApiOperation(value = "无限滚动分页查询", httpMethod = "GET")
@ApiImplicitParams({@ApiImplicitParam(name = "lastCreateTime", value = "上一页最后一条数据的创建时间", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "lastRowNum", value = "上一页最后一条数据的行号", dataType = "int", paramType = "query"),@ApiImplicitParam(name = "size", value = "每页条数", dataType = "int", paramType = "query"),@ApiImplicitParam(name = "userName", value = "用户名", dataType = "String", paramType = "query")
})
@GetMapping("/listPageByTime")
public ResponseEntity<RestResult> listPageByTime(String lastCreateTime, Integer lastRowNum, Integer size, String userName) {return ResponseEntity.ok(RestResult.getSuccessRestResult(orderService.listPageByTime(lastCreateTime, lastRowNum, size, userName)));
}

查询第一页数据时只需要提供每页条数参数即可,如果需要根据用户名查询可以加入用户名参数。从第二页开始就需要传入上一页最后一条数据的创建时间,以及上一页最后一条数据的行号。

@Override
public List<Order> listPageByTime(String lastCreateTime, Integer lastRowNum, Integer size, String userName) {//lastCreateTime 有助于快速定位当前查询的分表 ,如果是第一页则可不传,默认使用当前时间Date date = StringUtils.isBlank(lastCreateTime) ? new Date() : DateUtils.parseTime(lastCreateTime);String suffix = ShardingUtils.getSuffixByYearMonth(date);int resultSize = size == null ? 10 : size;//rowNum用于获取当前页数据的起始位置,如果是第一页可以不传,默认为0int rowNum = lastRowNum == null ? 0 : lastRowNum;List<Order> orderList = baseMapper.listByRowNum(suffix, resultSize, rowNum, userName);if (orderList.size() > 0) {while (orderList.size() < resultSize) { //查询出的数据不足 找更早的分表补足if ("2020_6".equals(suffix)) {    //假设最早的分表为 t_order_2020_6break;}suffix = ShardingUtils.getPrevSuffix(suffix);List<Order> tempOrderList = baseMapper.listByRowNum(suffix, resultSize - orderList.size(), 0, userName);if (tempOrderList.size() > 0) {orderList.addAll(tempOrderList);}}//获取orderList中数据的时间范围 查询子表数据Wrapper<OrderItem> orderItemWrapper = new EntityWrapper<OrderItem>().between("order_create_time", orderList.get(orderList.size() - 1).getCreateTime(), orderList.get(0).getCreateTime());this.appendOrderItem(orderList, orderItemWrapper);}return orderList;
}

上一页的最后一条数据的创建时间用于计算上次查询结束时数据所在的分表,此次查询也从该分表开始。如果该分表剩余数据不足每页条数,则继续查上一个月的分表,直到查询出的数据达到每页条数或已超过最早月份的分表。

SELECTA.*
FROM(SELECT@rownum := @rownum + 1 AS rowNum,t.*FROMt_order_${suffix} t,( SELECT @rownum := 0 ) r<trim prefix="WHERE" prefixOverrides="AND|OR"><if test="userName != null and userName != ''">AND t.user_name = #{userName}</if></trim>ORDER BYcreate_time DESC,id) A
WHEREA.rowNum > #{rowNum}LIMIT #{size}

从上面SQL语句可以看出,行号指的是当前分表里数据排序后的序号。因此我们可以根据上一页最后一条数据行号找到当前页的数据起始位置。
该分页查询方式经过实际测试在单分表数据量超过百万,总数据量超过千万级别时仍然可以在1秒内完成请求响应,当然查询条件需要建立索引。


三、题外话

强烈建议在数据量特别巨大的表使用长整型作为主键数据类型,在数据量超过百万同时用长整型数据作为外键查询时,在都建立索引的情况下,查询速度比字符类型外键快上十倍到百倍!当然长整型作为主键在传递到前端时会有JS精度丢失问题,这个问题可以通过转字符串传递解决。

源码下载

觉得有用的朋友顺手给个STAR吧

使用ShardingJDBC实现按时间维度分表轻松支撑千万级数据相关推荐

  1. Sharding-Jdbc 实现读写分离 + 分库分表,写得太好了!

    欢迎关注方志朋的博客,回复"666"获面试宝典 来自:CSDN,作者:邋遢的流浪剑客 链接:https://blog.csdn.net/qq_40378034/article/de ...

  2. mysql 按时间分库分表_sharding-jdbc 按时间分库分表

    一. 分库分表规则 会计系统按会计时间分库分表(一月一张表,一年一个库): 以center_entry_flow表为例: arch_center_acct(database) center_entry ...

  3. RocketMQ 任意时间维度的延迟消息(秒级)

    RocketMQ 任意时间维度的延迟消息(秒级) 基于开源版本固定等级的延迟消息实现原理的基础上进行扩展,将所有维度的延迟消息封装成任务添加到时间轮上,通过时间轮固定周期的扫描,检测任务是否到执行时间 ...

  4. 使用shardingJDBC实现数据库的分库分表

    一.概念 1.什么是分库分表?为什么要分库分表? 随着业务的增加,数据库的数据量越来越多,当数据量达到1000w或者100G后,数据的查询效率就会越来越低,为了提高效率,把数据量分散在不同的数据库中或 ...

  5. mysql分库分表中间件6_当当开源sharding-jdbc,轻量级数据库分库分表中间件

    近期,当当开源了数据库分库分表中间件sharding-jdbc. Sharding-JDBC是当当应用框架ddframe中,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数据 ...

  6. 当当网mysql分库分表策略_当当开源sharding-jdbc,轻量级数据库分库分表中间件

    近期,当当开源了数据库分库分表中间件sharding-jdbc. Sharding-JDBC是当当应用框架ddframe中,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数据 ...

  7. mysql 多维度分表_亿级订单数据分库分表设计方案(满足多维度查询:订单号、用户、商家、渠道)...

    根据业务初步预估订单业务量,每天500万的数据.我们将订单数据划分为了2大类型:分别为热数据和冷数据. 热数据:1个月内的订单数据,查询实时性较高; 冷数据:归档订单数据,查询频率不高; 根据实际业务 ...

  8. 256 变 4096:分库分表扩容如何实现平滑数据迁移?

    作者 | 亮言 背景 2020年,笔者负责的一个高德打车弹外订单系统进行了一次扩分库分表和数据库迁移.该订单系统整体部署在阿里云上,服务使用ECS部署.数据库采用RDS.配置中心基于阿里云ACM自研. ...

  9. 自己动手写一个分库分表中间件(三)数据源路由实现

    相关文章: 自己动手写一个分库分表中间件(一)思考 自己动手写一个分库分表中间件(二)数据源定义和分片代理层设计 排查项目中读写分离失效原因 小议 Java 内省机制 注:本文内容暂不涉及事务相关的问 ...

最新文章

  1. 深度学习与传统图像识别
  2. Android 中文API (94) —— MediaController
  3. JS进阶篇--ckplayer.js视频播放插件
  4. 如图两道面试题,顺便深入线程池,并连环17问
  5. Sublime Text 常用插件和快捷键
  6. foxmail提示不知道这样的主机_不知道和婚礼策划师沟通时谈哪些?这样做让你高效备婚...
  7. 2021常用单细胞数据库整理汇总
  8. BCGSoft Demo示例展示:控件示例集合(1/2)
  9. 力士乐伺服电机编码器调零_力士乐伺服电机编码器故障维修来这里
  10. tkinter中鼠标与键盘事件
  11. 吴裕雄 Bootstrap 前端框架开发——Bootstrap 图片:为图片添加圆角 (IE8 不支持)
  12. pp助手苹果版本_pp 助手停止服务/共享 ID 结束,旧版应用还能这样下载
  13. 文献检索与下载的几种方式
  14. 数值分析(2):线性代数方程组的直接解法
  15. word2007在试图打开文件时遇到错误解决方法
  16. 分布式计算原理之分布式协调与同步(1)——分布式互斥
  17. 【资讯】1177- 速看!2021 年 Google 最热门 Chrome 扩展程序名单出炉!
  18. word中设置奇偶页页眉页脚
  19. 单枪匹马的背锅侠,终于不再是一个人的军团了
  20. 8086cpu中的寄存器(cs、ds、es、ss、ip、sp、bp、si、di)

热门文章

  1. 深化校企合作|云畅科技与湖南女子学院签订校企合作协议
  2. 外贸网站必看!10个谷歌排名优化提升技巧
  3. VR制作中必须踩的坑365之026(oculus2、UE4、UE5、VR记录一年的踩坑之旅)UE HDR HDRI 天空球踩坑教程
  4. AUTOSAR CP、AP及其版本
  5. 【虚拟机数据恢复】碎片拼接恢复XenServer服务器被删除的虚拟机
  6. IEEE P2807.4《科技知识图谱指南》标准启动会成功召开
  7. 滴滴打车java_Java滴滴打车系统实现(同时实现租车购物车)
  8. 程序员应知必会的思维模型之 21 墨菲定律 (Murphy‘s Law / Sod‘s Law)
  9. 西瓜书笔记系列 - 第1章 绪论 - 1.1 引言
  10. 上机6-电自1902郑媛元