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';

索引使用有三大禁忌:

  1. 最左匹配原则:联合索引(a,b,c)只对a、ab、abc条件生效
  2. 隐式转换陷阱:varchar字段用数字查询会使索引失效
  3. 避免过度索引:每个额外索引会增加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=1  

5. 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同时失效。最终用多级缓存解决:

  1. 本地缓存(Caffeine):5秒过期
  2. 分布式缓存(Redis):30分钟过期
  3. 数据库:最终存储
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服务器调优要点

线上服务突然变慢时,我习惯用这套检查清单:

  1. CPUtop -H -p <PID> 看哪个线程CPU高
  2. 内存free -h 观察swap使用情况
  3. IOiostat -x 1 看await和%util
  4. 网络ss -tnlp 检查连接状态

有一次ES集群响应变慢,用vmstat 1发现cs(上下文切换)高达5万次/秒。调整线程池参数后恢复正常:

# Elasticsearch配置调整
thread_pool.search.size: 16
thread_pool.search.queue_size: 1000

TCP参数优化也能带来惊喜。针对高并发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
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