1. Java接口性能优化的15个实战技巧
作为一名在Java后端开发领域摸爬滚打多年的老手,我深知接口性能对系统稳定性的重要性。今天就来分享15个经过实战检验的优化方案,这些技巧曾帮助我们将核心接口的响应时间从500ms降到80ms,系统吞吐量提升了6倍。
1.1 本地缓存的选择与陷阱
本地缓存最大的优势在于零网络开销,访问速度堪比内存操作。我在电商促销系统中使用Guava Cache处理商品基础信息,QPS轻松突破5万+。但要注意几个关键点:
- 容量控制:一定要设置maximumSize,我们曾因未限制缓存大小导致OOM
java复制Cache<String, Product> cache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
- 失效策略:根据业务特点选择TTL策略。比如用户画像数据我们设置30分钟过期,而城市列表这种静态数据直接永不过期
特别注意:本地缓存会导致集群节点间数据不一致,比如用户修改密码后,其他节点可能仍缓存旧数据。我们的解决方案是通过Redis Pub/Sub发布变更事件
1.2 分布式缓存的最佳实践
当单机内存不够或需要数据共享时,Redis是不二之选。但要注意这些细节:
- 连接池配置:我们通过压测发现,连接数=QPS*平均RT/1000时性能最佳
java复制JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200); // 根据实际压力调整
config.setMaxIdle(50);
-
序列化优化:使用Protostuff比JDK序列化体积小3-5倍。某次优化将1MB的User对象序列化后从180KB降到35KB
-
热点Key处理:曾遇到某个明星商品缓存命中率99%,导致单分片过热。解决方案:
- 本地缓存+Redis多级缓存
- 对Key进行哈希分片
1.3 并行化编程的实战技巧
通过ForkJoinPool实现并行查询,这是我们的订单详情优化案例:
java复制CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> getOrder(id), pool);
CompletableFuture<List<Item>> itemsFuture = CompletableFuture.supplyAsync(() -> getItems(id), pool);
CompletableFuture<Delivery> deliveryFuture = CompletableFuture.supplyAsync(() -> getDelivery(id), pool);
CompletableFuture.allOf(orderFuture, itemsFuture, deliveryFuture)
.thenApply(v -> {
Order order = orderFuture.join();
order.setItems(itemsFuture.join());
order.setDelivery(deliveryFuture.join());
return order;
});
避坑指南:
- 线程池大小建议=CPU核数*(1+平均等待时间/计算时间)
- 避免在并行任务中操作ThreadLocal,会出现上下文丢失
- 使用自定义线程名前缀方便问题排查
1.4 异步化的正确姿势
我们通过事件总线实现非核心逻辑异步化:
java复制// 订单创建主流程
Order order = createOrder(request);
// 发送领域事件
eventBus.post(new OrderCreatedEvent(order));
// 事件处理器
@Subscribe
public void handleOrderCreated(OrderCreatedEvent event) {
// 发短信、更新推荐列表等非核心操作
}
经验总结:
- 事件处理要幂等,网络抖动可能导致重复投递
- 监控事件积压情况,我们曾因Kafka消费延迟导致数据不一致
- 重要业务建议采用事务消息(如RocketMQ)
1.5 池化技术的参数调优
数据库连接池配置示例:
properties复制# HikariCP配置(我们生产环境最优参数)
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-timeout=2000
关键指标监控:
- 活跃连接数波动
- 获取连接耗时(超过50ms需要告警)
- 连接等待线程数
1.6 分库分表的避坑指南
我们使用ShardingSphere处理10亿级用户表:
yaml复制spring:
shardingsphere:
datasource:
names: ds0,ds1
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: t_order_$->{user_id % 16}
踩过的坑:
- 避免使用UUID作为分片键,会导致热点问题
- 分布式ID建议用Snowflake,我们改造后支持了机房感知
- 跨分片查询要加limit,否则会内存溢出
1.7 SQL优化的黄金法则
几个救命级的优化案例:
-
索引失效:发现
WHERE status=1 AND create_time>'2023-01-01'即使有联合索引也全表扫描,原因是status的区分度太低(90%都是1)。解决方案:调整索引顺序为(create_time, status) -
深度分页:
LIMIT 1000000,10优化为:
sql复制SELECT * FROM table WHERE id > last_id ORDER BY id LIMIT 10
- 隐式转换:发现
WHERE mobile=13800138000(mobile是varchar类型)导致索引失效,要改为WHERE mobile='13800138000'
1.8 预先计算的实践方案
对于实时性要求不高的榜单数据,我们采用:
java复制// 每天凌晨预计算
@Scheduled(cron = "0 0 3 * * ?")
public void preComputeHotProducts() {
List<Product> hotProducts = computeTop100();
redisTemplate.opsForValue().set("hot_products", hotProducts);
}
技巧:采用双缓存策略避免计算期间缓存失效
java复制redisTemplate.opsForValue().set("hot_products:v2", newData);
redisTemplate.rename("hot_products:v2", "hot_products");
1.9 事务优化的关键点
通过@Transactional注解优化事务:
java复制// 反例:整个方法都在事务中
@Transactional
public void processOrder() {
// 查询操作1
// 查询操作2
// 写操作
}
// 正例:缩小事务范围
public void processOrder() {
// 查询操作1
// 查询操作2
@Transactional
void doInTransaction() {
// 写操作
}
}
重要原则:
- 事务中避免RPC调用,网络抖动会导致长事务
- @Transactional默认遇到RuntimeException才回滚,要明确指定rollbackFor
- 只读查询添加@Transactional(readOnly=true)能提升性能
1.10 NoSQL的选型策略
各场景下的选择建议:
| 场景 | 推荐方案 | 我们使用案例 |
|---|---|---|
| 商品搜索 | Elasticsearch | 支持中文分词、相关性排序 |
| 用户画像 | MongoDB | 灵活的动态字段 |
| 实时监控 | InfluxDB | 高效的时间序列处理 |
| 社交关系 | Neo4j | 图关系遍历 |
特别提醒:Elasticsearch的深分页问题要通过search_after解决,不要用from+size
1.11 批量操作的艺术
JDBC批量插入优化对比:
java复制// 错误做法:单条插入
for (Order order : orders) {
jdbcTemplate.update("INSERT...");
}
// 正确做法:批量插入
jdbcTemplate.batchUpdate("INSERT...", new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) {
// 设置参数
}
public int getBatchSize() {
return orders.size();
}
});
性能数据:批量插入1000条记录,从12s降到0.8s
1.12 锁粒度控制实战
库存扣减的优化案例:
java复制// 粗粒度锁:影响所有商品
public synchronized void deductInventory(Long productId) {
// ...
}
// 细粒度锁:只锁特定商品
private static final ConcurrentHashMap<Long, Object> locks = new ConcurrentHashMap<>();
public void deductInventory(Long productId) {
Object lock = locks.computeIfAbsent(productId, k -> new Object());
synchronized (lock) {
// ...
}
}
进阶方案:使用Redis分布式锁时,要加随机值防误删,设置过期时间防死锁
1.13 上下文传递的优化
ThreadLocal的典型用法:
java复制public class UserContext {
private static final ThreadLocal<User> holder = new ThreadLocal<>();
public static void set(User user) {
holder.set(user);
}
public static User get() {
return holder.get();
}
public static void clear() {
holder.remove();
}
}
// 在拦截器中设置
UserContext.set(currentUser);
// 在Service中获取
User user = UserContext.get();
注意事项:
- 线程池场景要用InheritableThreadLocal
- 必须及时remove(),否则会导致内存泄漏
- 跨线程传递考虑使用TransmittableThreadLocal
1.14 集合初始化的学问
HashMap初始化优化对比:
java复制// 反例:默认初始容量16,插入1000次要扩容7次
Map<String, Object> map = new HashMap<>();
// 正例:指定初始容量
Map<String, Object> map = new HashMap<>(1024);
扩容公式:当元素数 > 容量*负载因子(0.75)时扩容。我们通过压测发现,预设大小能减少30%的GC时间
1.15 分批查询的智慧
MyBatis分页查询示例:
xml复制<select id="queryUsers" resultType="User">
SELECT * FROM users
WHERE id > #{lastId}
ORDER BY id
LIMIT #{pageSize}
</select>
处理海量数据:
- 使用游标方式替代offset分页
- 导出任务采用生产者-消费者模式
- 添加sleep避免把数据库打挂
这些优化技巧都是我们在双11大促中经过实战检验的,当然具体实施时还需要根据业务特点进行调整。最后分享一个排查性能问题的心得:先通过Arthas等工具定位热点,再用火焰图分析耗时分布,最后针对性优化,切忌盲目动手。