业务设计——海量订单数据如何存储和查询

news2025/1/11 5:22:24

冷热数据架构

        假设我们考虑 12306 单个假期的人流量为 2 亿人次,这一估算基于每年的三个主要假期:五一、国庆和春节。这些假期通常都有来回的流动,因此数据存储与计算的公式变为:2 * (3*2) = 12 亿,即每年的假期总人次达到了 12 亿。

        考虑到假期订单数据以及日常购票数据的累积,随着多年的积累,数据量将会变得相当庞大。但我们需要再次审视一个关键问题:这些订单数据是否需要一直保留在数据库中呢?

        经过详细分析 12306 车票订单购买查看逻辑,我们发现用户账号只能查看最近一个月内的订单购买记录。这一时间范围最多涵盖一个节假日周期,考虑往返车票等情况,大致数据量约为 4 亿。这样的数据规模相较之前大幅减少,有效降低了整体的存储压力。

上述的这种数据存储技术叫做冷热数据的架构方案,那什么叫做冷数据?什么又是热数据?

  • 热数据通常指经常被访问和使用的数据,如最近的交易记录或最新的新闻文章等。这些数据需要快速的读写速度和响应时间,因此通常存储在快速存储介质(如内存或快速固态硬盘)中,以便快速访问和处理。
  • 冷数据则指很少被访问和使用的数据,如过去的交易记录或旧的新闻文章等。这些数据访问频率较低,但需要长期保存,因此存储在较慢的存储介质(如磁盘或云存储)中,以便节省成本和存储空间。

        如何实现这种冷热数据存储架构?比较简单的方案就是,我们每天有个定时任务,把一个月前的数据从当前的数据库迁移到冷数据库中。这里就涉及了分库分表操作。

        这时需要注意一件事情,就是我们迁移到冷数据库不意味着不查询这些数据。如果遇到查询历史数据的需求,我们还是要能支持,比如支付宝的交易数据查询。

订单分片键选择

每每说到分库分表,最头疼的是莫过于如何选择分片键,用户名?订单号?还是创建时间?

先说我们的业务基本诉求,订单分库分表的基本查询条件有两种情况

  • 用户要能查看自己的订单
  • 支持订单号精准查询。

        这样的话,我们就需要按照两个字段当做分片键,这也就意味着每次查询时需要带着用户和订单两个字段,非常的不方便。能不能通过一个字段分库分表,但是查询时两个字段任意传一个就能精准查询,而不导致读扩散问题?

1. 基因法

这就需要用到咱们项目中使用的基因算法。那什么是分库分表基因算法?

        说的通俗易懂点,就是我们通过把用户的后六位数据冗余到订单号里。这样的话,我们就可以按照用户 ID 后六位进行分库分表,并且将分片键定义为用户 ID 和订单号,只要查询中携带这两个字段,我们就取用户 ID 后六位进行查找分片表的位置。

        这样我们就可以很好支持分库分表需求了,同时能满足用户和订单号两种查询逻辑,这也是大家热衷于使用基因算法的原因。

2. 订单号生成

为了保证订单号生成递增,我们参考雪花算法自定义了一个 DistributedIdGenerator,生成后的分布式 ID 再拼接上用户的后六位。

@Component
@RequiredArgsConstructor
public final class OrderIdGeneratorManager implements InitializingBean {

    private static DistributedIdGenerator DISTRIBUTED_ID_GENERATOR;

    /**
     * 生成订单全局唯一 ID
     *
     * @param userId 用户名
     * @return 订单 ID
     */
    public static String generateId(long userId) {
        return DISTRIBUTED_ID_GENERATOR.generateId() + String.valueOf(userId % 1000000);
    }
}

 这种将用户 ID 后六位拼接订单号后面的技术方案,是参考了淘宝的订单号设计。


订单分库分表代码实战

如果你没有使用过 ShardingSphere 分库分表操作,可以查看官网进行一些前置条件理解。

 1. 引入 ShardingSphere 依赖

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.3.2</version>
</dependency>

2. 定义分片规则

spring:
  datasource:
  	# ShardingSphere 对 Driver 自定义,实现分库分表等隐藏逻辑
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    # ShardingSphere 配置文件路径
    url: jdbc:shardingsphere:classpath:shardingsphere-config.yaml

