数据库层面优化
架构优化
主从复制与读写分离
方案 :搭建数据库主从集群,主库负责处写操作(增、删、改),从库负责处理读操作。
好处 :减轻主库压力,提升读性能,并且从库可以作为备份和故障转移。
实现:应用层(通过中间件或框架)需要实现数据源路由,将写请求发往主库,读请求发往从库。
使用 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入手 :这是成本最低、效果最显著的优化。解决慢查询,优化索引。
引入缓存 :缓解数据库的读压力。
架构升级 :当单机数据库到达瓶颈时,实施读写分离和分库分表。
代码级优化:贯穿始终,优化业务逻辑,使用连接池、批处理和异步。
优化是一个持续的过程,需要根据实际的业务场景和监控数据来不断调整策略。