1. 为什么我们需要不同的锁机制?
在并发编程的世界里,锁就像交通信号灯,协调着多个线程对共享资源的访问。想象一下,如果没有红绿灯,十字路口的车辆会陷入怎样的混乱?数据库和程序中的共享数据面临同样的挑战。
我曾在电商系统中遇到过典型的库存超卖问题:当100个用户同时抢购10件商品时,如果不加控制,系统可能会卖出20件甚至更多。这就是典型的并发冲突场景,而不同的锁机制正是为解决这类问题而生。
锁的核心矛盾在于:安全性与性能的权衡。锁得太严实(悲观锁)会影响系统吞吐量,锁得太宽松(乐观锁)又可能导致数据不一致。就像疫情防控,隔离措施太严格影响经济,太宽松又可能造成疫情扩散。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 悲观锁:先发制人的保守派
2.1 工作原理与实现方式
悲观锁就像个疑心重的管家,它假设最坏情况一定会发生——只要多个线程访问共享数据,就必定会发生冲突。因此它在访问数据前会先加锁,确保在自己操作期间别人无法染指。
在MySQL中,典型的悲观锁实现是:
sql复制SELECT * FROM products WHERE id=1 FOR UPDATE; -- 获取行锁
UPDATE products SET stock=stock-1 WHERE id=1;
这个FOR UPDATE子句会在事务期间锁定id=1的记录,其他事务想要修改这条记录必须等待锁释放。我在金融支付系统中就曾用这种方式保证账户余额更新的原子性。
2.2 适用场景与性能影响
悲观锁特别适合:
- 写多读少的场景(如秒杀系统)
- 临界区执行时间较长的操作
- 需要强一致性的金融交易
但要注意它的代价:
- 锁等待会导致线程阻塞,增加系统延迟
- 不当使用可能引发死锁(两个事务互相等待对方释放锁)
- 在分布式系统中实现成本较高
实战经验:在高并发场景下,建议将悲观锁的持有时间控制在20ms以内,否则容易成为系统瓶颈。可以通过将业务逻辑拆分为"加锁-核心操作-释放锁"三个阶段来优化。
3. 乐观锁:后发制人的乐天派
3.1 版本控制的核心思想
乐观锁就像个乐观的年轻人,它相信冲突很少发生,所以不提前加锁,而是在提交时检查是否有冲突。典型的实现是通过版本号或时间戳:
java复制// 伪代码示例
Product product = productDao.getById(1);
int oldVersion = product.getVersion();
product.setStock(product.getStock() - 1);
int affected = productDao.update(
"UPDATE products SET stock=?, version=version+1
WHERE id=? AND version=?",
product.getStock(), product.getId(), oldVersion
);
if(affected == 0) {
throw new OptimisticLockException("版本已变更,请重试");
}
我在社交平台的点赞功能中就采用这种方案,因为:
- 读多写少(查看点赞数远多于实际点赞)
- 冲突概率低(同一用户很少连续快速点赞)
- 允许重试(点赞失败可以重新尝试)
3.2 CAS操作的底层原理
乐观锁的底层常依赖CAS(Compare-And-Swap)指令。现代CPU提供的这个原子操作包含三个步骤:
- 读取内存值V
- 比较V是否等于预期值A
- 如果相等,则将V更新为新值B
Java中的AtomicInteger就是典型实现:
java复制AtomicInteger counter = new AtomicInteger(0);
counter.compareAndSet(0, 1); // 当前值为0时才更新为1
3.3 适用场景与ABA问题
乐观锁最适合:
- 读多写少的场景
- 冲突概率低的业务
- 需要高吞吐量的系统
但要注意ABA问题:如果值从A变成B又变回A,CAS会误认为没变化。解决方法是用版本号或时间戳而非原始值判断。
4. 局部锁:精准控制的狙击手
4.1 细粒度锁的实现方式
局部锁(也称细粒度锁)不像悲观锁那样锁整个资源,而是只锁需要的部分。就像图书馆不必关闭整个场馆,只需将热门书籍暂时收起来即可。
Java中的典型实现:
java复制ConcurrentHashMap<String, Lock> lockMap = new ConcurrentHashMap<>();
public void updateUser(String userId) {
Lock lock = lockMap.computeIfAbsent(userId, k -> new ReentrantLock());
lock.lock();
try {
// 处理用户数据
} finally {
lock.unlock();
}
}
我在用户画像系统中就用这种方式,不同用户的画像更新可以并行,只有同一用户的更新需要串行。
4.2 分段锁的巧妙设计
Java7的ConcurrentHashMap采用分段锁技术,将数据分为16个Segment,每个Segment独立加锁。这样不同Segment的操作可以完全并行,只有同一Segment的写操作需要互斥。
5. 其他重要锁类型详解
5.1 自旋锁:CPU时间的博弈
当锁被占用时,线程可以选择:
- 挂起等待(传统互斥锁)
- 循环检查(自旋锁)
java复制// 简单自旋锁实现
public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
while(!locked.compareAndSet(false, true)) {
// 自旋等待
}
}
}
适用场景:
- 锁持有时间非常短(纳秒级)
- 多核CPU环境
- 不希望线程切换开销的情况
我在内存缓存系统中使用自旋锁保护热点计数器,因为计数器更新通常只需几十纳秒。
5.2 读写锁:读写的分离艺术
读写锁(ReentrantReadWriteLock)允许多个读操作并行,但写操作独占。就像图书馆允许多人同时阅读,但修改书目时需要独占。
java复制ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作
rwLock.readLock().lock();
try {
// 读取数据
} finally {
rwLock.readLock().unlock();
}
// 写操作
rwLock.writeLock().lock();
try {
// 修改数据
} finally {
rwLock.writeLock().unlock();
}
5.3 分布式锁:跨进程的协调者
在分布式系统中,我们需要跨JVM的锁机制。常见实现方式:
- Redis实现(SETNX命令):
bash复制SET lock_key unique_value NX PX 30000
-
ZooKeeper实现(临时有序节点)
-
数据库实现(唯一索引)
我在微服务架构中用Redis红锁(RedLock)算法实现分布式锁,但要注意:
- 时钟漂移问题
- 网络分区时的可用性
- 锁的自动释放机制
6. 锁的性能优化实战经验
6.1 锁粒度选择原则
根据我的经验,选择锁粒度要考虑:
- 临界区执行时间:超过10ms考虑细化
- 冲突概率:高于10%需要更严格锁
- 系统吞吐量要求:高吞吐倾向乐观锁
6.2 避免死锁的编码规范
我团队遵循的规范:
- 按固定顺序获取多个锁
- 设置锁超时时间
- 使用tryLock而非无条件lock
- 静态代码分析工具检查潜在死锁
6.3 锁性能监控指标
在生产环境中要监控:
- 锁等待时间(超过100ms报警)
- 锁持有时间(超过50ms需要优化)
- 死锁次数(任何死锁都应立即处理)
- 锁竞争热度(识别热点锁)
7. 真实场景下的锁选择策略
7.1 电商库存扣减方案
经过多次618大促实战,我们的最佳实践是:
- 前端限流:按钮置灰
- 缓存库存:Redis预减
- 数据库更新:乐观锁+库存校验
- 最终一致:异步补偿
7.2 社交平台计数系统
对于点赞、浏览数等:
- 内存计数器:LongAdder
- 定期持久化:每分钟同步到DB
- 丢失容忍:允许少量计数丢失
7.3 金融交易系统设计
银行转账必须:
- 悲观锁锁定双方账户
- 事务隔离级别至少REPEATABLE_READ
- 添加事务重试机制
- 完备的对账系统