3. 订单分片配置

为了避免繁琐,这里只分 2 个库以及对应业务 16 张表。

shardingsphere-config.yaml

# 数据源集合,也就是咱们刚才说的分两个库
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/12306_order_0?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/12306_order_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

rules:
  	# 分片规则
    - !SHARDING
    # 分片表
    tables:
      # 订单表
      t_order:
        # 真实的数据节点,也对应着在数据库中存储的真实表
        actualDataNodes: ds_${0..1}.t_order_${0..15}
        # 分库策略
        databaseStrategy:
          # 复合分库策略(多个分片键)
          complex:
            # 用户 ID 和订单号
            shardingColumns: user_id,order_sn
            # 搜索 order_database_complex_mod 下方会有分库算法
            shardingAlgorithmName: order_database_complex_mod
        # 分表策略
        tableStrategy:
          # 复合分表策略(多个分片键)
          complex:
            # 用户 ID 和订单号
            shardingColumns: user_id,order_sn
            # 搜索 order_table_complex_mod 下方会有分表算法
            shardingAlgorithmName: order_table_complex_mod
      # 订单明细表,规则同订单表
      t_order_item:
        actualDataNodes: ds_${0..1}.t_order_item_${0..15}
        databaseStrategy:
          complex:
            shardingColumns: user_id,order_sn
            shardingAlgorithmName: order_item_database_complex_mod
        tableStrategy:
          complex:
            shardingColumns: user_id,order_sn
            shardingAlgorithmName: order_item_table_complex_mod
    # 分片算法
    shardingAlgorithms:
      # 订单分库算法
      order_database_complex_mod:
        # 通过加载全限定名类实现分片算法,相当于分片逻辑都在 algorithmClassName 对应的类中
        type: CLASS_BASED
        props:
          algorithmClassName: org.opengoofy.index12306.biz.orderservice.dao.algorithm.OrderCommonDataBaseComplexAlgorithm
          # 分库数量
          sharding-count: 2
          # 复合(多分片键)分库策略
          strategy: complex
      # 订单分表算法
      order_table_complex_mod:
        # 通过加载全限定名类实现分片算法,相当于分片逻辑都在 algorithmClassName 对应的类中
        type: CLASS_BASED
        props:
          algorithmClassName: org.opengoofy.index12306.biz.orderservice.dao.algorithm.OrderCommonTableComplexAlgorithm
          # 分表数量
          sharding-count: 16
          # 复合(多分片键)分表策略
          strategy: complex
      order_item_database_complex_mod:
        type: CLASS_BASED
        props:
          algorithmClassName: org.opengoofy.index12306.biz.orderservice.dao.algorithm.OrderCommonDataBaseComplexAlgorithm
          sharding-count: 2
          strategy: complex
      order_item_table_complex_mod:
        type: CLASS_BASED
        props:
          algorithmClassName: org.opengoofy.index12306.biz.orderservice.dao.algorithm.OrderCommonTableComplexAlgorithm
          sharding-count: 16
          strategy: complex
props:
  sql-show: true

4. 分片算法解析

        调试的话可以分为两种,一种是创建订单,一种是查看订单,控制台都有现成的功能,Debug 到分片算法方法上就可以。

        因为订单和订单明细表都是按照用户和订单号进行的分片,分片算法规则一致,所以就进行了复用。

订单分库分片算法代码如下:

/**
 * 订单数据库复合分片算法配置
 * ComplexKeysShardingAlgorithm 是 ShardingSphere 预留出来的可扩展分片算法接口
 * 注意:不同版本的 ShardingSphere 可能包路径、类名或者方法名不一致
 */
public class OrderCommonDataBaseComplexAlgorithm implements ComplexKeysShardingAlgorithm {

    @Getter
    private Properties props;

    // 分库数量,读取的配置中定义的分库数量
    private int shardingCount;

    private static final String SHARDING_COUNT_KEY = "sharding-count";

