大型系统性能优化问题

数据库层面优化

架构优化

主从复制与读写分离

方案 :搭建数据库主从集群,主库负责处写操作(增、删、改),从库负责处理读操作。
好处 :减轻主库压力,提升读性能,并且从库可以作为备份和故障转移。
实现:应用层(通过中间件或框架)需要实现数据源路由,将写请求发往主库,读请求发往从库。

使用 MyBatis-Plus实现读写分离:

MyBatis-Plus 在其 3.1.0 及以上版本内置了简易的读写分离插件。

1. 项目依赖 (Maven)
xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version> <!-- 请使用最新版本 -->
</dependency>
2. 配置多个数据源

首先需要手动配置主从数据源。这里使用 Spring 的 @ConfigurationProperties 来绑定。

java 复制代码
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
}

application.yml 中配置数据源连接信息:

yaml 复制代码
spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://master-host:3306/db_name
      username: root
      password: master-password
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://slave-host:3306/db_name
      username: root
      password: slave-password

3. 配置读写分离路由数据源和 MyBatis-Plus 插件

这是核心步骤,创建一个路由数据源,并将其设置给 MyBatis

java 复制代码
@Configuration
@MapperScan("com.yourpackage.mapper")
public class MybatisPlusConfig {

    @Bean
    public DynamicDataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);

        // 创建动态数据源,并设置默认数据源为主库
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加读写分离插件
        interceptor.addInnerInterceptor(new DynamicDataSourceInnerInterceptor());
        return interceptor;
    }

    // 将动态数据源设置为 SqlSessionFactory 的数据源
    @Bean
    public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dynamicDataSource);
        // ... 其他配置,如mapper locations, type aliases等
        return sqlSessionFactory.getObject();
    }
}

在 Service 层,你也可以在类或方法上使用 @DS。

java 复制代码
@Service
@DS("slave") // 整个类的读方法默认走从库
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUserById(Long id) {
        // 此方法会走从库,因为类上有 @DS("slave")
        return userMapper.selectById(id);
    }

    @Override
    @DS("master") // 这个方法显式覆盖,走主库
    @Transactional
    public void updateUser(User user) {
        userMapper.updateUser(user);
    }
}

分库分表

当标数据超过千万,索引膨胀,性能胡急剧下降。
水平分表 :最常用。将一张大表的数据,按照某种规则(如用户ID取模、时间范围)拆分到多个结构相同的表中。
垂直分表 :将一张宽表中不常用或者占用空间大的字段拆分到另一张表中,用主键关联。
分库 :在分表的基础上,将不同的表分布到不同的数据库实例中,进一步分散压力。
工具:可以使用MyCat、ShardingSphere等中间件来透明地管理分库分表。

实现水平分表

方案一:ShardingSphere-JDBC

1. 项目依赖

xml 复制代码
<!-- Spring Boot 3.x 使用 -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.3.2</version>
</dependency>

<!-- 或者使用 Spring Boot Starter -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>

2. 哈希分表配置

yaml 复制代码
# application.yml
spring:
  shardingsphere:
    datasource:
      names: ds
      ds:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/test_db
        username: root
        password: 123456
    rules:
      sharding:
        tables:
          t_user: # 逻辑表名
            actual-data-nodes: ds.t_user_$->{0..3} # 4张分表
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: user_mod
            key-generate-strategy:
              column: user_id
              key-generator-name: snowflake
        sharding-algorithms:
          user_mod:
            type: INLINE
            props:
              algorithm-expression: t_user_$->{user_id % 4}
        key-generators:
          snowflake:
            type: SNOWFLAKE
    props:
      sql-show: true # 显示实际SQL,调试用

3. 实体类和Mapper

java 复制代码
// 实体类
@Data
@TableName("t_user") // MyBatis-Plus 注解
public class User {
    private Long userId;
    private String username;
    private String email;
    private Integer age;
    private LocalDateTime createTime;
}

// Mapper接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
    
    // 复杂查询示例
    @Select("SELECT * FROM t_user WHERE age > #{minAge}")
    List<User> selectUsersByAge(@Param("minAge") int minAge);
}

4. 业务层使用

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public void addUser(User user) {
        // 会自动根据 user_id 分片到对应的物理表
        userMapper.insert(user);
    }
    
    public User getUserById(Long userId) {
        // 会自动路由到正确的分表查询
        return userMapper.selectById(userId);
    }
    
    // 范围查询会扫描所有分表
    public List<User> getUsersByAge(int minAge) {
        return userMapper.selectUsersByAge(minAge);
    }
}

