1. 并发控制的核心矛盾
在多线程或分布式系统中,当多个操作同时访问同一数据资源时,如何保证数据一致性成为关键挑战。我处理过的一个电商库存系统案例中,就曾因并发控制不当导致超卖——明明数据库显示库存充足,实际发货时却发现商品早已售罄。这种"读-改-写"场景下的数据竞争问题,正是锁机制要解决的核心矛盾。
锁的本质是在并发环境下建立一种秩序,就像十字路口的红绿灯。没有锁时,多个线程如同没有交通信号的车辆,可能同时修改数据导致最终结果错乱。根据对并发冲突可能性的预判态度,锁策略分为两大阵营:认为冲突很可能发生的悲观锁,和假定冲突少发的乐观锁。
2. 悲观锁的深度解析
2.1 工作机制与实现方式
悲观锁采用"先锁后改"的策略,就像图书馆里对珍贵典籍的管理——读者必须登记获得独占权限后才能阅读。在MySQL中,典型的SELECT...FOR UPDATE语句会在读取数据时直接加排他锁:
BEGIN; SELECT stock FROM products WHERE id=1 FOR UPDATE; -- 获取排他锁 UPDATE products SET stock=stock-1 WHERE id=1; COMMIT;这个过程中,第一个事务获取锁后,其他事务的相同操作会被阻塞直到锁释放。Java中的synchronized关键字和ReentrantLock类都是悲观锁的实现,它们在字节码层面通过monitorenter/monitorexit指令或AQS队列实现线程阻塞。
2.2 适用场景与性能考量
悲观锁特别适合写操作频繁且冲突概率高的场景。比如银行核心系统的账户余额变更,或者机票系统的座位锁定。但它的代价也显而易见:
- 锁获取带来的线程上下文切换开销
- 可能导致的死锁问题(需要超时机制或死锁检测)
- 降低系统整体吞吐量(长事务会放大这些问题)
我曾优化过一个支付系统,将悲观锁的持有时间从平均200ms压缩到50ms内,QPS直接提升了3倍。关键技巧包括:
- 缩小锁粒度(从表锁到行锁)
- 减少事务内非数据库操作
- 使用锁超时设置(如innodb_lock_wait_timeout)
3. 乐观锁的技术实现
3.1 版本控制机制
乐观锁采用"先改后验"的思路,如同多人协作编辑文档时的冲突检测。最常见的实现是通过版本号或时间戳:
// 更新前先获取当前版本 Product product = productDao.getById(1); int oldVersion = product.getVersion(); // 更新时校验版本 int affected = productDao.updateWithVersion( "UPDATE products SET stock=stock-1, version=version+1 WHERE id=1 AND version=?", oldVersion); if(affected == 0) { throw new OptimisticLockException("版本已变更,请重试"); }在MySQL中,这可以通过一个原子操作完成。CAS(Compare-And-Swap)指令是CPU层面支持的乐观锁,Java中的AtomicInteger等原子类就是基于此实现。
3.2 适用场景与调优策略
乐观锁在读多写少的场景下优势明显。比如社交媒体的点赞功能,或者配置信息的更新。某次性能测试显示,在读写比10:1的场景下,乐观锁方案比悲观锁吞吐量高出一个数量级。
但需要注意的陷阱包括:
- ABA问题(可通过追加时间戳或递增计数器解决)
- 高冲突场景下的重试风暴(需配合退避算法)
- 业务逻辑的幂等性设计
一个实用的技巧是结合Redis实现分布式乐观锁。用SETNX命令获取锁,EXPIRE设置超时,配合Lua脚本保证原子性:
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end4. 面试深度问答模板
4.1 基础概念辨析
面试官:能解释下两种锁的根本区别吗?
参考答案: "悲观锁像严谨的防守者,默认每次操作都可能冲突,因此提前加锁确保独占访问。典型实现如Java的synchronized和MySQL的SELECT FOR UPDATE。乐观锁则是积极的冒险家,假设冲突很少发生,只在提交时检查版本变化,如CAS操作和版本号机制。
选择依据主要看冲突概率——当写操作超过30%或临界区执行时间长时,悲观锁更合适;反之乐观锁性能更好。比如秒杀系统前期用乐观锁应对浏览流量,最后下单环节切为悲观锁。"
4.2 实战场景分析
面试官:如果让你设计一个分布式库存系统,会怎么选择锁策略?
分层设计思路:
- 前端层面:库存缓存用Redis乐观锁(WATCH+MULTI)
- 服务层:本地库存预扣用Guava的RateLimiter
- 数据库层:
- 热点商品用SELECT...FOR UPDATE SKIP LOCKED
- 普通商品用版本号乐观锁
- 最终一致性:通过定时任务补偿异常订单
数据支撑:在某次大促中,这种混合方案使系统承受住了5万QPS的冲击,超卖率控制在0.01%以下。
4.3 陷阱识别题
面试官:遇到过乐观锁导致CPU飙升的情况吗?
问题诊断: "确实遇到过。在一次秒杀活动中,由于重试逻辑没有设置退避间隔,导致大量请求在版本冲突时立即重试,形成恶性循环。通过加入随机延迟(如50-100ms的jitter)和最大重试次数限制解决了这个问题。
监控指标显示优化后CPU使用率从90%降至60%,关键是要识别出'冲突率'这个指标——当它超过20%时就应考虑切部分流量到悲观锁方案。"
5. 混合锁策略实践
5.1 读写锁的应用
ReentrantReadWriteLock是两种思想的结合体:
- 读锁是乐观的(共享访问)
- 写锁是悲观的(独占访问)
在配置中心这类读多写少的场景特别有效。我曾通过将synchronized改为读写锁,使配置读取性能提升8倍。
5.2 分布式锁演进
从ZooKeeper的临时节点到Redis的RedLock算法,再到etcd的lease机制,分布式锁的选择要考虑:
- CP还是AP特性
- 锁粒度(全局锁vs资源锁)
- 锁续约机制(避免死锁)
一个经验公式:锁持有时间应小于TTL的1/3,比如设置30秒TTL时,业务操作必须在10秒内完成。
6. 性能优化实测数据
通过JMeter对两种锁进行压测(4核8G环境):
| 指标 | 悲观锁(synchronized) | 乐观锁(CAS) |
|---|---|---|
| 100线程QPS | 1,200 | 8,500 |
| 平均延迟(ms) | 45 | 12 |
| CPU使用率 | 75% | 60% |
| 冲突率10%时QPS | 800 | 6,200 |
关键发现:当冲突率超过40%时,乐观锁性能会急剧下降,此时悲观锁反而更稳定。这提示我们需要实时监控冲突率指标。