1. 代码层面的性能优化实战
第一次接手老项目时,我被一个简单的分页查询接口震惊了——响应时间竟然超过5秒!翻开代码一看,发现开发者在for循环里疯狂拼接字符串,还用+号操作符。这种初级错误就像用勺子挖隧道,效率低得让人抓狂。
字符串拼接的正确姿势其实很简单。实测下来,StringBuilder比+号快20倍以上。特别是在处理日志拼接、SQL拼接等场景时,建议预分配足够容量:
// 错误示范:每次循环都创建新对象 String result = ""; for (int i = 0; i < 10000; i++) { result += "data" + i; } // 正确做法:预分配缓冲区 StringBuilder builder = new StringBuilder(50000); for (int i = 0; i < 10000; i++) { builder.append("data").append(i); }更隐蔽的性能杀手是循环体内的数据库查询。上周优化过一个商品详情接口,原来需要3秒,只因开发者在循环里查了N次商品SKU表。改造为批量查询后,直接降到200毫秒:
// 优化前:N+1查询问题 for (Long skuId : skuIds) { Sku sku = skuMapper.selectById(skuId); // 每次循环都查库 // ...处理逻辑 } // 优化后:一次批量查询 List<Sku> skus = skuMapper.selectBatchIds(skuIds); Map<Long, Sku> skuMap = skus.stream().collect(Collectors.toMap(Sku::getId, v -> v)); for (Long skuId : skuIds) { Sku sku = skuMap.get(skuId); // 内存操作 // ...处理逻辑 }2. 数据库查询的黄金法则
执行计划是DBA的听诊器。有次排查慢查询,发现个有趣现象:同样的SQL,测试环境跑0.1秒,生产环境要8秒。用EXPLAIN ANALYZE一看,生产环境漏了索引,导致150万行全表扫描。加上索引后,查询直接起飞:
-- 优化前:全表扫描 EXPLAIN ANALYZE SELECT * FROM orders WHERE create_time > '2023-01-01'; -- 优化后:索引扫描 CREATE INDEX idx_orders_create_time ON orders(create_time); EXPLAIN ANALYZE SELECT * FROM orders WHERE create_time > '2023-01-01';索引使用有三大禁忌:
- 最左匹配原则:联合索引(a,b,c)只对a、ab、abc条件生效
- 隐式转换陷阱:varchar字段用数字查询会使索引失效
- 避免过度索引:每个额外索引会增加10%的写入开销
批量插入也有讲究。曾优化过数据迁移脚本,原来插入10万条要15分钟。采用三个技巧后降到30秒:
- 使用COPY替代INSERT(PostgreSQL特有)
- 事务分批提交(每5000条commit一次)
- 临时禁用索引和约束
-- 高性能批量插入示范 BEGIN; ALTER TABLE orders DISABLE TRIGGER ALL; -- 禁用触发器 COPY orders FROM '/data/orders.csv' WITH CSV; ALTER TABLE orders ENABLE TRIGGER ALL; -- 恢复触发器 COMMIT;3. 并发编程的锁优化艺术
多线程环境下,锁竞争是性能头号杀手。去年优化过支付系统,TPS卡在200上不去。用arthas监控发现,80%线程阻塞在订单状态锁上。通过锁细化改造,最终提升到1200TPS:
// 优化前:粗粒度锁 public synchronized void processPayment(Order order) { // 20行业务逻辑 } // 优化后:细粒度锁 private final Map<Long, Object> orderLocks = new ConcurrentHashMap<>(); public void processPayment(Order order) { Object lock = orderLocks.computeIfAbsent(order.getId(), k -> new Object()); synchronized (lock) { // 只锁当前订单 // 20行业务逻辑 } }读写分离是另一个利器。在配置中心项目中,用ReadWriteLock使读性能提升5倍:
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); public String getConfig(String key) { rwLock.readLock().lock(); try { return configMap.get(key); } finally { rwLock.readLock().unlock(); } } public void updateConfig(String key, String value) { rwLock.writeLock().lock(); try { configMap.put(key, value); } finally { rwLock.writeLock().unlock(); } }4. JVM调优实战手册
内存泄漏排查就像破案。有次线上服务频繁Full GC,用MAT分析heap dump后,发现是ThreadLocal未清理导致的。这类问题有典型特征:
- Old区内存持续增长
- GC后内存不下降
- 线程数异常增多
JVM参数配置要量体裁衣。电商大促前,我们这样调整CMS参数:
-XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=8 -XX:ConcGCThreads=4 -XX:CMSInitiatingOccupancyFraction=75 -XX:+ExplicitGCInvokesConcurrent年轻代大小设置很关键。有个计算密集型应用,默认配置下每分钟YGC 15次。调整NewRatio后,YGC降到3次/分钟:
# 年轻代占比从1/3提高到1/2 -XX:NewRatio=15. SQL语句的魔鬼细节
同样的查询条件,不同写法性能可能差百倍。分享几个真实案例:
分页优化:
-- 低效写法(全表扫描) SELECT * FROM orders ORDER BY id LIMIT 1000000, 20; -- 高效写法(索引扫描) SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;JOIN优化:
-- 错误示范:使用OR条件 SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.status = 1 OR o.amount > 1000; -- 正确做法:UNION ALL拆分 SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE u.status = 1 UNION ALL SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE o.amount > 1000 AND u.status != 1隐式转换问题:
/* phone是varchar字段但存数字 */ EXPLAIN SELECT * FROM customers WHERE phone = 13800138000; -- 全表扫描 EXPLAIN SELECT * FROM customers WHERE phone = '13800138000'; -- 走索引6. 缓存使用的正确姿势
缓存击穿是高频面试题。去年双11,商品详情页突现大量503,就是因为热点key同时失效。最终用多级缓存解决:
- 本地缓存(Caffeine):5秒过期
- 分布式缓存(Redis):30分钟过期
- 数据库:最终存储
public Product getProduct(Long id) { // 一级缓存查询 Product product = caffeineCache.get(id, k -> { // 二级缓存查询 return redisTemplate.opsForValue().get("product:" + id) .orElseGet(() -> dbMapper.selectById(id)); }); return product; }缓存更新策略也很关键。我们采用「先更新库再删缓存」策略,配合消息队列重试:
@Transactional public void updateProduct(Product product) { // 1. 更新数据库 productMapper.updateById(product); // 2. 删除缓存 redisTemplate.delete("product:" + product.getId()); // 3. 发延迟消息(防删除失败) mqTemplate.sendDelayMessage("cache-refresh", product.getId(), 5, TimeUnit.SECONDS); }7. Linux服务器调优要点
线上服务突然变慢时,我习惯用这套检查清单:
- CPU:
top -H -p <PID>看哪个线程CPU高 - 内存:
free -h观察swap使用情况 - IO:
iostat -x 1看await和%util - 网络:
ss -tnlp检查连接状态
有一次ES集群响应变慢,用vmstat 1发现cs(上下文切换)高达5万次/秒。调整线程池参数后恢复正常:
# Elasticsearch配置调整 thread_pool.search.size: 16 thread_pool.search.queue_size: 1000TCP参数优化也能带来惊喜。针对高并发HTTP服务,我们调整了这些内核参数:
# 增大连接队列 echo 4096 > /proc/sys/net/core/somaxconn # 加快TIME_WAIT回收 echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle # 扩大端口范围 echo "1024 65000" > /proc/sys/net/ipv4/ip_local_port_range