如果按时间分表

yaml 复制代码
spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds.t_order_$->{2024..2025}0$->{1..12} # 202401-202512
            table-strategy:
              standard:
                sharding-column: create_time
                sharding-algorithm-name: order_by_month
        sharding-algorithms:
          order_by_month:
            type: INTERVAL
            props:
              datetime-pattern: "yyyy-MM-dd HH:mm:ss"
              datetime-lower: "2024-01-01 00:00:00"
              datetime-upper: "2025-12-31 23:59:59"
              sharding-suffix-pattern: "yyyyMM"
              datetime-interval-amount: 1
              datetime-interval-unit: MONTHS

方案二:MyBatis-Plus 动态表名

1. 依赖配置

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.6</version>
</dependency>

2. 动态表名处理器

java 复制代码
@Component
public class UserTableNameHandler implements TableNameHandler {
    
    @Override
    public String dynamicTableName(String sql, String tableName) {
        // 基于线程上下文获取分表后缀
        String tableSuffix = TableContext.getSuffix();
        if (StringUtils.isNotBlank(tableSuffix)) {
            return tableName + "_" + tableSuffix;
        }
        return tableName;
    }
}

// 表名上下文(基于ThreadLocal)
public class TableContext {
    private static final ThreadLocal<String> TABLE_SUFFIX = new ThreadLocal<>();
    
    public static void setSuffix(String suffix) {
        TABLE_SUFFIX.set(suffix);
    }
    
    public static String getSuffix() {
        return TABLE_SUFFIX.get();
    }
    
    public static void clear() {
        TABLE_SUFFIX.remove();
    }
}

3. 配置MyBatis-Plus

java 复制代码
@Configuration
@MapperScan("com.example.mapper")
public class MybatisPlusConfig {
    
    @Bean
    public DynamicTableNameParser dynamicTableNameParser() {
        return new DynamicTableNameParser().setTableNameHandlerMap(
            Collections.singletonMap("t_user", new UserTableNameHandler())
        );
    }
}

4. 业务层使用

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public void addUserByMonth(User user) {
        try {
            // 设置表名后缀(按月份)
            String monthSuffix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
            TableContext.setSuffix(monthSuffix);
            
            // 执行插入
            userMapper.insert(user);
        } finally {
            TableContext.clear();
        }
    }
    
    public User getUserByIdAndMonth(Long userId, String month) {
        try {
            TableContext.setSuffix(month);
            return userMapper.selectById(userId);
        } finally {
            TableContext.clear();
        }
    }
}

方案三:手动路由(简单场景)

1. 手动分表工具类

java 复制代码
@Component
public class TableRouter {
    
    /**
     * 根据用户ID获取表名
     */
    public String getUserTableName(Long userId) {
        int tableIndex = Math.abs(userId.hashCode()) % 4;
        return "t_user_" + tableIndex;
    }
    
    /**
     * 根据时间获取表名(按月分表)
     */
    public String getOrderTableName(LocalDateTime time) {
        return "t_order_" + time.format(DateTimeFormatter.ofPattern("yyyyMM"));
    }
}

2. 使用MyBatis的Provider

java 复制代码
@Mapper
public interface UserManualMapper {
    
    @SelectProvider(type = UserSqlProvider.class, method = "selectById")
    User selectById(@Param("userId") Long userId, @Param("tableName") String tableName);
    
    @InsertProvider(type = UserSqlProvider.class, method = "insertUser")
    void insertUser(@Param("user") User user, @Param("tableName") String tableName);
}

// SQL Provider
public class UserSqlProvider {
    
    public String selectById(Map<String, Object> params) {
        String tableName = (String) params.get("tableName");
        return "SELECT * FROM " + tableName + " WHERE user_id = #{userId}";
    }
    
    public String insertUser(Map<String, Object> params) {
        String tableName = (String) params.get("tableName");
        User user = (User) params.get("user");
        return "INSERT INTO " + tableName + " (user_id, username, email) VALUES (" +
               "#{user.userId}, #{user.username}, #{user.email})";
    }
}

3. 业务层整合

java 复制代码
@Service
public class UserManualService {
    
    @Autowired
    private UserManualMapper userManualMapper;
    
    @Autowired
    private TableRouter tableRouter;
    
    public void addUser(User user) {
        String tableName = tableRouter.getUserTableName(user.getUserId());
        userManualMapper.insertUser(user, tableName);
    }
    
    public User getUser(Long userId) {
        String tableName = tableRouter.getUserTableName(userId);
        return userManualMapper.selectById(userId, tableName);
    }
}

