锁在应用层的优化思路

并发编程锁优化
本文介绍了在并发编程中如何通过减少锁持有时间、减小锁粒度、锁分离和锁粗化等方法来优化锁的使用,提高程序的并发性能。

减少锁持有时间

如下面的代码:

public synchronized void test() {
        executeMethod1();
        multiThreadExecute();
        executeMethod2();
    }

如果真正存在资源的竞争,需要加锁的函数是multiThreadExecute(),其他两个函数executeMethod1和executeMethod2都没有资源的竞争时,这样写只会增加线程持有锁的时间,就会导致其他线程等待这个锁的时间增长,影响性能。这种情况下,应该修改为:

    public void test() {
        executeMethod1();
        synchronized (this) {
            MultiThreadExecute();
        }
        executeMethod2();
    }

减小锁粒度

这个思路最典型的例子就是JDK中的重要成员ConcurrentHashMap,ConcurrentHashMap将整个区间分成若干个Segment(默认是16个),每一个Segment都是一个子map,每个Segment都拥有自己的一把锁。当需要向map中插入数据时,并不是先申请所有的锁,而是根据需要插入的数据的key的hashcode计算出应该从插入到哪一个Segment,然后再申请这个Segment的锁。所以理想情况下,ConcurrentHashMap最多可能有16个线程真正同时插入数据。

但是较小锁粒度会有一个问题:如果需要访问全局数据(这时需要取得全局锁),消耗的资源会比较多。以ConcurrentHashMap为例,put操作使用分段锁提高了并发,但是size()函数却没那么幸运,size函数返回map中所有有效的元素个数,所以需要访问所有数据,也就需要取得所有的锁,损耗的性能是比较多的。

锁分离

同样以JDK中的重要成员LinkedBlockingQueue为例,take()和put()函数分别从队列中取得数据和向队列中添加元素。因为LinkedBlockingQueue是链表实现的,take和put操作分别在队头和队尾操作,互不影响,所以这两个操作就不应该公用一把锁。下面是jdk中LinkedBlockingQueue的代码的一部分:

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();

可以看到分别定义了takeLock和putLock,这两个操作不适用同一把锁,削弱了锁竞争的可能性,提高了性能。

锁粗化

所谓的锁粗化就是如果代码中有连续的对同一把锁的申请操作,则需要考虑将这些锁操作合并为一个。比如:

    public void test() {
        synchronized (this) {
            // do sth
        }
        synchronized (this) {
            // do sth
        }
    }

这样的代码应该合并为:

    public void test() {
        synchronized (this) {
            // do sth
        }
    }

锁粗化的思想和减少锁持有时间是相反的,但是在不同的场合下,他们的效果并不相同,需要我们权衡利弊再做决策。

