数据库层面优化
架构优化
主从复制与读写分离
方案 :搭建数据库主从集群,主库负责处写操作(增、删、改),从库负责处理读操作。
好处 :减轻主库压力,提升读性能,并且从库可以作为备份和故障转移。
实现:应用层(通过中间件或框架)需要实现数据源路由,将写请求发往主库,读请求发往从库。
使用 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);
}
        分表确实会降低查询效率,但是通过合理的分表策略、查询优化和架构设计,可以将影响降到最低。关键是要在分表设计阶段就考虑查询模式,避免"先分表再优化"的被动局面。
缓存层
方案 :在应用和数据库之间加入缓存层,将热点数据(如用户信息、商品信息、首页配置)存储在内存中。
好处 :缓存性能高,查询效率快。
注意:虽然缓存可以提高查询效率,但是也使得更新更加复杂,可能存在数据一致性丢失的问题,综合实际场景使用延迟双删,分布式锁或者先删再更新等策略进行处理。
结构与索引优化
精细化的索引设计:
- 为WHERE、JOIN、ORDER BY子句中的字段创建索引。
 - 使用组合索引并注意最左前缀原则。
 - 避免在索引列上使用函数或计算。
 - 使用EXPLAIN命令分析SQL执行计划,确保查询使用了正确的索引。
 
字段类型优化
- 使用最精确的数据类型。例如,TINYINT代替INT,CHAR代替VARCHAR(如果长度固定)。
 - 避免使用TEXT/BLOB等大字段,如果必须使用,考虑垂直分表。
 - 尽量使用NOT NULL,因为NULL值处理起来更复杂。
 
SQL语句优化
- 只查询需要的列,避免 SELECT *。
 - 连表查询时,小表驱动大表,确保关联字段有索引。
 - 合理使用批量操作,如INSERT INTO ... VALUES (...), (...), ...,减少网络IO次数。
 - 处理大量数据时,使用分页查询,但避免使用 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入手 :这是成本最低、效果最显著的优化。解决慢查询,优化索引。
引入缓存 :缓解数据库的读压力。
架构升级 :当单机数据库到达瓶颈时,实施读写分离和分库分表。
代码级优化:贯穿始终,优化业务逻辑,使用连接池、批处理和异步。
优化是一个持续的过程,需要根据实际的业务场景和监控数据来不断调整策略。