锁的应用与场景:从单机到分布式
摘要:在多线程和分布式系统中,“锁”是避免资源竞争、保障数据一致性的核心机制。但你真的了解锁吗?什么时候该用锁?用哪种锁?本文通过通俗的比喻和代码示例,带你彻底搞懂锁的应用场景!
一、为什么需要锁?
想象一下:多人同时编辑同一份文档,如果不加控制,最终文档内容会乱成一锅粥。程序中的共享资源(如数据库字段、文件、内存变量)同样面临这个问题——锁的作用就是让多个线程/进程“排队”访问资源。
常见问题场景:
- 订单重复处理:用户疯狂点击提交订单,导致重复扣款。
- 超卖问题:秒杀活动中库存被减到负数。
- 数据覆盖:两个线程同时修改用户余额,后者覆盖前者结果。
二、单机环境下的锁
1. 乐观锁 vs 悲观锁
- 悲观锁:假设一定会发生冲突,先加锁再操作。
-
应用场景:冲突频繁、临界区代码执行时间长。
-
实现方式:
- 数据库:
SELECT ... FOR UPDATE
- Java:
synchronized
、ReentrantLock
- 数据库:
-
维度 | synchronized | ReentrantLock |
---|---|---|
锁管理 | JVM 自动管理,无需手动释放 | 需手动获取和释放,易忘记导致死锁 |
灵活性 | 功能简单,仅支持非公平锁 | 支持公平锁、超时、中断、多条件变量 |
性能 | JVM 优化后性能接近(低竞争场景更优) | 高并发场景更灵活(如 tryLock 减少竞争) |
调试支持 | 锁信息不易获取(如等待队列长度) | 提供 isLocked() , getQueueLength() 等方法 |
适用场景 | 简单同步需求(如单方法内的线程安全) | 复杂同步逻辑(如多条件协调、精细控制) |
- 乐观锁:假设冲突很少,先操作再检查是否冲突。
- 应用场景:读多写少、冲突概率低。
- 实现方式:
- 数据库:版本号(Version字段)+ CAS更新
- Java:
AtomicInteger
、StampedLock
代码示例:数据库乐观锁
-- 1. 查询时获取版本号
SELECT stock, version FROM product WHERE id = 1;-- 2. 更新时校验版本号
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1; -- 如果version被修改过,更新失败
2. 读写锁(ReadWriteLock)
- 核心思想:读操作不互斥,写操作互斥。
- 应用场景:读多写少,如缓存系统。
- Java实现:
ReentrantReadWriteLock
ReadWriteLock rwLock = new ReentrantReadWriteLock();// 读操作
rwLock.readLock().lock();
try {// 读取数据(允许多个线程同时读)
} finally {rwLock.readLock().unlock();
}// 写操作
rwLock.writeLock().lock();
try {// 修改数据(独占锁)
} finally {rwLock.writeLock().unlock();
}
三、分布式锁
当服务部署在多台机器上时(即使在同一台物理机上的多个容器/Pod),单机锁失效,必须使用分布式锁协调跨进程的资源访问。
1. 常见实现方案
方案 | 核心原理 | 优点 | 缺点 |
---|---|---|---|
Redis锁 | SETNX + 过期时间 + Lua脚本删除 | 性能高,实现简单 | 存在锁过期提前释放风险 |
ZooKeeper | 创建临时顺序节点,监听前序节点删除 | 可靠性高,自动释放锁 | 性能较低,需要维护ZK集群 |
数据库锁 | 基于唯一索引或行级锁 | 无需额外组件 | 性能差,高并发易成瓶颈 |
- 高并发且允许偶发锁失效:Redis + Redisson。
- 强一致性需求:ZooKeeper 或 Etcd。
- 简单场景:数据库(不推荐生产环境高频使用)
2. Redis分布式锁示例(Redisson实现)
// 1. 获取锁对象
RLock lock = redissonClient.getLock("orderLock");// 2. 尝试加锁(等待10秒,锁自动释放时间30秒)
boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLocked) {try {// 处理业务逻辑processOrder();} finally {lock.unlock();}
}
四、如何选择合适的锁?
1. 决策流程图
2. 黄金原则
- 能用单机锁就别用分布式锁(复杂度陡增)
- 锁粒度要小:锁住的范围越小,性能越高
- 优先考虑无锁设计:如使用线程安全的类(
ConcurrentHashMap
)、本地线程存储(ThreadLocal
)
五、避坑指南
- 死锁:避免嵌套锁,设置超时时间。
- 锁饥饿:公平锁可缓解,但性能会下降。
- 锁泄露:确保finally块中释放锁。
- 脑裂问题(分布式锁):选择强一致性协调器(如ZooKeeper)。
六、总结
- 单机多线程:本地锁 + 数据库唯一索引即可满足需求,无需分布式锁。优先选
synchronized
或ReentrantLock
。 - 多机/多实例:必须引入分布式锁,同时结合数据库约束保证最终安全。
- 高并发读:读写锁(
ReadWriteLock
)是救星。 - 分布式系统:Redis锁(性能)或ZooKeeper锁(可靠性)二选一。
- 终极目标:在安全性和性能之间找到平衡!
技术没有银弹,理解场景才能选出最合适的锁!