慢 SQL 优化数据库性能调优的核心环节。一个高效的 SQL 不仅能提升响应速度,还能降低服务器负载、减少竞争资源消耗。 以下是 **系统化、可落地的慢 SQL 优化思路**,从“发现问题”到“彻底解决”,并附上代码示例与执行命令。 --- ## ✅ 一、慢 SQL 优化的整体思路(6 步法) ```text 1. 发现 → 2. 定位 → 3. 分析 → 4. 优化 → 5. 验证 → 6. 预防 ``` 我们逐层展开: --- ## ✅ 第一步:发现慢 SQL(如何知道有慢查询?) ### 🔹 方法 1:开启 MySQL 慢查询日志 ```ini # my.cnf 配置 [mysqld] slow_query_log = ON slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 # 超过1秒记录 log_queries_not_using_indexes = ON # 记录未用索引的SQL ``` 动态启用(无需重启): ```sql SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; ``` ### 🔹 方法 2:使用 `pt-query-digest` 分析日志(推荐工具) ```bash pt-query-digest /var/log/mysql/slow.log > report.txt ``` 输出内容包括: - 最耗时 Top 10 SQL - 执行次数最多但总时间长的 SQL - 是否存在全表扫描、临时表、文件排序等问题 > 📌 推荐每天定时生成报告,自动告警异常 SQL --- ## ✅ 第二步:定位问题 SQL(哪条 SQL 最需要优化?) 通过 `pt-query-digest` 报告找到以下特征的 SQL: | 特征 | 说明 | |------|------| | `Query_time` 平均 > 1s | 用户已明显感知卡顿 | | `Rows_examined` > 1万 | 扫描行数过多,可能缺索引 | | `Executes` 次数高 × 单次慢 | 总耗时巨大,优先级最高 | 👉 示例: ```text # Query 1: 1.2k QPS, Avg time 1.8s, Examines 100K rows select * from orders where user_id=123 and status='PAID' ``` → 这类高频+高延迟 SQL 是首要优化目标! --- ## ✅ 第三步:分析执行计划(为什么慢?) 使用 `EXPLAIN` 查看 SQL 执行路径: ```sql EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID'; ``` 重点关注字段: | 字段 | 合理值 | 异常表现 | 含义 | |------|--------|----------|-------| | `type` | `ref`, `range`, `index` | `ALL` ❌ | 访问类型,`ALL`=全表扫描 | | `key` | 非 NULL | `NULL` ❌ | 实际使用的索引 | | `rows` | 越小越好 | >10000 ❌ | 预估扫描行数 | | `Extra` | — | `Using filesort`, `Using temporary` ❌ | 额外操作,影响性能 | --- ## ✅ 第四步:常见问题及优化策略 ### 🟢 优化策略 1:添加合适索引 #### 场景:WHERE 条件字段无索引 ❌ 慢: ```sql SELECT * FROM orders WHERE user_id = 123; -- type=ALL, key=NULL → 全表扫描 ``` ✅ 快: ```sql ALTER TABLE orders ADD INDEX idx_user_id (user_id); ``` #### 复合条件 → 建复合索引 ```sql SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID'; ``` ✅ 创建联合索引: ```sql ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); ``` > ⚠️ 注意最左前缀原则:`WHERE status='PAID' AND user_id=123` 也能命中该索引 --- ### 🟢 优化策略 2:使用覆盖索引减少回表 当查询字段都在索引中时,无需回到主键查数据。 ❌ 回表多: ```sql -- 查询了 create_time,但索引只有 (user_id, status) SELECT create_time FROM orders WHERE user_id = 123; ``` ✅ 改为覆盖索引: ```sql ALTER TABLE orders ADD INDEX idx_covering (user_id, status, create_time); ``` 此时 `Extra=Using index` → 直接从索引获取数据,不回表! --- ### 🟢 优化策略 3:避免 `Using filesort`(文件排序) #### 问题:ORDER BY 字段无索引 ❌ 慢: ```sql SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC; -- Extra: Using filesort ``` ✅ 解决:创建 `(user_id, create_time)` 联合索引 ```sql ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time); ``` 这样既能快速过滤用户订单,又能按时间排序,避免额外排序开销。 --- ### 🟢 优化策略 4:深分页优化(LIMIT M,N) #### 问题:跳过大量行 ❌ 慢: ```sql SELECT * FROM orders ORDER BY id LIMIT 10000, 20; -- 跳过1万行,效率极低 ``` ✅ 方案 1:基于主键继续查询(推荐) ```sql -- 上一页最后一条记录 id=10000 SELECT * FROM orders WHERE id > 10000 ORDER BY id LIMIT 20; ``` ✅ 方案 2:延迟关联(适用于非主键排序) ```sql SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY create_time LIMIT 10000, 20 ) t ON o.id = t.id; ``` 只对 ID 排序分页,再关联原表,大幅减少扫描。 --- ### 🟢 优化策略 5:避免函数操作字段(导致索引失效) ❌ 慢: ```sql SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01'; -- 函数导致索引无法使用 ``` ✅ 快: ```sql SELECT * FROM orders WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00'; ``` 让字段独立出现在左边,才能走索引。 --- ### 🟢 优化策略 6:禁止 `SELECT *` ❌ 浪费带宽、增加回表概率、影响覆盖索引命中 ```sql SELECT * FROM large_table WHERE condition = 'xxx'; ``` ✅ 明确指定字段: ```sql SELECT id, name, email FROM users WHERE active = 1; ``` 尤其在有索引的情况下,可以更好地利用覆盖索引。 --- ### 🟢 优化策略 7:防止 N+1 查询(应用层问题) ❌ Java 示例: ```java List<User> users = userDao.findAll(); // 1次 for (User u : users) { List<Order> orders = orderDao.findByUserId(u.getId()); // N次 } ``` ✅ 改为批量查询: ```sql SELECT * FROM orders WHERE user_id IN (1,2,3,...); ``` 或使用 JOIN 一次性查出所有关联数据。 --- ## ✅ 第五步:验证优化效果 优化后必须验证是否真正变快! ### 方法 1:对比执行时间 ```sql -- 优化前 SELECT SQL_NO_CACHE * FROM orders WHERE user_id = 123; -- 添加索引后 ALTER TABLE orders ADD INDEX idx_user_id (user_id); -- 再次执行,观察时间变化 SELECT SQL_NO_CACHE * FROM orders WHERE user_id = 123; ``` > 使用 `SQL_NO_CACHE` 避免查询缓存干扰测试结果 ### 方法 2:再次运行 `EXPLAIN` 确认: - `type` 从 `ALL` 变成 `ref` - `rows` 从 10万 → 100 - `Extra` 不再出现 `Using filesort` --- ## ✅ 第六步:建立预防机制(防患于未然) | 措施 | 说明 | |------|------| | ✅ 开发规范 | 禁止 `SELECT *`、强制审核上线 SQL | | ✅ APM 监控 | 使用 Druid、SkyWalking 实时监控 SQL 耗时 | | ✅ 慢 SQL 告警 | 设置阈值(如 >500ms)自动通知 | | ✅ 定期巡检 | 每周分析慢日志,持续优化 | | ✅ 使用 ORM 注意事项 | MyBatis/Hibernate 避免自动生成低效 SQL | --- ## ✅ 附加技巧:SQL 重写建议 | 原写法 | 优化写法 | 原因 | |--------|-----------|--------| | `LIKE '%abc%'` | 尽量不用,或用全文索引 | 前导通配符无法用索引 | | `IN (子查询)` | 改为 `JOIN` 或缓存结果 | 子查询可能不走索引 | | `OR` 条件 | 改为 `UNION` | OR 容易导致索引失效 | | `NOT IN` / `!=` | 改为 `LEFT JOIN IS NULL` | 否定条件难优化 | --- ## ✅ 总结:慢 SQL 优化 Checklist ✅ 每次遇到慢 SQL,请按此清单排查: | 检查项 | 是否完成 | |--------|----------| | ☐ 是否开启了慢查询日志? | □ | | ☐ 是否用 `pt-query-digest` 分析了 Top 慢 SQL? | □ | | ☐ 是否使用 `EXPLAIN` 查看了执行计划? | □ | | ☐ `type` 是否为 `ALL`?需加索引 | □ | | ☐ `Extra` 是否有 `Using filesort` 或 `Using temporary`? | □ | | ☐ 是否存在深分页 `LIMIT 10000,20`? | □ | | ☐ 是否用了 `SELECT *`? | □ | | ☐ WHERE 中是否有函数操作字段? | □ | | ☐ 是否存在 N+1 查询? | □ | | ☐ 优化后是否验证了效果? | □ | > 💡 **黄金法则:不要猜!要用工具看真实数据** --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值