引言:

  • 本文总字数:约 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…