关于范围查询

分表后,做范围查询,可能要从多个表聚合查询结果,这样会导致查询效率明显下降。
优化方案
方案1:按时间分表 + 查询剪枝

yaml 复制代码
# 优化配置:按时间分表,查询时自动过滤不相关的分表
spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds.t_order_$->{202401..202412}
            table-strategy:
              standard:
                sharding-column: create_time
                sharding-algorithm-name: order_by_month
        sharding-algorithms:
          order_by_month:
            type: INTERVAL
            props:
              datetime-pattern: "yyyy-MM-dd HH:mm:ss"
              datetime-lower: "2024-01-01 00:00:00"
              datetime-upper: "2024-12-31 23:59:59"
              sharding-suffix-pattern: "yyyyMM"
              datetime-interval-amount: 1
              datetime-interval-unit: MONTHS
java 复制代码
// 查询时,ShardingSphere会自动只查询相关月份的分表
public List<Order> getOrdersByTimeRange(LocalDateTime start, LocalDateTime end) {
    // 如果查询2024年1月到3月的数据,只会扫描 t_order_202401, t_order_202402, t_order_202403
    // 而不是所有12张表
    return orderMapper.selectByTimeRange(start, end);
}

方案2:分表键+查询条件优化

java 复制代码
@Mapper
public interface OrderMapper {
    
    // 不推荐的写法:纯范围查询
    @Select("SELECT * FROM t_order WHERE create_time BETWEEN #{start} AND #{end}")
    List<Order> selectByTimeRange(@Param("start") LocalDateTime start, 
                                  @Param("end") LocalDateTime end);
    
    // 推荐的写法:结合分表键进行查询
    @Select("SELECT * FROM t_order WHERE order_id IN " +
            "(SELECT order_id FROM t_order WHERE user_id = #{userId}) " +
            "AND create_time BETWEEN #{start} AND #{end}")
    List<Order> selectByUserAndTimeRange(@Param("userId") Long userId,
                                         @Param("start") LocalDateTime start,
                                         @Param("end") LocalDateTime end);
    
    // 更好的写法:先通过分表键定位,再范围查询
    @Select("<script>" +
            "SELECT * FROM t_order WHERE user_id = #{userId} " +
            "<if test='start != null'> AND create_time >= #{start} </if>" +
            "<if test='end != null'> AND create_time <= #{end} </if>" +
            "</script>")
    List<Order> selectByUserIdWithTimeFilter(@Param("userId") Long userId,
                                            @Param("start") LocalDateTime start,
                                            @Param("end") LocalDateTime end);
}

分表确实会降低查询效率,但是通过合理的分表策略、查询优化和架构设计,可以将影响降到最低。关键是要在分表设计阶段就考虑查询模式,避免"先分表再优化"的被动局面。

缓存层

方案 :在应用和数据库之间加入缓存层,将热点数据(如用户信息、商品信息、首页配置)存储在内存中。
好处 :缓存性能高,查询效率快。
注意:虽然缓存可以提高查询效率,但是也使得更新更加复杂,可能存在数据一致性丢失的问题,综合实际场景使用延迟双删,分布式锁或者先删再更新等策略进行处理。

结构与索引优化

精细化的索引设计:

  1. 为WHERE、JOIN、ORDER BY子句中的字段创建索引。
  2. 使用组合索引并注意最左前缀原则。
  3. 避免在索引列上使用函数或计算。
  4. 使用EXPLAIN命令分析SQL执行计划,确保查询使用了正确的索引。

字段类型优化

  1. 使用最精确的数据类型。例如,TINYINT代替INT,CHAR代替VARCHAR(如果长度固定)。
  2. 避免使用TEXT/BLOB等大字段,如果必须使用,考虑垂直分表。
  3. 尽量使用NOT NULL,因为NULL值处理起来更复杂。

SQL语句优化

  1. 只查询需要的列,避免 SELECT *。
  2. 连表查询时,小表驱动大表,确保关联字段有索引。
  3. 合理使用批量操作,如INSERT INTO ... VALUES (...), (...), ...,减少网络IO次数。
  4. 处理大量数据时,使用分页查询,但避免使用 LIMIT M, N 形式的深度分页,可改为 WHERE id > ? LIMIT N。

数据库参数调优

连接池配置 :调整 max_connections, wait_timeout, interactive_timeout 等。
InnoDB缓冲池 :innodb_buffer_pool_size 是MySQL最重要的参数,通常设置为机器物理内存的 50%-70%,让热数据尽可能留在内存中。
日志设置:根据业务对一致性的要求,合理设置 sync_binlog 和 innodb_flush_log_at_trx_commit。在允许少量数据丢失风险的场景下,可以调低这些参数以提升写性能。