    @Override
    public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
        Map<String, Collection<Comparable<Long>>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        if (CollUtil.isNotEmpty(columnNameAndShardingValuesMap)) {
            String userId = "user_id";
            // 首先判断 SQL 是否包含用户 ID,如果包含直接取用户 ID 后六位
            Collection<Comparable<Long>> customerUserIdCollection = columnNameAndShardingValuesMap.get(userId);
            if (CollUtil.isNotEmpty(customerUserIdCollection)) {
                // 获取到 SQL 中包含的用户 ID 对应值
                Comparable<?> comparable = customerUserIdCollection.stream().findFirst().get();
                // 如果使用 MybatisPlus 因为传入时没有强类型判断,所以有可能用户 ID 是字符串,也可能是 Long 等数值
                // 比如传入的用户 ID 可能是 1683025552364568576 也可能是 '1683025552364568576'
                // 根据不同的值类型,做出不同的获取后六位判断。字符串直接截取后六位,Long 类型直接通过 % 运算获取后六位
                if (comparable instanceof String) {
                    String actualOrderSn = comparable.toString();
                    // 获取真实数据库的方法其实还是通过 HASH_MOD 方式取模的,shardingCount 就是咱们配置中的分库数量
                    result.add("ds_" + hashShardingValue(actualOrderSn.substring(Math.max(actualOrderSn.length() - 6, 0))) % shardingCount);
                } else {
                    String dbSuffix = String.valueOf(hashShardingValue((Long) comparable % 1000000) % shardingCount);
                    result.add("ds_" + dbSuffix);
                }
            } else {
                // 如果对订单中的 SQL 语句不包含用户 ID 那么就要从订单号中获取后六位,也就是用户 ID 后六位
                // 流程同用户 ID 获取流程
                String orderSn = "order_sn";
                Collection<Comparable<Long>> orderSnCollection = columnNameAndShardingValuesMap.get(orderSn);
                Comparable<?> comparable = orderSnCollection.stream().findFirst().get();
                if (comparable instanceof String) {
                    String actualOrderSn = comparable.toString();
                    result.add("ds_" + hashShardingValue(actualOrderSn.substring(Math.max(actualOrderSn.length() - 6, 0))) % shardingCount);
                } else {
                    result.add("ds_" + hashShardingValue((Long) comparable % 1000000) % shardingCount);
                }
            }
        }
        // 返回的是表名,
        return result;
    }

    @Override
    public void init(Properties props) {
        this.props = props;
        shardingCount = getShardingCount(props);
    }

    private int getShardingCount(final Properties props) {
        Preconditions.checkArgument(props.containsKey(SHARDING_COUNT_KEY), "Sharding count cannot be null.");
        return Integer.parseInt(props.getProperty(SHARDING_COUNT_KEY));
    }

    private long hashShardingValue(final Comparable<?> shardingValue) {
        return Math.abs((long) shardingValue.hashCode());
    }
}

订单分表算法逻辑基本与订单分库算法一致,大家查看代码也基本上都能清楚,就不再过多赘述。

/**
 * 订单表相关复合分片算法配置
 */
public class OrderCommonTableComplexAlgorithm implements ComplexKeysShardingAlgorithm {

    @Getter
    private Properties props;

    private int shardingCount;

    private static final String SHARDING_COUNT_KEY = "sharding-count";

