大型系统性能优化问题

数据库层面优化

架构优化

主从复制与读写分离

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

使用 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入手 :这是成本最低、效果最显著的优化。解决慢查询,优化索引。
引入缓存 :缓解数据库的读压力。
架构升级 :当单机数据库到达瓶颈时,实施读写分离和分库分表。
代码级优化:贯穿始终,优化业务逻辑,使用连接池、批处理和异步。

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

相关推荐
达文汐2 分钟前
【困难】力扣算法题解析LeetCode332:重新安排行程
java·数据结构·经验分享·算法·leetcode·力扣
培风图南以星河揽胜3 分钟前
Java版LeetCode热题100之零钱兑换:动态规划经典问题深度解析
java·leetcode·动态规划
启山智软27 分钟前
【中大企业选择源码部署商城系统】
java·spring·商城开发
我真的是大笨蛋29 分钟前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怪兽源码1 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
恒悦sunsite1 小时前
Redis之配置只读账号
java·redis·bootstrap
梦里小白龙1 小时前
java 通过Minio上传文件
java·开发语言
人道领域1 小时前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql
sheji52612 小时前
JSP基于信息安全的读书网站79f9s--程序+源码+数据库+调试部署+开发环境
java·开发语言·数据库·算法
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于Java Web的电子商务网站的用户行为分析与个性化推荐系统为例,包含答辩的问题和答案
java·开发语言