Java应用层面优化

应用层是数据库的"客户端",其代码质量直接影响数据库的压力。

1. 连接池优化

使用高效的连接池,如 HikariCP(Spring Boot默认,性能极佳)或 Druid(功能全面,带监控)。

正确配置连接池参数:

复制代码
maximumPoolSize:根据系统并发量和数据库处理能力设置,不是越大越好。
minimumIdle:维持的最小空闲连接数。
connectionTimeout:获取连接的超时时间。
idleTimeout:连接空闲超时时间。

2. ORM框架优化 (以MyBatis/JPA为例)

MyBatis

使用批量执行器 ExecutorType.BATCH 进行大量插入或更新。

避免N+1查询问题:使用或标签进行关联查询,或者手动编写JOIN SQL,而不是在循环中查询。

只查询需要的字段。

JPA (Hibernate)

警惕N+1问题,使用 @EntityGraph 或 JOIN FETCH 进行急加载。

对于复杂查询,使用原生SQL(@Query)或Specification来获得更好的控制。

在需要更新大量数据时,考虑使用批量更新,并调整 hibernate.jdbc.batch_size。

3. 异步与批量处理

异步化

对于非实时要求的操作(如记录日志、发送通知、更新统计数据),可以将其放入消息队列(如RabbitMQ、Kafka)中,由后台Worker异步处理,快速释放Web线程,提升接口响应速度。

批量化

无论是数据库操作还是外部API调用,都应尽可能合并为批量操作。例如,收集1000条记录后一次性插入,而不是循环1000次单条插入。

4. 业务逻辑与缓存

本地缓存 :对于极少变化的数据(如字典数据、配置项),可以使用 Caffeine 或 Guava Cache 在JVM内部做一层本地缓存,比访问Redis还要快。
减少不必要的交互:审视业务代码,看是否能减少数据库的访问次数。例如,一次查询出所有需要的数据,而不是在循环中多次查询。

整体架构与技术栈推荐

一个典型的优化后的架构如下:

复制代码
[客户端] 
   |
[负载均衡 - Nginx]
   |
[Java应用集群]  --> [本地缓存(Caffeine)] 
   |                  |
   |--------------> [分布式缓存(Redis)]  // 二级缓存
   |
[消息队列 - Kafka/RabbitMQ]  --> [异步处理Worker]
   |
[数据库中间件 - ShardingSphere]  --> [MySQL主库]  --> [MySQL从库]

推荐技术栈组合:

应用层:Spring Boot + MyBatis-Plus + HikariCP

缓存层:Redis (Codis/Redis Cluster)

消息队列:Kafka (高吞吐) / RabbitMQ (高可靠)

分库分表:ShardingSphere

监控:Prometheus + Grafana, SkyWalking, 阿里云Arms

总结与优化步骤

监控与定位瓶颈 :不要盲目优化。先上监控!使用APM工具(如SkyWalking, Arthas)和数据库慢查询日志,找到最耗时的接口和最慢的SQL。
从索引和SQL入手 :这是成本最低、效果最显著的优化。解决慢查询,优化索引。
引入缓存 :缓解数据库的读压力。
架构升级 :当单机数据库到达瓶颈时,实施读写分离和分库分表。
代码级优化:贯穿始终,优化业务逻辑,使用连接池、批处理和异步。

优化是一个持续的过程,需要根据实际的业务场景和监控数据来不断调整策略。

相关推荐
悟能不能悟6 小时前
idea运行tomcat的日志文件放到哪里了
java·tomcat·intellij-idea
屹奕6 小时前
基于EasyExcel实现Excel导出功能
java·开发语言·spring boot·excel
吴名氏.6 小时前
细数Java中List的10个坑
java·开发语言·数据结构·list
初学者,亦行者6 小时前
Rayon并行迭代器:原理、实践与性能优化
java·开发语言·spring·rust
毕设源码-赖学姐7 小时前
【开题答辩全过程】以 二手交易系统的设计与实现为例,包含答辩的问题和答案
java·eclipse
whltaoin7 小时前
【Spring Boot 注解解析】Bean 生命周期注解深度解析:@PostConstruct 与 @PreDestroy 面试高频考点 + 实战案例
java·spring boot·面试·bean生命周期
蒲公英源码7 小时前
教务管理系统源码
java·mysql
刃神太酷啦7 小时前
力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式
java·c语言·数据结构·c++·算法·leetcode·职场和发展