從 0 到 1 攻克訂單表分表分庫:億級流量下的數據庫架構實戰指南

引言:

  • 本文總字數:約 8500 字
  • 建議閱讀時間:35 分鐘

當訂單表撐爆數據庫,我們該怎么辦?

想象一下,你負責的電商平臺在經歷了幾個雙十一后,訂單系統開始頻繁出現問題:數據庫查詢越來越慢,甚至在高峰期出現超時;運維團隊每天都在抱怨數據庫服務器負載過高;營銷部門的數據分析報告總是延遲,因為全表掃描需要數小時。

這不是危言聳聽,而是每個快速發展的業務都會面臨的真實挑戰。根據 MySQL 官方文檔(https://dev.mysql.com/doc/refman/8.0/en/table-size-limit.html),單表數據量超過 1000 萬行時,性能會顯著下降。而對于訂單表這種寫入密集、查詢頻繁的核心業務表,這個閾值可能還要低得多。

分表分庫(Sharding)技術正是解決這個問題的關鍵。它通過將一個大表的數據分散到多個小表、多個數據庫中,從而提升系統的并發能力和查詢效率。本文將從理論到實踐,全方位解析訂單表分表分庫的設計與實現,讓你不僅能理解其底層邏輯,更能直接應用到實際項目中。

一、分表分庫核心概念:你必須理解的基礎知識

1.1 什么是分表分庫?

分表分庫是一種數據庫水平擴展技術,主要包括兩種方式:

  • 分表(Table Sharding):將一個大表按照某種規則拆分成多個小表,這些小表可以在同一個數據庫中,也可以分布在不同的數據庫中。
  • 分庫(Database Sharding):將一個數據庫按照某種規則拆分成多個數據庫,每個數據庫可以部署在不同的服務器上。

用一個形象的比喻:如果把數據庫比作倉庫,表比作貨架,那么分表就像是把一個長貨架拆分成多個短貨架,分庫則是把一個大倉庫分成多個小倉庫。

1.2 垂直拆分 vs 水平拆分

分表分庫可以分為垂直和水平兩種拆分策略:

  • 垂直拆分:按照業務或數據的重要性進行拆分

    • 垂直分庫:將不同業務模塊的數據拆分到不同的數據庫,如訂單庫、用戶庫、商品庫
    • 垂直分表:將一個表中不常用的字段拆分到另一個表,如訂單表中的基本信息和詳細信息分離
  • 水平拆分:按照某種規則將同一業務的數據分散存儲

    • 水平分庫:將同一表的數據拆分到多個數據庫
    • 水平分表:將同一表的數據拆分到同一數據庫的多個表中

對于訂單表,我們通常采用水平拆分,因為訂單數據具有天然的可拆分性,且隨著業務增長,數據量會持續增加。

1.3 分表分庫的優勢與挑戰

優勢

  1. 提升查詢性能:小表的查詢速度遠快于大表
  2. 提高并發能力:分散到多個數據庫,可同時處理更多請求
  3. 便于擴容:可以按需增加數據庫服務器
  4. 提高可用性:單個庫表故障不會導致整個系統不可用

挑戰

  1. 分布式事務:跨庫操作需要特殊處理
  2. 跨庫查詢:JOIN 操作變得復雜
  3. 數據遷移:擴容時需要遷移數據
  4. 全局 ID:需要生成唯一的全局標識符
  5. 運維復雜度:管理多個庫表增加了運維難度

二、訂單表分表分庫設計:從理論到方案

2.1 拆分策略選擇:為什么訂單表適合按時間和用戶 ID 拆分?

訂單表的拆分策略需要結合業務查詢模式。常見的拆分鍵(Sharding Key)選擇有:

  1. 按用戶 ID 拆分:適合需要查詢某個用戶所有訂單的場景
  2. 按訂單創建時間拆分:適合按時間范圍查詢的場景,如月度報表
  3. 按訂單 ID 哈希拆分:數據分布均勻,但時間范圍查詢困難

對于大多數電商平臺,我們推薦復合策略:先按時間范圍(如月份)拆分,再在每個時間范圍內按用戶 ID 哈希拆分。這樣既滿足了按用戶查詢的需求,也方便了按時間歸檔數據。

阿里巴巴《Java 開發手冊(嵩山版)》中明確建議:"分庫分表時,拆分字段的選擇至關重要,需要結合業務查詢場景,盡量避免跨庫跨表查詢。"

2.2 分表分庫粒度:多少數據量一個表合適?

單表數據量的閾值需要根據業務場景和硬件配置來確定,通常有以下參考:

  • 并發查詢較多的表:建議單表數據量控制在 500 萬以內
  • 以插入和簡單查詢為主的表:可以放寬到 1000 萬 - 2000 萬

訂單表作為核心業務表,查詢復雜且頻繁,建議單表數據量控制在 500 萬以內。根據預估的年訂單量,可以計算出需要的表數量:

例如,若預計年訂單量為 1 億,單表 500 萬,則每年需要 20 個表。如果按月份拆分,每月大約需要 2 個表,這意味著每個月內還需要再按用戶 ID 進一步拆分。

2.3 數據庫和表的命名規范

清晰的命名規范有助于維護和排查問題,建議如下:

  • 分庫命名:order_db_${時間標識}_${分片序號}
    • 示例:order_db_202310_00(2023 年 10 月的第 00 號訂單庫)
  • 分表命名:order_tbl_${時間標識}_${分片序號}
    • 示例:order_tbl_202310_01(2023 年 10 月的第 01 號訂單表)

時間標識可以是年份(如 2023)、年份 + 季度(如 2023Q4)或年份 + 月份(如 202310),根據數據量和查詢頻率選擇合適的粒度。

2.4 全局 ID 生成策略

分表分庫后,傳統的自增 ID 無法保證全局唯一,需要全局 ID 生成策略:

  1. UUID/GUID:優點是簡單,缺點是無序、占空間大
  2. 雪花算法(Snowflake):64 位 ID,包含時間戳、機器 ID 等,有序且唯一
  3. 數據庫自增 ID 表:單獨的數據庫表生成 ID,可能成為瓶頸
  4. Redis 自增:利用 INCR 命令,性能好但依賴 Redis

對于訂單 ID,推薦使用雪花算法,因為它生成的 ID 有序,有利于索引性能,且包含時間信息,便于定位數據所在的分片。

三、分表分庫中間件選型:ShardingSphere 實戰

3.1 主流分表分庫中間件對比

目前主流的分表分庫中間件有:

中間件優點缺點適用場景
ShardingSphere功能全面,支持多種數據庫,社區活躍配置復雜大多數企業級應用
MyCat基于 MySQL 協議,透明接入對新特性支持較慢以 MySQL 為主的應用
DRDS阿里云產品,運維簡單商業化,成本高阿里云生態用戶

Apache ShardingSphere(Apache ShardingSphere)是目前最受歡迎的開源分表分庫解決方案,它包含 JDBC、Proxy 和 Sidecar 三個產品,本文將以 ShardingSphere-JDBC 為例進行講解。

3.2 ShardingSphere 核心概念

  • 邏輯表:拆分前的原表,如order_tbl
  • 實際表:拆分后的物理表,如order_tbl_202310_00
  • 數據節點:由數據源和實際表組成,如order_db_202310_00.order_tbl_202310_00
  • 分片鍵:用于拆分的字段,如user_idcreate_time
  • 分片策略:如何將數據分配到不同的分片,包括分片算法和分片規則

3.3 項目環境搭建

3.3.1 Maven 依賴配置

首先,我們需要在pom.xml中添加必要的依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/></parent><groupId>com.jam.order</groupId><artifactId>order-sharding</artifactId><version>1.0.0</version><properties><java.version>17</java.version><shardingsphere.version>5.4.0</shardingsphere.version><mybatis-plus.version>3.5.5</mybatis-plus.version><lombok.version>1.18.30</lombok.version><commons-lang3.version>3.14.0</commons-lang3.version><springdoc.version>2.1.0</springdoc.version></properties><dependencies><!-- Spring Boot 核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- 數據庫 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- ShardingSphere --><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId><version>${shardingsphere.version}</version></dependency><!-- MyBatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><scope>provided</scope></dependency><!-- 工具類 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>${commons-lang3.version}</version></dependency><!-- Swagger3 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>${springdoc.version}</version></dependency><!-- 測試 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
3.3.2 數據庫初始化腳本

我們需要創建分庫分表的數據庫和表結構。這里以按月分庫,每個庫按用戶 ID 哈希分為 4 個表為例:

-- 創建2023年10月的訂單庫
CREATE DATABASE IF NOT EXISTS order_db_202310_00 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS order_db_202310_01 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS order_db_202310_02 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS order_db_202310_03 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 在每個庫中創建4個訂單表
DELIMITER $$
CREATE PROCEDURE create_order_tables(IN db_suffix CHAR(2))
BEGINDECLARE i INT DEFAULT 0;WHILE i < 4 DOSET @sql = CONCAT('CREATE TABLE IF NOT EXISTS order_db_202310_', db_suffix, '.order_tbl_202310_', i, ' (id BIGINT NOT NULL COMMENT \'訂單ID\',user_id BIGINT NOT NULL COMMENT \'用戶ID\',order_no VARCHAR(64) NOT NULL COMMENT \'訂單編號\',total_amount DECIMAL(10,2) NOT NULL COMMENT \'訂單總金額\',pay_amount DECIMAL(10,2) NOT NULL COMMENT \'實付金額\',freight DECIMAL(10,2) NOT NULL COMMENT \'運費\',order_status TINYINT NOT NULL COMMENT \'訂單狀態:0-待付款,1-待發貨,2-已發貨,3-已完成,4-已取消\',payment_time DATETIME COMMENT \'支付時間\',delivery_time DATETIME COMMENT \'發貨時間\',receive_time DATETIME COMMENT \'確認收貨時間\',comment_time DATETIME COMMENT \'評價時間\',create_time DATETIME NOT NULL COMMENT \'創建時間\',update_time DATETIME NOT NULL COMMENT \'更新時間\',PRIMARY KEY (id),KEY idx_user_id (user_id),KEY idx_create_time (create_time)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=\'訂單表\'');PREPARE stmt FROM @sql;EXECUTE stmt;DEALLOCATE PREPARE stmt;SET i = i + 1;END WHILE;
END$$
DELIMITER ;-- 調用存儲過程創建表
CALL create_order_tables('00');
CALL create_order_tables('01');
CALL create_order_tables('02');
CALL create_order_tables('03');-- 創建訂單_item表(略,結構類似)

3.4 ShardingSphere 配置

下面是application.yml的配置,實現按時間(月份)分庫,按用戶 ID 哈希分表:

spring:shardingsphere:datasource:names: db-202310-00, db-202310-01, db-202310-02, db-202310-03db-202310-00:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/order_db_202310_00?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdb-202310-01:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/order_db_202310_01?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdb-202310-02:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/order_db_202310_02?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdb-202310-03:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/order_db_202310_03?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: rootrules:sharding:tables:order_tbl:actual-data-nodes: db-202310-${0..3}.order_tbl_202310-${0..3}database-strategy:standard:sharding-column: create_timesharding-algorithm-name: order-db-inlinetable-strategy:standard:sharding-column: user_idsharding-algorithm-name: order-tbl-inlinekey-generate-strategy:column: idkey-generator-name: snowflakesharding-algorithms:order-db-inline:type: INLINEprops:algorithm-expression: db-202310-${create_time.toString('yyyyMM') % 4}order-tbl-inline:type: INLINEprops:algorithm-expression: order_tbl_202310-${user_id % 4}key-generators:snowflake:type: SNOWFLAKEprops:worker-id: 1data-center-id: 1props:sql-show: truequery-with-cipher-column: truemybatis-plus:mapper-locations: classpath*:mapper/**/*.xmlglobal-config:db-config:id-type: ASSIGN_IDlogic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplspringdoc:api-docs:path: /api-docsswagger-ui:path: /swagger-ui.htmloperationsSorter: methodpackages-to-scan: com.jam.order.controller

四、訂單表分表分庫核心代碼實現

4.1 實體類設計

package com.jam.order.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;import java.math.BigDecimal;
import java.time.LocalDateTime;/*** 訂單實體類** @author 果醬*/
@Data
@TableName("order_tbl")
@Schema(description = "訂單信息")
public class Order {/*** 訂單ID*/@TableId(type = IdType.ASSIGN_ID)@Schema(description = "訂單ID")private Long id;/*** 用戶ID*/@Schema(description = "用戶ID")private Long userId;/*** 訂單編號*/@Schema(description = "訂單編號")private String orderNo;/*** 訂單總金額*/@Schema(description = "訂單總金額")private BigDecimal totalAmount;/*** 實付金額*/@Schema(description = "實付金額")private BigDecimal payAmount;/*** 運費*/@Schema(description = "運費")private BigDecimal freight;/*** 訂單狀態:0-待付款,1-待發貨,2-已發貨,3-已完成,4-已取消*/@Schema(description = "訂單狀態:0-待付款,1-待發貨,2-已發貨,3-已完成,4-已取消")private Integer orderStatus;/*** 支付時間*/@Schema(description = "支付時間")private LocalDateTime paymentTime;/*** 發貨時間*/@Schema(description = "發貨時間")private LocalDateTime deliveryTime;/*** 確認收貨時間*/@Schema(description = "確認收貨時間")private LocalDateTime receiveTime;/*** 評價時間*/@Schema(description = "評價時間")private LocalDateTime commentTime;/*** 創建時間*/@Schema(description = "創建時間")private LocalDateTime createTime;/*** 更新時間*/@Schema(description = "更新時間")private LocalDateTime updateTime;
}

4.2 Mapper 接口

package com.jam.order.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.order.entity.Order;
import org.apache.ibatis.annotations.Param;import java.time.LocalDateTime;
import java.util.List;/*** 訂單Mapper接口** @author 果醬*/
public interface OrderMapper extends BaseMapper<Order> {/*** 根據用戶ID查詢訂單列表** @param userId 用戶ID* @param startTime 開始時間* @param endTime 結束時間* @return 訂單列表*/List<Order> selectByUserIdAndTimeRange(@Param("userId") Long userId,@Param("startTime") LocalDateTime startTime,@Param("endTime") LocalDateTime endTime);/*** 根據訂單狀態查詢訂單數量** @param orderStatus 訂單狀態* @param startTime 開始時間* @param endTime 結束時間* @return 訂單數量*/Long countByStatusAndTimeRange(@Param("orderStatus") Integer orderStatus,@Param("startTime") LocalDateTime startTime,@Param("endTime") LocalDateTime endTime);
}

4.3 Service 層實現

package com.jam.order.service;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.order.entity.Order;
import com.jam.order.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;/*** 訂單服務實現類** @author 果醬*/
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {/*** 創建訂單** @param order 訂單信息* @return 訂單ID*/@Override@Transactional(rollbackFor = Exception.class)public Long createOrder(Order order) {// 參數校驗Objects.requireNonNull(order, "訂單信息不能為空");Objects.requireNonNull(order.getUserId(), "用戶ID不能為空");StringUtils.hasText(order.getOrderNo(), "訂單編號不能為空");Objects.requireNonNull(order.getTotalAmount(), "訂單總金額不能為空");// 設置默認值LocalDateTime now = LocalDateTime.now();order.setCreateTime(now);order.setUpdateTime(now);order.setOrderStatus(0); // 默認為待付款狀態// 保存訂單boolean saveResult = save(order);if (!saveResult) {log.error("創建訂單失敗,訂單信息:{}", order);throw new RuntimeException("創建訂單失敗");}log.info("創建訂單成功,訂單ID:{},訂單編號:{}", order.getId(), order.getOrderNo());return order.getId();}/*** 根據用戶ID查詢訂單列表** @param userId 用戶ID* @param startTime 開始時間* @param endTime 結束時間* @return 訂單列表*/@Overridepublic List<Order> getOrdersByUserId(Long userId, LocalDateTime startTime, LocalDateTime endTime) {Objects.requireNonNull(userId, "用戶ID不能為空");Objects.requireNonNull(startTime, "開始時間不能為空");Objects.requireNonNull(endTime, "結束時間不能為空");log.info("查詢用戶訂單,用戶ID:{},時間范圍:{}至{}", userId, startTime, endTime);return baseMapper.selectByUserIdAndTimeRange(userId, startTime, endTime);}/*** 更新訂單狀態** @param orderId 訂單ID* @param orderStatus 訂單狀態* @return 是否更新成功*/@Override@Transactional(rollbackFor = Exception.class)public boolean updateOrderStatus(Long orderId, Integer orderStatus) {Objects.requireNonNull(orderId, "訂單ID不能為空");Objects.requireNonNull(orderStatus, "訂單狀態不能為空");Order order = new Order();order.setId(orderId);order.setOrderStatus(orderStatus);order.setUpdateTime(LocalDateTime.now());// 根據狀態更新對應的時間switch (orderStatus) {case 1: // 待發貨,說明已支付order.setPaymentTime(LocalDateTime.now());break;case 2: // 已發貨order.setDeliveryTime(LocalDateTime.now());break;case 3: // 已完成order.setReceiveTime(LocalDateTime.now());break;case 4: // 已取消break;default:log.error("不支持的訂單狀態:{}", orderStatus);throw new IllegalArgumentException("不支持的訂單狀態");}boolean updateResult = updateById(order);log.info("更新訂單狀態,訂單ID:{},新狀態:{},結果:{}", orderId, orderStatus, updateResult);return updateResult;}/*** 統計指定狀態的訂單數量** @param orderStatus 訂單狀態* @param startTime 開始時間* @param endTime 結束時間* @return 訂單數量*/@Overridepublic Long countOrdersByStatus(Integer orderStatus, LocalDateTime startTime, LocalDateTime endTime) {Objects.requireNonNull(orderStatus, "訂單狀態不能為空");Objects.requireNonNull(startTime, "開始時間不能為空");Objects.requireNonNull(endTime, "結束時間不能為空");log.info("統計訂單數量,狀態:{},時間范圍:{}至{}", orderStatus, startTime, endTime);return baseMapper.countByStatusAndTimeRange(orderStatus, startTime, endTime);}
}

4.4 Controller 層實現

package com.jam.order.controller;import com.jam.order.entity.Order;
import com.jam.order.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;/*** 訂單控制器** @author 果醬*/
@Slf4j
@RestController
@RequestMapping("/api/v1/orders")
@Tag(name = "訂單管理", description = "訂單相關的CRUD操作")
public class OrderController {private final OrderService orderService;public OrderController(OrderService orderService) {this.orderService = orderService;}/*** 創建訂單** @param order 訂單信息* @return 訂單ID*/@PostMapping@Operation(summary = "創建訂單", description = "創建新的訂單")public ResponseEntity<Long> createOrder(@RequestBody Order order) {Long orderId = orderService.createOrder(order);return ResponseEntity.ok(orderId);}/*** 查詢訂單詳情** @param orderId 訂單ID* @return 訂單詳情*/@GetMapping("/{orderId}")@Operation(summary = "查詢訂單詳情", description = "根據訂單ID查詢訂單詳情")public ResponseEntity<Order> getOrderDetail(@Parameter(description = "訂單ID", required = true)@PathVariable Long orderId) {Order order = orderService.getById(orderId);return ResponseEntity.ok(order);}/*** 根據用戶ID查詢訂單列表** @param userId 用戶ID* @param startTime 開始時間* @param endTime 結束時間* @return 訂單列表*/@GetMapping("/user/{userId}")@Operation(summary = "查詢用戶訂單", description = "根據用戶ID和時間范圍查詢訂單列表")public ResponseEntity<List<Order>> getOrdersByUserId(@Parameter(description = "用戶ID", required = true)@PathVariable Long userId,@Parameter(description = "開始時間", required = true)@RequestParam LocalDateTime startTime,@Parameter(description = "結束時間", required = true)@RequestParam LocalDateTime endTime) {List<Order> orders = orderService.getOrdersByUserId(userId, startTime, endTime);return ResponseEntity.ok(orders);}/*** 更新訂單狀態** @param orderId 訂單ID* @param orderStatus 訂單狀態* @return 是否更新成功*/@PutMapping("/{orderId}/status")@Operation(summary = "更新訂單狀態", description = "根據訂單ID更新訂單狀態")public ResponseEntity<Boolean> updateOrderStatus(@Parameter(description = "訂單ID", required = true)@PathVariable Long orderId,@Parameter(description = "訂單狀態:0-待付款,1-待發貨,2-已發貨,3-已完成,4-已取消", required = true)@RequestParam Integer orderStatus) {boolean result = orderService.updateOrderStatus(orderId, orderStatus);return ResponseEntity.ok(result);}/*** 統計指定狀態的訂單數量** @param orderStatus 訂單狀態* @param startTime 開始時間* @param endTime 結束時間* @return 訂單數量*/@GetMapping("/count")@Operation(summary = "統計訂單數量", description = "統計指定狀態和時間范圍內的訂單數量")public ResponseEntity<Long> countOrdersByStatus(@Parameter(description = "訂單狀態:0-待付款,1-待發貨,2-已發貨,3-已完成,4-已取消", required = true)@RequestParam Integer orderStatus,@Parameter(description = "開始時間", required = true)@RequestParam LocalDateTime startTime,@Parameter(description = "結束時間", required = true)@RequestParam LocalDateTime endTime) {Long count = orderService.countOrdersByStatus(orderStatus, startTime, endTime);return ResponseEntity.ok(count);}
}

4.5 自定義分片算法

上面的配置使用了 ShardingSphere 的內置 INLINE 算法,對于更復雜的場景,我們可以實現自定義分片算法:

package com.jam.order.sharding;import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;/*** 訂單表數據庫分片算法(按時間)** @author 果醬*/
public class OrderDatabaseShardingAlgorithm implements StandardShardingAlgorithm<LocalDateTime> {private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMM");private static final int DB_COUNT = 4;@Overridepublic String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<LocalDateTime> shardingValue) {LocalDateTime createTime = shardingValue.getValue();String month = createTime.format(FORMATTER);int dbIndex = Integer.parseInt(month) % DB_COUNT;String targetName = "db-" + month + "-" + String.format("%02d", dbIndex);if (availableTargetNames.contains(targetName)) {return targetName;}throw new IllegalArgumentException("未找到匹配的數據庫:" + targetName);}@Overridepublic Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<LocalDateTime> shardingValue) {// 處理范圍查詢,如between andSet<String> result = new HashSet<>();// 獲取時間范圍LocalDateTime lower = shardingValue.getValueRange().lowerEndpoint();LocalDateTime upper = shardingValue.getValueRange().upperEndpoint();// 遍歷時間范圍內的所有月份LocalDateTime current = lower;while (!current.isAfter(upper)) {String month = current.format(FORMATTER);for (int i = 0; i < DB_COUNT; i++) {String targetName = "db-" + month + "-" + String.format("%02d", i);if (availableTargetNames.contains(targetName)) {result.add(targetName);}}// 月份加1current = current.plusMonths(1);}return result;}@Overridepublic void init(Properties props) {// 初始化配置}@Overridepublic String getType() {return "ORDER_DATABASE_SHARDING";}
}

對應的表分片算法:

package com.jam.order.sharding;import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;/*** 訂單表分片算法(按用戶ID)** @author 果醬*/
public class OrderTableShardingAlgorithm implements StandardShardingAlgorithm<Long> {private static final int TABLE_COUNT = 4;@Overridepublic String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {Long userId = shardingValue.getValue();// 獲取邏輯表名,如order_tblString logicTableName = shardingValue.getLogicTableName();// 從數據源名中提取月份信息,如db-202310-00 -> 202310String month = extractMonthFromDataSourceName(shardingValue.getDataSourceName());int tableIndex = Math.toIntExact(userId % TABLE_COUNT);String targetTableName = logicTableName + "_" + month + "_" + tableIndex;if (availableTargetNames.contains(targetTableName)) {return targetTableName;}throw new IllegalArgumentException("未找到匹配的表:" + targetTableName);}@Overridepublic Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {// 處理范圍查詢Set<String> result = new HashSet<>();// 從數據源名中提取月份信息String month = extractMonthFromDataSourceName(shardingValue.getDataSourceName());// 對于范圍查詢,可能需要查詢所有表for (int i = 0; i < TABLE_COUNT; i++) {String targetTableName = shardingValue.getLogicTableName() + "_" + month + "_" + i;if (availableTargetNames.contains(targetTableName)) {result.add(targetTableName);}}return result;}/*** 從數據源名中提取月份信息** @param dataSourceName 數據源名,如db-202310-00* @return 月份,如202310*/private String extractMonthFromDataSourceName(String dataSourceName) {// 數據源名格式:db-202310-00String[] parts = dataSourceName.split("-");if (parts.length < 3) {throw new IllegalArgumentException("無效的數據源名:" + dataSourceName);}return parts[1];}@Overridepublic void init(Properties props) {// 初始化配置}@Overridepublic String getType() {return "ORDER_TABLE_SHARDING";}
}

然后在配置文件中使用自定義算法:

spring:shardingsphere:rules:sharding:tables:order_tbl:actual-data-nodes: db-202310-${0..3}.order_tbl_202310-${0..3}database-strategy:standard:sharding-column: create_timesharding-algorithm-name: order-db-customtable-strategy:standard:sharding-column: user_idsharding-algorithm-name: order-tbl-customkey-generate-strategy:column: idkey-generator-name: snowflakesharding-algorithms:order-db-custom:type: ORDER_DATABASE_SHARDINGprops:# 自定義屬性order-tbl-custom:type: ORDER_TABLE_SHARDINGprops:# 自定義屬性

五、分表分庫高級問題解決方案

5.1 分布式事務處理

分表分庫后,跨庫操作會導致事務問題。目前主流的分布式事務解決方案有:

  1. 2PC(兩階段提交):強一致性,但性能較差
  2. TCC(Try-Confirm-Cancel):業務侵入性強,性能好
  3. SAGA 模式:長事務支持好,實現復雜
  4. 本地消息表:可靠性高,實現簡單
  5. 事務消息:基于消息隊列,如 RocketMQ 的事務消息

對于訂單系統,推薦使用本地消息表事務消息,因為它們既能保證最終一致性,又不會對性能造成太大影響。

下面是一個基于本地消息表的分布式事務示例:

package com.jam.order.service;import com.jam.order.entity.Order;
import com.jam.order.entity.OrderMessage;
import com.jam.order.enums.MessageStatus;
import com.jam.order.mapper.OrderMapper;
import com.jam.order.mapper.OrderMessageMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;
import java.util.UUID;/*** 基于本地消息表的分布式事務示例** @author 果醬*/
@Slf4j
@Service
public class OrderTransactionService {private final OrderMapper orderMapper;private final OrderMessageMapper orderMessageMapper;private final RabbitTemplate rabbitTemplate;public OrderTransactionService(OrderMapper orderMapper, OrderMessageMapper orderMessageMapper,RabbitTemplate rabbitTemplate) {this.orderMapper = orderMapper;this.orderMessageMapper = orderMessageMapper;this.rabbitTemplate = rabbitTemplate;}/*** 創建訂單并發送消息* 本地事務:創建訂單 + 記錄消息表*/@Transactional(rollbackFor = Exception.class)public Long createOrderWithMessage(Order order) {// 1. 創建訂單orderMapper.insert(order);// 2. 記錄消息到本地消息表OrderMessage message = new OrderMessage();message.setId(UUID.randomUUID().toString());message.setOrderId(order.getId());message.setContent("訂單創建:" + order.getOrderNo());message.setStatus(MessageStatus.PENDING);message.setCreateTime(LocalDateTime.now());message.setUpdateTime(LocalDateTime.now());orderMessageMapper.insert(message);// 3. 發送消息到MQ(非事務操作,可能失敗)try {rabbitTemplate.convertAndSend("order.exchange", "order.created", message);// 發送成功,更新消息狀態message.setStatus(MessageStatus.SENT);message.setUpdateTime(LocalDateTime.now());orderMessageMapper.updateById(message);} catch (Exception e) {log.error("發送消息失敗", e);// 發送失敗,消息狀態還是PENDING,由定時任務重試}return order.getId();}/*** 定時任務重試發送失敗的消息*/@Transactional(rollbackFor = Exception.class)public void retryFailedMessages() {// 查詢狀態為PENDING且創建時間超過5分鐘的消息LocalDateTime fiveMinutesAgo = LocalDateTime.now().minusMinutes(5);List<OrderMessage> messages = orderMessageMapper.selectByStatusAndCreateTimeBefore(MessageStatus.PENDING, fiveMinutesAgo);if (CollectionUtils.isEmpty(messages)) {return;}for (OrderMessage message : messages) {// 限制重試次數,避免無限重試if (message.getRetryCount() >= 3) {message.setStatus(MessageStatus.FAILED);message.setUpdateTime(LocalDateTime.now());orderMessageMapper.updateById(message);continue;}try {rabbitTemplate.convertAndSend("order.exchange", "order.created", message);// 發送成功,更新消息狀態message.setStatus(MessageStatus.SENT);message.setRetryCount(message.getRetryCount() + 1);message.setUpdateTime(LocalDateTime.now());orderMessageMapper.updateById(message);} catch (Exception e) {log.error("重試發送消息失敗,消息ID:{}", message.getId(), e);// 更新重試次數message.setRetryCount(message.getRetryCount() + 1);message.setUpdateTime(LocalDateTime.now());orderMessageMapper.updateById(message);}}}
}

5.2 跨庫查詢解決方案

分表分庫后,跨庫查詢變得復雜,常見的解決方案有:

  1. 應用層聚合:在應用層查詢多個分片,然后聚合結果
  2. 視圖聚合:在數據庫層創建視圖,聚合多個分片的數據
  3. 中間件支持:使用 ShardingSphere 等中間件自動處理跨庫查詢
  4. 讀寫分離 + 只讀庫:將數據同步到只讀庫,在只讀庫上進行跨庫查詢

對于訂單查詢,推薦使用中間件支持+讀寫分離的方案:

package com.jam.order.service;import com.jam.order.entity.Order;
import com.jam.order.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;/*** 跨庫查詢示例** @author 果醬*/
@Slf4j
@Service
public class CrossDbQueryService {private final OrderMapper orderMapper;private final ReadOnlyOrderMapper readOnlyOrderMapper;public CrossDbQueryService(OrderMapper orderMapper, ReadOnlyOrderMapper readOnlyOrderMapper) {this.orderMapper = orderMapper;this.readOnlyOrderMapper = readOnlyOrderMapper;}/*** 查詢指定時間段內的所有訂單(跨庫查詢)* 使用只讀庫進行查詢,避免影響主庫性能*/public List<Order> queryOrdersByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {Objects.requireNonNull(startTime, "開始時間不能為空");Objects.requireNonNull(endTime, "結束時間不能為空");log.info("跨庫查詢訂單,時間范圍:{}至{}", startTime, endTime);// 使用只讀庫進行跨庫查詢return readOnlyOrderMapper.selectByTimeRange(startTime, endTime);}/*** 應用層聚合示例(如果中間件不支持跨庫查詢)*/public List<Order> queryOrdersByTimeRangeWithAppAggregation(LocalDateTime startTime, LocalDateTime endTime) {Objects.requireNonNull(startTime, "開始時間不能為空");Objects.requireNonNull(endTime, "結束時間不能為空");log.info("應用層聚合查詢訂單,時間范圍:{}至{}", startTime, endTime);List<Order> result = new ArrayList<>();// 遍歷所有可能的分片,查詢并聚合結果// 實際應用中需要根據分片規則計算需要查詢的分片for (int dbIndex = 0; dbIndex < 4; dbIndex++) {for (int tableIndex = 0; tableIndex < 4; tableIndex++) {List<Order> orders = orderMapper.selectByTimeRangeAndShard(startTime, endTime, dbIndex, tableIndex);result.addAll(orders);}}return result;}
}

5.3 數據遷移與擴容

隨著業務增長,原有的分表分庫策略可能需要調整,這時候就需要進行數據遷移和擴容。

數據遷移的步驟:

  1. 準備新的分片:創建新的數據庫和表
  2. 雙寫數據:同時向舊分片和新分片寫入數據
  3. 遷移歷史數據:將舊分片的歷史數據遷移到新分片
  4. 切換路由:將查詢路由到新分片
  5. 驗證數據:確認新分片數據正確
  6. 下線舊分片:移除舊的數據庫和表

下面是一個數據遷移工具類的示例:

package com.jam.order.util;import com.jam.order.entity.Order;
import com.jam.order.mapper.OrderMapper;
import com.jam.order.mapper.OrderNewMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** 訂單數據遷移工具** @author 果醬*/
@Slf4j
@Component
public class OrderDataMigrationTool {private final OrderMapper orderMapper;private final OrderNewMapper orderNewMapper;// 線程池,用于并行遷移數據private final ExecutorService executorService = Executors.newFixedThreadPool(4);public OrderDataMigrationTool(OrderMapper orderMapper, OrderNewMapper orderNewMapper) {this.orderMapper = orderMapper;this.orderNewMapper = orderNewMapper;}/*** 遷移指定時間范圍內的訂單數據** @param startTime 開始時間* @param endTime 結束時間* @param batchSize 批次大小*/public void migrateOrders(LocalDateTime startTime, LocalDateTime endTime, int batchSize) {log.info("開始遷移訂單數據,時間范圍:{}至{},批次大小:{}", startTime, endTime, batchSize);long totalCount = orderMapper.countByCreateTimeRange(startTime, endTime);log.info("需要遷移的訂單總數:{}", totalCount);long totalPages = (totalCount + batchSize - 1) / batchSize;log.info("總批次數:{}", totalPages);for (long page = 0; page < totalPages; page++) {long currentPage = page;executorService.submit(() -> {migrateOrderBatch(startTime, endTime, currentPage, batchSize);});}// 等待所有任務完成executorService.shutdown();try {boolean finished = executorService.awaitTermination(24, TimeUnit.HOURS);if (finished) {log.info("所有訂單數據遷移完成");} else {log.error("訂單數據遷移超時未完成");}} catch (InterruptedException e) {log.error("訂單數據遷移被中斷", e);Thread.currentThread().interrupt();}}/*** 遷移單批次訂單數據*/@Transactional(rollbackFor = Exception.class)public void migrateOrderBatch(LocalDateTime startTime, LocalDateTime endTime, long page, int batchSize) {try {log.info("開始遷移批次:{},時間范圍:{}至{}", page, startTime, endTime);// 查詢舊表數據List<Order> orders = orderMapper.selectByCreateTimeRangeWithPage(startTime, endTime, page * batchSize, batchSize);if (CollectionUtils.isEmpty(orders)) {log.info("批次:{} 沒有數據需要遷移", page);return;}// 遷移到新表int insertCount = orderNewMapper.batchInsert(orders);log.info("批次:{} 遷移完成,遷移數量:{}", page, insertCount);} catch (Exception e) {log.error("批次:{} 遷移失敗", page, e);// 可以記錄失敗的批次,以便重試}}/*** 驗證遷移后的數據是否正確*/public void verifyMigration(LocalDateTime startTime, LocalDateTime endTime) {log.info("開始驗證遷移結果,時間范圍:{}至{}", startTime, endTime);// 統計舊表數據量long oldCount = orderMapper.countByCreateTimeRange(startTime, endTime);// 統計新表數據量long newCount = orderNewMapper.countByCreateTimeRange(startTime, endTime);if (oldCount != newCount) {log.error("數據量不一致,舊表:{},新表:{}", oldCount, newCount);return;}log.info("數據量驗證通過,舊表和新表數據量均為:{}", oldCount);// 隨機抽查部分數據int sampleSize = 100;List<Order> oldSamples = orderMapper.selectRandomSamples(startTime, endTime, sampleSize);for (Order oldOrder : oldSamples) {Order newOrder = orderNewMapper.selectById(oldOrder.getId());if (newOrder == null) {log.error("數據缺失,訂單ID:{}", oldOrder.getId());continue;}// 比較訂單關鍵字段if (!oldOrder.getOrderNo().equals(newOrder.getOrderNo()) ||!oldOrder.getUserId().equals(newOrder.getUserId()) ||!oldOrder.getTotalAmount().equals(newOrder.getTotalAmount())) {log.error("數據不一致,訂單ID:{},舊數據:{},新數據:{}", oldOrder.getId(), oldOrder, newOrder);}}log.info("數據驗證完成");}
}

六、分表分庫監控與運維

6.1 監控指標設計

為了確保分表分庫系統的穩定運行,需要監控以下關鍵指標:

  1. 數據庫指標

    • 各分片的 CPU、內存、磁盤使用率
    • 連接數、慢查詢數量
    • 讀寫吞吐量、響應時間
  2. 應用指標

    • 分表分庫相關操作的成功率、響應時間
    • 跨庫查詢的比例和性能
    • 分布式事務的成功率
  3. 數據均衡性指標

    • 各分片的數據量差異
    • 各分片的 QPS 差異

下面是一個簡單的監控數據收集工具類:

package com.jam.order.monitor;import com.jam.order.entity.ShardingMetrics;
import com.jam.order.mapper.ShardingMetricsMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;/*** 分表分庫監控數據收集器** @author 果醬*/
@Slf4j
@Component
public class ShardingMonitor {private final ShardingMetricsMapper metricsMapper;private final DatabaseMonitorClient databaseMonitorClient;public ShardingMonitor(ShardingMetricsMapper metricsMapper, DatabaseMonitorClient databaseMonitorClient) {this.metricsMapper = metricsMapper;this.databaseMonitorClient = databaseMonitorClient;}/*** 每5分鐘收集一次監控數據*/@Scheduled(fixedRate = 300000)public void collectMetrics() {log.info("開始收集分表分庫監控數據");try {// 獲取所有分片信息List<String> shardNames = databaseMonitorClient.getAllShardNames();for (String shardName : shardNames) {// 收集數據庫指標Map<String, Object> dbMetrics = databaseMonitorClient.getDatabaseMetrics(shardName);// 收集表指標List<String> tableNames = databaseMonitorClient.getTablesInShard(shardName);for (String tableName : tableNames) {Map<String, Object> tableMetrics = databaseMonitorClient.getTableMetrics(shardName, tableName);// 保存監控數據ShardingMetrics metrics = new ShardingMetrics();metrics.setShardName(shardName);metrics.setTableName(tableName);metrics.setCollectTime(LocalDateTime.now());metrics.setRowCount(((Number) tableMetrics.get("rowCount")).longValue());metrics.setReadQps(((Number) tableMetrics.get("readQps")).doubleValue());metrics.setWriteQps(((Number) tableMetrics.get("writeQps")).doubleValue());metrics.setAvgQueryTime(((Number) tableMetrics.get("avgQueryTime")).doubleValue());metrics.setCpuUsage(((Number) dbMetrics.get("cpuUsage")).doubleValue());metrics.setMemoryUsage(((Number) dbMetrics.get("memoryUsage")).doubleValue());metrics.setConnectionCount(((Number) dbMetrics.get("connectionCount")).intValue());metricsMapper.insert(metrics);}}log.info("分表分庫監控數據收集完成");} catch (Exception e) {log.error("收集分表分庫監控數據失敗", e);}}/*** 檢測數據均衡性*/@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1點執行public void checkDataBalance() {log.info("開始檢測數據均衡性");try {// 獲取各分片的數據量List<Map<String, Object>> shardDataCount = metricsMapper.selectShardDataCount();if (CollectionUtils.isEmpty(shardDataCount)) {log.info("沒有分片數據,無需檢測均衡性");return;}// 計算平均值和標準差long total = 0;for (Map<String, Object> data : shardDataCount) {total += ((Number) data.get("totalCount")).longValue();}double avg = total / (double) shardDataCount.size();double variance = 0;for (Map<String, Object> data : shardDataCount) {long count = ((Number) data.get("totalCount")).longValue();variance += Math.pow(count - avg, 2);}variance /= shardDataCount.size();double stdDev = Math.sqrt(variance);// 計算變異系數(標準差/平均值)double cv = stdDev / avg;log.info("數據均衡性檢測結果:平均值={}, 標準差={}, 變異系數={}", avg, stdDev, cv);// 如果變異系數大于0.2,說明數據分布不均勻if (cv > 0.2) {log.warn("數據分布不均勻,變異系數:{},建議進行數據重平衡", cv);// 可以發送告警通知} else {log.info("數據分布均勻,變異系數:{}", cv);}} catch (Exception e) {log.error("檢測數據均衡性失敗", e);}}
}

6.2 常見問題排查

分表分庫系統可能遇到的常見問題及排查方法:

  1. 數據不一致

    • 檢查分片鍵是否正確
    • 檢查分布式事務實現是否正確
    • 對比新舊數據,找出差異點
  2. 查詢性能差

    • 檢查是否使用了分片鍵查詢
    • 檢查是否有大量跨庫查詢
    • 檢查索引是否合理
  3. 數據傾斜

    • 分析分片鍵的分布情況
    • 調整分片算法
    • 進行數據重平衡
  4. 擴容困難

    • 檢查數據遷移工具是否正常工作
    • 優化遷移策略,減少停機時間
    • 考慮使用自動化遷移工具

七、參考

  1. 單表數據量閾值:參考 MySQL 官方文檔(https://dev.mysql.com/doc/refman/8.0/en/table-size-limit.html)
  2. 分庫分表命名規范:參考阿里巴巴《Java 開發手冊(嵩山版)》
  3. 分片鍵選擇原則:參考 Apache ShardingSphere 官方文檔(https://shardingsphere.apache.org/documentation/5.4.0/en/concepts/sharding/)
  4. 分布式事務解決方案:參考《Designing Data-Intensive Applications》(Martin Kleppmann 著)
  5. 數據遷移策略:參考 AWS 數據庫遷移最佳實踐(https://aws.amazon.com/cn/dms/best-practices/)
  6. 監控指標設計:參考 Prometheus 官方文檔(Metric types | Prometheus)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/95999.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/95999.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/95999.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

網絡編程(5)Modbus

【1】Modbus 1. 起源Modbus由Modicon公司于1979年開發&#xff0c;是全球第一個真正用于工業現場的總線協議在中國&#xff0c;Modbus 已經成為國家標準&#xff0c;并有專業的規范文檔&#xff0c;感興趣的可以去查閱相關的文件&#xff0c;詳情如下&#xff1a;標準編號為:GB…

WordPress性能優化全攻略:從插件實戰到系統級優化

一、性能診斷&#xff1a;定位瓶頸是優化第一步 在對 WordPress 進行性能優化前&#xff0c;精準定位性能瓶頸至關重要。這就好比醫生看病&#xff0c;只有先準確診斷&#xff0c;才能對癥下藥。下面將從核心性能指標檢測工具和服務器基礎性能排查兩個方面展開。 1.1 核心性能…

十、網絡與信息安全基礎知識

1 網絡概述 1.1 計算機網絡的概念 1.1.1 計算機網絡的發展 計算機網絡的發展經歷了四個主要階段&#xff1a; 具有通信功能的單機系統&#xff1a; 早期形式&#xff1a;一臺計算機連接多個終端。例子&#xff1a;20 世紀 50 年代的 SAGE 系統。 具有通信功能的多機系統&#x…

校園管理系統|基于SpringBoot和Vue的校園管理系統(源碼+數據庫+文檔)

項目介紹 : SpringbootMavenMybatis PlusVue Element UIMysql 開發的前后端分離的校園管理系統&#xff0c;項目分為管理端和用戶端和院校管理員端 項目演示: 基于SpringBoot和Vue的校園管理系統 運行環境: 最好是java jdk 1.8&#xff0c;我們在這個平臺上運行的。其他版本理…

新后端漏洞(上)- Weblogic SSRF漏洞

漏洞介紹&#xff1a;Weblogic中存在一個SSRF漏洞&#xff0c;利用該漏洞可以發送任意HTTP請求&#xff0c;進而攻擊內網中redis、fastcgi等脆弱組件。編譯及啟動測試環境docker-compose up -d訪問http://127.0.0.1:7001/uddiexplorer/&#xff0c;無需登錄即可查看uddiexplore…

Fiddler 實戰案例解析,開發者如何用抓包工具快速解決問題

在現代軟件開發中&#xff0c;網絡通信問題幾乎是最常見的 Bug 來源。無論是前端調用后端 API、移動端與服務端交互&#xff0c;還是第三方 SDK 請求&#xff0c;都會因為參數錯誤、環境差異、網絡條件不穩定而出現各種難以復現的問題。 在這些場景下&#xff0c;日志往往并不…

【佳易王藥品進銷存軟件實測】:操作簡單 + 全流程管理,醫藥臺賬管理好幫手#軟件教程全解析

前言&#xff1a; &#xff08;一&#xff09;試用版獲取方式 資源下載路徑&#xff1a;進入博主頭像主頁第一篇文章末尾&#xff0c;點擊卡片按鈕&#xff1b;或訪問左上角博客主頁&#xff0c;通過右側按鈕獲取詳細資料。 說明&#xff1a;下載文件為壓縮包&#xff0c;使用…

【設計模式】UML 基礎教程總結(軟件設計師考試重點)

【設計模式】UML 基礎教程總結(軟件設計師考試重點) 統一建模語言(Unified Modeling Language,UML),是一種標準化的面向對象建模語言,用于可視化、規范化和文檔化軟件系統設計。 參考資料:UML基礎教程資料(可用于軟件設計師考試)! (關注不迷路哈!!!) 文章目錄 【…

vite_react 插件 find_code 最終版本

vite_react 插件 find_code 最終版本當初在開發一個大型項目的時候&#xff0c;第一次接觸 vite 構建&#xff0c;由于系統功能很龐大&#xff0c;在問題排查上和模塊開發上比較耗時&#xff0c;然后就開始找解決方案&#xff0c;find-code 插件方案就這樣實現出來了&#xff0…

Python+DRVT 從外部調用 Revit:批量創建梁(2)

接著昨天的示例&#xff0c;繼續創建梁&#xff0c;這次展示以橢圓弧、Nurbs為軸線。 創建以橢圓弧為軸線的梁 橢圓弧曲線的創建&#xff1a; # 創建橢圓弧 def CreateEllipse(ctx : MyContext, z: float) -> DB.Curve:"""create a horizontal partial el…

Flutter × 鴻蒙系統:一文搞懂如何將你的 App 移植到 HarmonyOS!

摘要 Flutter 是一個高效的跨平臺框架&#xff0c;開發者可以使用同一套代碼快速部署到 Android、iOS 等主流平臺。隨著華為鴻蒙系統&#xff08;HarmonyOS&#xff09;的崛起&#xff0c;越來越多開發者希望能將已有的 Flutter 應用遷移到鴻蒙生態中運行。目前&#xff0c;通過…

QML Charts組件之主題與動畫

目錄前言相關系列ChartView 概述&#xff1a;主題與動畫示例一&#xff1a;主題設置&#xff08;ChartTheme.qml&#xff09;圖表與主題設置主題切換部分示例二&#xff1a;動畫設置&#xff08;ChartAnimation.qml&#xff09;圖表與動畫屬性部分分類軸與柱狀圖數據部分交互與…

【論文閱讀】Security of Language Models for Code: A Systematic Literature Review

Security of Language Models for Code: A Systematic Literature Review 該論文于2025年被CCF A類期刊TOSEM收錄&#xff0c;作者來自南京大學和南洋理工大學。 概述 代碼語言模型&#xff08;CodeLMs&#xff09;已成為代碼相關任務的強大工具&#xff0c;其性能優于傳統方法…

[光學原理與應用-422]:非線性光學 - 計算機中的線性與非線性運算

在計算機科學中&#xff0c;線性運算和非線性運算是兩類核心的數學操作&#xff0c;它們在算法設計、數據處理、機器學習等領域有廣泛應用。兩者的核心區別在于是否滿足疊加原理&#xff08;即輸入信號的線性組合的輸出是否等于輸出信號的線性組合&#xff09;。以下是詳細解釋…

Day21_【機器學習—決策樹(3)—剪枝】

決策樹剪枝是一種防止決策樹過擬合的一種正則化方法&#xff1b;提高其泛化能力。決策樹在訓練過程中如果生長過深、過于復雜&#xff0c;會過度擬合訓練數據中的噪聲和異常值&#xff0c;導致在新數據上表現不佳。剪枝通過簡化樹結構&#xff0c;去除不必要的分支&#xff0c;…

從零構建企業級LLMOps平臺:LMForge——支持多模型、可視化編排、知識庫與安全審核的全棧解決方案

&#x1f680; 從零構建企業級LLMOps平臺&#xff1a;LMForge——支持多模型、可視化編排、知識庫與安全審核的全棧解決方案 &#x1f517; 項目地址&#xff1a;https://github.com/Haohao-end/LMForge-End-to-End-LLMOps-Platform-for-Multi-Model-Agents ? 歡迎 Star &…

如何使顯示器在筆記本蓋上蓋子時還能正常運轉

1、搜索找到控制面板&#xff0c;打開進入 2、找到硬件和聲音&#xff0c;進入 3、選擇電源選項 4、選擇 選擇關閉筆記本計算機蓋的功能 5、把關閉子蓋時&#xff0c;改成不采取任何操作 參考鏈接&#xff1a;筆記本電腦合上蓋子外接顯示器依然能夠顯示設置_筆記本合上外接顯示…

FPGA學習筆記——SDR SDRAM的讀寫(調用IP核版)

目錄 一、任務 二、需求分析 三、Visio圖 四、具體分析 1.需要注意的問題 &#xff08;1&#xff09;器件SDRAM需要的時鐘 &#xff08;2&#xff09;跨時鐘域&#xff08;異步FIFO&#xff09; 2.模塊分析和調用 &#xff08;1&#xff09;SDR SDRAM IP核調用 &…

離散數學學習指導與習題解析

《離散數學學習指導與習題解析&#xff08;第2版&#xff09;》是屈婉玲、耿素云、張立昂編著的《離散數學&#xff08;第2版&#xff09;》的配套參考書&#xff0c;旨在為學生提供系統的學習指導和豐富的習題解析。本書內容全面&#xff0c;涵蓋數理邏輯、集合論、代數結構、…

Qt網絡通信服務端與客戶端學習

Qt網絡通信服務端與客戶端學習 一、項目概述 本項目基于Qt框架實現了TCP服務端與客戶端的基本通信&#xff0c;涵蓋連接、消息收發、斷開管理等功能&#xff0c;適合初學者系統學習Qt網絡模塊的實際用法。 二、項目結構 52/ 服務端&#xff1a;main.cpp、widget.cpp、widget.h5…