<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官:海量訂單數(shù)據(jù)如何存儲和查詢

          共 23937字,需瀏覽 48分鐘

           ·

          2023-10-31 22:23

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!

          編輯:業(yè)余草

          來源:juejin.cn/post/7294841029681250319

          推薦:https://t.zsxq.com/13WJ1INdw

          自律才能自由

          冷熱數(shù)據(jù)架構(gòu)

          假設(shè)我們考慮 12306 單個假期的人流量為 2 億人次,這一估算基于每年的三個主要假期:五一、國慶和春節(jié)。這些假期通常都有來回的流動,因此數(shù)據(jù)存儲與計算的公式變?yōu)椋? * (3*2) = 12 億,即每年的假期總?cè)舜芜_(dá)到了 12 億。

          考慮到假期訂單數(shù)據(jù)以及日常購票數(shù)據(jù)的累積,隨著多年的積累,數(shù)據(jù)量將會變得相當(dāng)龐大。但我們需要再次審視一個關(guān)鍵問題:這些訂單數(shù)據(jù)是否需要一直保留在數(shù)據(jù)庫中呢?

          經(jīng)過詳細(xì)分析 12306 車票訂單購買查看邏輯,我們發(fā)現(xiàn)用戶賬號只能查看最近一個月內(nèi)的訂單購買記錄。這一時間范圍最多涵蓋一個節(jié)假日周期,考慮往返車票等情況,大致數(shù)據(jù)量約為 4 億。這樣的數(shù)據(jù)規(guī)模相較之前大幅減少,有效降低了整體的存儲壓力。

          上述的這種數(shù)據(jù)存儲技術(shù)叫做「冷熱數(shù)據(jù)」的架構(gòu)方案,那什么叫做冷數(shù)據(jù)?什么又是熱數(shù)據(jù)?

          ?
          • 「熱數(shù)據(jù)」通常指經(jīng)常被訪問和使用的數(shù)據(jù),如最近的交易記錄或最新的新聞文章等。這些數(shù)據(jù)需要快速的讀寫速度和響應(yīng)時間,因此通常存儲在快速存儲介質(zhì)(如內(nèi)存或快速固態(tài)硬盤)中,以便快速訪問和處理。
          • 「冷數(shù)據(jù)」則指很少被訪問和使用的數(shù)據(jù),如過去的交易記錄或舊的新聞文章等。這些數(shù)據(jù)訪問頻率較低,但需要長期保存,因此存儲在較慢的存儲介質(zhì)(如磁盤或云存儲)中,以便節(jié)省成本和存儲空間。
          ?

          如何實現(xiàn)這種冷熱數(shù)據(jù)存儲架構(gòu)?比較簡單的方案就是,「我們每天有個定時任務(wù),把一個月前的數(shù)據(jù)從當(dāng)前的數(shù)據(jù)庫遷移到冷數(shù)據(jù)庫中」。這里就涉及了分庫分表操作。

          這時需要注意一件事情,就是我們遷移到冷數(shù)據(jù)庫不意味著不查詢這些數(shù)據(jù)。如果遇到查詢歷史數(shù)據(jù)的需求,我們還是要能支持,比如支付寶的交易數(shù)據(jù)查詢。

          訂單分片鍵選擇

          每每說到分庫分表,最頭疼的是莫過于如何選擇分片鍵,用戶名?訂單號?還是創(chuàng)建時間?

          先說我們的業(yè)務(wù)基本訴求,訂單分庫分表的基本查詢條件有兩種情況

          • 「用戶要能查看自己的訂單」
          • 「支持訂單號精準(zhǔn)查詢?!?/strong>

          這樣的話,我們就需要按照兩個字段當(dāng)做分片鍵,這也就意味著每次查詢時需要帶著用戶和訂單兩個字段,非常的不方便。能不能通過一個字段分庫分表,但是查詢時兩個字段任意傳一個就能精準(zhǔn)查詢,而不導(dǎo)致讀擴(kuò)散問題?

          基因法

          這就需要用到咱們項目中使用的基因算法。那什么是分庫分表基因算法?

          說的通俗易懂點,就是我們通過把用戶的后六位數(shù)據(jù)冗余到訂單號里。這樣的話,我們就可以按照用戶 ID 后六位進(jìn)行分庫分表,并且將分片鍵定義為用戶 ID 和訂單號,只要查詢中攜帶這兩個字段,我們就取用戶 ID 后六位進(jìn)行查找分片表的位置。

          這樣我們就可以很好支持分庫分表需求了,同時能滿足用戶和訂單號兩種查詢邏輯,這也是大家熱衷于使用基因算法的原因。

          訂單號生成

          為了保證訂單號生成遞增,我們參考雪花算法自定義了一個 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 后六位拼接訂單號后面的技術(shù)方案,是參考了淘寶的訂單號設(shè)計。


          訂單分庫分表代碼實戰(zhàn)

          如果你沒有使用過 ShardingSphere 分庫分表操作,可以查看官網(wǎng)進(jìn)行一些前置條件理解。

          引入 ShardingSphere 依賴

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

          定義分片規(guī)則

          spring:
            datasource:
             # ShardingSphere 對 Driver 自定義,實現(xiàn)分庫分表等隱藏邏輯
              driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
              # ShardingSphere 配置文件路徑
              url: jdbc:shardingsphere:classpath:shardingsphere-config.yaml

          訂單分片配置

          為了避免繁瑣,這里只分 2 個庫以及對應(yīng)業(yè)務(wù) 16 張表。

          shardingsphere-config.yaml

          # 數(shù)據(jù)源集合,也就是咱們剛才說的分兩個庫
          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:
             # 分片規(guī)則
              - !SHARDING
              # 分片表
              tables:
                # 訂單表
                t_order:
                  # 真實的數(shù)據(jù)節(jié)點,也對應(yīng)著在數(shù)據(jù)庫中存儲的真實表
                  actualDataNodes: ds_${0..1}.t_order_${0..15}
                  # 分庫策略
                  databaseStrategy:
                    # 復(fù)合分庫策略(多個分片鍵)
                    complex:
                      # 用戶 ID 和訂單號
                      shardingColumns: user_id,order_sn
                      # 搜索 order_database_complex_mod 下方會有分庫算法
                      shardingAlgorithmName: order_database_complex_mod
                  # 分表策略
                  tableStrategy:
                    # 復(fù)合分表策略(多個分片鍵)
                    complex:
                      # 用戶 ID 和訂單號
                      shardingColumns: user_id,order_sn
                      # 搜索 order_table_complex_mod 下方會有分表算法
                      shardingAlgorithmName: order_table_complex_mod
                # 訂單明細(xì)表,規(guī)則同訂單表
                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:
                  # 通過加載全限定名類實現(xiàn)分片算法,相當(dāng)于分片邏輯都在 algorithmClassName 對應(yīng)的類中
                  type: CLASS_BASED
                  props:
                    algorithmClassName: org.opengoofy.index12306.biz.orderservice.dao.algorithm.OrderCommonDataBaseComplexAlgorithm
                    # 分庫數(shù)量
                    sharding-count: 2
                    # 復(fù)合(多分片鍵)分庫策略
                    strategy: complex
                # 訂單分表算法
                order_table_complex_mod:
                  # 通過加載全限定名類實現(xiàn)分片算法,相當(dāng)于分片邏輯都在 algorithmClassName 對應(yīng)的類中
                  type: CLASS_BASED
                  props:
                    algorithmClassName: org.opengoofy.index12306.biz.orderservice.dao.algorithm.OrderCommonTableComplexAlgorithm
                    # 分表數(shù)量
                    sharding-count: 16
                    # 復(fù)合(多分片鍵)分表策略
                    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

          分片算法解析

          調(diào)試的話可以分為兩種,一種是創(chuàng)建訂單,一種是查看訂單,控制臺都有現(xiàn)成的功能,Debug 到分片算法方法上就可以。

          因為訂單和訂單明細(xì)表都是按照用戶和訂單號進(jìn)行的分片,分片算法規(guī)則一致,所以就進(jìn)行了復(fù)用。

          訂單分庫分片算法代碼如下:

          /**
           * 訂單數(shù)據(jù)庫復(fù)合分片算法配置
           * ComplexKeysShardingAlgorithm 是 ShardingSphere 預(yù)留出來的可擴(kuò)展分片算法接口
           * 注意:不同版本的 ShardingSphere 可能包路徑、類名或者方法名不一致
           */

          public class OrderCommonDataBaseComplexAlgorithm implements ComplexKeysShardingAlgorithm {
           
              @Getter
              private Properties props;
           
              // 分庫數(shù)量,讀取的配置中定義的分庫數(shù)量
              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 對應(yīng)值
                          Comparable<?> comparable = customerUserIdCollection.stream().findFirst().get();
                          // 如果使用 MybatisPlus 因為傳入時沒有強(qiáng)類型判斷,所以有可能用戶 ID 是字符串,也可能是 Long 等數(shù)值
                          // 比如傳入的用戶 ID 可能是 1683025552364568576 也可能是 '1683025552364568576'
                          // 根據(jù)不同的值類型,做出不同的獲取后六位判斷。字符串直接截取后六位,Long 類型直接通過 % 運(yùn)算獲取后六位
                          if (comparable instanceof String) {
                              String actualOrderSn = comparable.toString();
                              // 獲取真實數(shù)據(jù)庫的方法其實還是通過 HASH_MOD 方式取模的,shardingCount 就是咱們配置中的分庫數(shù)量
                              result.add("ds_" + hashShardingValue(actualOrderSn.substring(Math.max(actualOrderSn.length() - 60))) % 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() - 60))) % 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());
              }
          }

          訂單分表算法邏輯基本與訂單分庫算法一致,大家查看代碼也基本上都能清楚,就不再過多贅述。

          /**
           * 訂單表相關(guān)復(fù)合分片算法配置
           */

          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() - 60))) % 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() - 60))) % 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());
              }
          }

          架構(gòu)的設(shè)計沒有終點,架構(gòu)的盡頭是架構(gòu)師,架構(gòu)師的盡頭是找到最佳平衡點!

          瀏覽 734
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕A V在线播放 | 风间精品一区二区三区 | 亚洲国产成人7777 | 婷婷五月在线视频 | 大屁股喷水视频 |