    @Override
    public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
        Map<String, Collection<Comparable<?>>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        if (CollUtil.isNotEmpty(columnNameAndShardingValuesMap)) {
            String userId = "user_id";
            Collection<Comparable<?>> customerUserIdCollection = columnNameAndShardingValuesMap.get(userId);
            if (CollUtil.isNotEmpty(customerUserIdCollection)) {
                Comparable<?> comparable = customerUserIdCollection.stream().findFirst().get();
                if (comparable instanceof String) {
                    String actualOrderSn = comparable.toString();
                    result.add(shardingValue.getLogicTableName() + "_" + hashShardingValue(actualOrderSn.substring(Math.max(actualOrderSn.length() - 6, 0))) % shardingCount);
                } else {
                    String dbSuffix = String.valueOf(hashShardingValue((Long) comparable % 1000000) % shardingCount);
                    result.add(shardingValue.getLogicTableName() + "_" + dbSuffix);
                }
            } else {
                String orderSn = "order_sn";
                Collection<Comparable<?>> orderSnCollection = columnNameAndShardingValuesMap.get(orderSn);
                Comparable<?> comparable = orderSnCollection.stream().findFirst().get();
                if (comparable instanceof String) {
                    String actualOrderSn = comparable.toString();
                    result.add(shardingValue.getLogicTableName() + "_" + hashShardingValue(actualOrderSn.substring(Math.max(actualOrderSn.length() - 6, 0))) % shardingCount);
                } else {
                    String dbSuffix = String.valueOf(hashShardingValue((Long) comparable % 1000000) % shardingCount);
                    result.add(shardingValue.getLogicTableName() + "_" + dbSuffix);
                }
            }
        }
        return result;
    }

    @Override
    public void init(Properties props) {
        this.props = props;
        shardingCount = getShardingCount(props);
    }

    private int getShardingCount(final Properties props) {
        Preconditions.checkArgument(props.containsKey(SHARDING_COUNT_KEY), "Sharding count cannot be null.");
        return Integer.parseInt(props.getProperty(SHARDING_COUNT_KEY));
    }

    private long hashShardingValue(final Comparable<?> shardingValue) {
        return Math.abs((long) shardingValue.hashCode());
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1147174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

el -table 多层级嵌套

只要你后端可以查到数据这个层级可以无限嵌套 这里用了懒加载&#xff0c;每次点击的时候将当前点击的父级id作为查询条件&#xff0c;向后端发送请求&#xff0c;来获取他子级的数据&#xff0c;并不是将所有数据查出来拼接返回的。 前端代码 <el-table:data"dataLis…

12、SpringCloud -- redis库存和redis预库存保持一致、优化后的压测效果

目录 redis库存和redis预库存保持一致问题的产生需求:代码:测试:优化后的压测效果之前的测试数据优化后的测试数据redis库存和redis预库存保持一致 redis库存是指初始化是从数据库中获取最新的秒杀商品列表数据存到redis中 redis的预库存是指每个秒杀商品每次成功秒杀之后…

【JAVA学习笔记】52 - 本章作业

1.字符反转 注意String是final的不能改变需要toCharArray改成char数组 返回String需要将char改成valueOf改为String public class HomeWork01 {public static void main(String[] args) {String str "0123456789";//改变的是char&#xff0c;和str无关try {System…

项目|金额场景计算BigDecimal使用简记

前言 在实际项目开发中&#xff0c;我们经常会遇到一些金额计算&#xff0c;分摊等问题&#xff0c;通常我们都使用java.math.BigDecimal 来完成各种计算&#xff0c;避免使用浮点数float,double来计算金额&#xff0c;以免丢失精度&#xff0c;以下是博主部分使用场景和使用Bi…

element-plus走马灯不显示

问题描述 依赖正确&#xff0c;代码用法正确&#xff0c;但是element-plu走马灯就是不显示&#xff01;&#xff01; <div class"content"><el-carousel height"150px" width"200px"><el-carousel-item v-for"item in 4&qu…

联想电脑thinkpad x13摄像头打不开,史上最全的针对联想电脑摄像头的解决方案

前言 最近面试&#xff0c;临近面试的前30min&#xff0c;发现摄像头打不开。具体情况如下&#xff1a; 这可没把我吓坏&#xff0c;我可是要露脸的&#xff0c;最后在我的不屑努力下&#xff0c;我选择了手机视频面试&#xff0c;很干。未来的几天都在琢磨这玩意儿了&#…

Docker 部署spring-boot项目(超详细 包括Docker详解、Docker常用指令整理等)

文章目录 DockerDocker的定义Docker有哪些作用Docker有哪些好处使用docker部署springboot项目安装docker创建Dockerfile镜像文件执行镜像文件(Dockerfile文件)查看Docker镜像启动容器查看Docker中运行的容器查看服务容器日志 Docker常用指令查看docker安装目录启动Docker停止Do…

MGRE环境下的OSPF

实验拓扑 需求 1 R6为ISP只能配置IP地址&#xff0c;R1-R5的环回为私有网段 2 R1/4/5为全连的MGRE结构&#xff0c;R1/2/3为星型的拓扑结构&#xff0c;R1为中心站点 3 所有私有网段可以互相通讯&#xff0c;私有网段使用OSPF完成。 IP规划 配置IP R1 # interface GigabitEt…

第三次ACM校队周赛考核题+生活随笔

本周ACM校队周赛考核题 1.简单数学&#xff08;签到题&#xff09; 题目&#xff1a; Joker想要买三张牌&#xff0c;但是三张牌太少了&#xff0c;老板不卖&#xff0c;除非Joker算出老板给出的数学题。 现在老板给出t组数据&#xff0c;每一组数据有三个数a,b,c&#xff0c…

【从0到1设计一个网关】整合Nacos-服务注册与服务订阅的实现

文章目录 Nacos定义服务注册与订阅方法服务信息加载与配置实现将网关注册到注册中心实现服务的订阅 Nacos Nacos提供了许多强大的功能&#xff1a; 比如服务发现、健康检测。 Nacos支持基于DNS和基于RPC的服务发现。 同时Nacos提供对服务的实时的健康检查&#xff0c;阻止向不…

【JavaSE】运算符详解及与C语言中的区别

在文章的最后&#xff0c;总结了Java与C语言的某些不同点 目录 一、什么是运算符 二、算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符/-- 三、关系运算符 四、逻辑运算符&#xff08;重点&#xff09; 1.逻辑与&& 2.逻辑或|| 3.逻辑非 4.补…

SQL Server 安装失败 服务“MSSQLServerOLAPService”启动请求失败 一定有效的解决方案

问题描述 首次安装 SQL Server 2022&#xff0c;在安装结束时出现弹窗“无法启动服务。原因&#xff1a;服务“MSSQLServerOLAPService”启动请求失败”。 点击“确定”按钮后&#xff0c;出现新弹窗。 问题原因 在Windows服务中手动启动“MSSQLServerOLAPService”&#x…

建筑木模板现货供应,广东隧道地铁木模板批发。

我们是一家专业供应建筑木模板的公司&#xff0c;提供广东地区的现货供应服务。我们特别推荐我们的隧道地铁木模板&#xff0c;专为隧道和地铁工程而设计&#xff0c;为工程施工提供优质可靠的支撑材料。我们的隧道地铁木模板采用高品质的木材制造而成&#xff0c;具有卓越的强…

muduo源码剖析之Buffer缓冲区类

简介 Buffer封装了一个可变长的buffer&#xff0c;支持廉价的前插操作&#xff0c;以及内部挪腾操作避免额外申请空间 使用vector作为缓冲区(可自动调整扩容) 设计图 源码剖析 已经编写好注释 buffer.h // Copyright 2010, Shuo Chen. All rights reserved. // http://c…

msvcp140.dll丢失的正确解决方法

在使用电脑中我们经常会遇到一些错误提示&#xff0c;其中之一就是“msvcp140.dll丢失”。这个错误通常会导致某些应用程序无法正常运行。为了解决这个问题&#xff0c;我们需要采取一些措施来修复丢失的msvcp140.dll文件。本文将介绍6个不同的解决方法&#xff0c;帮助读者解决…

java lombok

Lombok是一个实用的Java类库&#xff0c;可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码。 通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法&#xff0c;并可以自动化生成日志变量&#xff0c;简化java开发、提高效率&#…

AI:39-基于深度学习的车牌识别检测

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌本专栏包含以下学习方向: 机器学习、深度学…

不用动脑子的技巧!已知二叉树的前序中序遍历,确定二叉树/求后序遍历

根据前&#xff08;后&#xff09;序、中序&#xff0c;确定二叉树&#xff0c;高妙的方法&#xff01;&#xff01;&#xff01; 二叉树的前中后序遍历⏩巧妙的方法&#xff01;根据前序遍历和中序遍历&#xff0c;确定二叉树例题1例题2 根据后序遍历和中序遍历&#xff0c;确…

CS224W3.2——随机游走(Random Walk)

上一文中说道定义节点相似度函数的时候使用Random Walk方法&#xff1a; CS224W3.1——节点Embedding 这节课来说一下Random Walk方法。在这篇中&#xff0c;我们来看一个更有效的相似函数——在图上随机游走的节点共现的概率。我们介绍随机游走背后的直觉&#xff0c;我们将…

中电文思海辉:塑造全球AI能力,持续强化诸多行业战略

【科技明说 &#xff5c; 重磅专题】 中电文思海辉以前就是叫文思海辉&#xff0c; 这是由之前两家上市软件外包公司文思信息和海辉软件合并而来&#xff0c;2018年当时各自股票以1:1的比例进行整合&#xff0c;双方股东各持有新公司50%的股权&#xff0c;合并后新公司名称为文…