当前位置: 首页 > news >正文

大型系统性能优化问题

数据库层面优化

架构优化

主从复制与读写分离

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

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

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

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

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

@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 中配置数据源连接信息:

spring:datasource:master:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://master-host:3306/db_nameusername: rootpassword: master-passwordslave:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://slave-host:3306/db_nameusername: rootpassword: slave-password

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

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

@Configuration
@MapperScan("com.yourpackage.mapper")
public class MybatisPlusConfig {@Beanpublic 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;}@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加读写分离插件interceptor.addInnerInterceptor(new DynamicDataSourceInnerInterceptor());return interceptor;}// 将动态数据源设置为 SqlSessionFactory 的数据源@Beanpublic SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();sqlSessionFactory.setDataSource(dynamicDataSource);// ... 其他配置,如mapper locations, type aliases等return sqlSessionFactory.getObject();}
}

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

@Service
@DS("slave") // 整个类的读方法默认走从库
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic User getUserById(Long id) {// 此方法会走从库,因为类上有 @DS("slave")return userMapper.selectById(id);}@Override@DS("master") // 这个方法显式覆盖,走主库@Transactionalpublic void updateUser(User user) {userMapper.updateUser(user);}
}

分库分表

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

实现水平分表

方案一:ShardingSphere-JDBC

1. 项目依赖

<!-- 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. 哈希分表配置

# application.yml
spring:shardingsphere:datasource:names: dsds:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/test_dbusername: rootpassword: 123456rules:sharding:tables:t_user: # 逻辑表名actual-data-nodes: ds.t_user_$->{0..3} # 4张分表table-strategy:standard:sharding-column: user_idsharding-algorithm-name: user_modkey-generate-strategy:column: user_idkey-generator-name: snowflakesharding-algorithms:user_mod:type: INLINEprops:algorithm-expression: t_user_$->{user_id % 4}key-generators:snowflake:type: SNOWFLAKEprops:sql-show: true # 显示实际SQL,调试用

3. 实体类和Mapper

// 实体类
@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. 业务层使用

@Service
public class UserService {@Autowiredprivate 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);}
}

如果按时间分表

spring:shardingsphere:rules:sharding:tables:t_order:actual-data-nodes: ds.t_order_$->{2024..2025}0$->{1..12} # 202401-202512table-strategy:standard:sharding-column: create_timesharding-algorithm-name: order_by_monthsharding-algorithms:order_by_month:type: INTERVALprops: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: 1datetime-interval-unit: MONTHS

方案二:MyBatis-Plus 动态表名

1. 依赖配置

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.6</version>
</dependency>

2. 动态表名处理器

@Component
public class UserTableNameHandler implements TableNameHandler {@Overridepublic 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

@Configuration
@MapperScan("com.example.mapper")
public class MybatisPlusConfig {@Beanpublic DynamicTableNameParser dynamicTableNameParser() {return new DynamicTableNameParser().setTableNameHandlerMap(Collections.singletonMap("t_user", new UserTableNameHandler()));}
}

4. 业务层使用

@Service
public class UserService {@Autowiredprivate 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. 手动分表工具类

@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

@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. 业务层整合

@Service
public class UserManualService {@Autowiredprivate UserManualMapper userManualMapper;@Autowiredprivate 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:按时间分表 + 查询剪枝

# 优化配置:按时间分表,查询时自动过滤不相关的分表
spring:shardingsphere:rules:sharding:tables:t_order:actual-data-nodes: ds.t_order_$->{202401..202412}table-strategy:standard:sharding-column: create_timesharding-algorithm-name: order_by_monthsharding-algorithms:order_by_month:type: INTERVALprops: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: 1datetime-interval-unit: MONTHS
// 查询时,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:分表键+查询条件优化

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

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

http://www.dtcms.com/a/552901.html

相关文章:

  • 国际版多语言语聊语音厅交友源码开发:技术特性、中外差异与核心注意事项
  • 面对网络蠕虫的紧急处理方式和防护措施
  • 深入掌握 Maven Settings:从配置到实战
  • 厦门做网站seo的惠州哪家做网站比较好
  • 目标检测原理分享
  • AI驱动的VSCode插件开发:从模型研发到性能优化的全栈实践
  • 6 种无误的方法:如何备份和恢复华为手机
  • Spring Boot 整合 ShedLock 处理定时任务重复
  • 静态网站开发课程相关新闻做设计图的网站
  • MCU(微控制器单元)上的语音识别
  • 【普中Hi3861开发攻略--基于鸿蒙OS】-- 第 31 章 WIFI 实验-华为 IoTDA 设备接入
  • hive的全连接
  • 集团企业网站设计方案专业做化学招聘的网站有哪些
  • win11 wsl安装Ubuntu-22.04并迁移D盘 以及安装docker
  • 频闪拍照中的相机和光源同步问题
  • 乐吾乐3D可视化案例【智慧汽车三维可视化】
  • 网站图片验证码出不来潜江资讯网电脑版
  • 大模型在百度电商机审应用的落地实践
  • esp8266+dht11传感器实时上报温湿度
  • WPF中为Button设置IsMouseOver和IsPressed事件中改变背景颜色不起作用
  • vagrant nat网络问题-虚拟机同时存在两个IP,并且不能访问外网
  • 微信小程序代理商加盟多少钱郑州网站优化公司价位
  • Ingress-Nginx故障排除
  • WPF ListView 列表
  • 国内资深易经风水专家谷晟阳
  • seo网站优化优化排名关于网站建设电话销售的开场白
  • Docker Swarm K8s云原生分布式部署2025年版
  • 量化投资从入门到入土:什么是股市
  • Windows 下 ComfyUI + Comfy CLI + PyTorch(CUDA)完整源码安装教程
  • wordpress站点管理wordpress去除仪表盘