分布式锁与线程锁的理解和使用
一、线程锁(本地锁,JVM级别)
理解:
线程锁用于同一进程内多线程对共享资源的互斥访问,保证线程安全。常见的有 synchronized、ReentrantLock、ReadWriteLock 等。
例子:
使用 ReentrantLock 保证库存扣减的线程安全(单服务场景)。
java
import java.util.concurrent.locks.ReentrantLock;
public class InventoryService {
private int stock = 10;
private final ReentrantLock lock = new ReentrantLock();
public void decreaseStock(int quantity) {
lock.lock(); // 1. 加锁
try { // 2. 操作共享资源
if (stock >= quantity) {
stock -= quantity;
System.out.println(Thread.currentThread().getName() + " 扣减成功,剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 库存不足");
}
} finally {
lock.unlock(); // 3. 解锁
}
}
public static void main(String[] args) { // main方法用于测试
InventoryService service = new InventoryService();
// 创建10个线程模拟并发扣减库存
for (int i = 0; i < 15; i++) {
new Thread(() -> {
service.decreaseStock(1); // 循环,每个新线程调用
}, "线程-" + i).start();
}
}
}
测试结果:(实现了互斥锁,因为线程调度是随机的,所以资源归属顺序不定) 
二、分布式锁(跨服务、跨进程)
理解:
在微服务架构中,多个服务实例可能同时操作同一共享资源(如数据库、Redis、文件存储),需要分布式锁来保证互斥。常见实现方式:Redis(SET NX EX)、ZooKeeper、etcd。分布式锁的实现选择本质上是一致性、可用性、性能的权衡。分布式锁通过跨进程协调机制,确保同一时间只有一个客户端能访问共享资源,常用于分布式事务、幂等控制、并发限流等场景。
常见实现方式:
1. 基于数据库 ------ 利用唯一索引 或行锁 实现互斥:
- 唯一索引:插入锁记录,冲突则获取失败;删除记录释放锁。
- 行锁:SELECT ... FOR UPDATE在事务中锁定记录。
优点:实现简单,依赖现有数据库。 缺点:性能瓶颈明显,存在单点风险。
2. 基于 Redis ------ 利用SETNX+过期时间实现高性能分布式锁:
- 加锁:SET key value NX PX expireTime 保证原子性。
- 解锁:Lua 脚本校验 value(客户端ID)后删除,防止误删。
- 高可用方案:RedLock 算法在多个 Redis 节点上加锁,需多数节点成功。
优点:高性能,部署简单。 缺点:弱一致性,需处理时钟漂移与主从切换锁丢失问题。
3. 基于 ZooKeeper ------ 利用临时顺序节点 和事件监听 实现强一致性锁:
- 客户端创建临时顺序节点,判断是否为最小节点,是则获取锁,否则监听前一节点删除事件。
- 节点断开连接自动删除,避免死锁。
4. 基于分布式一致性算法(Raft/Paxos) 如etcd 、Consul,通过日志复制和多数派确认实现强一致性锁,适用于金融交易等高一致性场景。 缺点是实现复杂度高,性能低于 Redis。
例子 1 : 使用 Redis 实现分布式锁,防止重复下单。
java
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private Jedis jedis = new Jedis("localhost", 6379);
private final String lockKey = "order_lock:12345";
private final String requestId = UUID.randomUUID().toString();
// 加锁(超时自动释放,避免死锁)
public boolean tryLock(long expireMs) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireMs);
return "OK".equals(result);
}
// 释放锁(使用Lua脚本保证原子性,只有持锁者才能释放)
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, requestId);
}
}
使用:
java
RedisDistributedLock lock = new RedisDistributedLock();
if (lock.tryLock(3000)) {
try {
// 执行业务(创建订单、扣减库存等)
} finally {
lock.unlock();
}
}
例子2: 基于 ZooKeeper 的分布式锁实现
原理说明:
利用 ZooKeeper 的临时顺序节点特性,多个客户端在同一个锁节点下创建临时顺序子节点,节点序号最小的客户端获得锁,其他客户端监听前一个节点的删除事件,实现公平的分布式锁。
- 获取锁的核心逻辑
java
// 创建临时顺序节点
String currentPath = zk.create(LOCK_ROOT + "/lock_",
new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点并排序
List<String> children = zk.getChildren(LOCK_ROOT, false);
Collections.sort(children);
// 判断是否为最小节点
String currentNode = currentPath.substring(currentPath.lastIndexOf("/") + 1);
int index = children.indexOf(currentNode);
if (index == 0) {
// 是最小节点 → 获得锁
return;
} else {
// 不是最小节点 → 监听前一个节点
String waitPath = LOCK_ROOT + "/" + children.get(index - 1);
CountDownLatch latch = new CountDownLatch(1);
zk.exists(waitPath, true); // 注册监听
latch.await(); // 阻塞等待
lock(); // 唤醒后重新尝试
}
- 释放锁的核心逻辑
java
// 删除当前节点即释放锁
zk.delete(currentPath, -1);
- 监听回调(唤醒等待线程)
java
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
latch.countDown(); // 前一个节点被删除,唤醒
}
}
测试结果:


三、 其他常见锁类型
|------------------|---------------------------|--------------------------------------------------------------------------------------|
| 锁类型 | 作用 | 简单例子 |
| 乐观锁 | 基于版本号,更新时检查数据是否被修改 | UPDATE goods SET stock=stock-1, version=version+1 WHERE id=1 AND version=old_version |
| 悲观锁 | 认为冲突必然发生,操作前先锁定数据 | SELECT * FROM goods WHERE id=1 FOR UPDATE |
| 读写锁 | 读共享、写互斥,提高并发读性能 | ReentrantReadWriteLock:多线程可同时读,写时互斥 |
| 自旋锁 | 不释放CPU,循环尝试获取锁(适合锁持有时间极短) | AtomicBoolean + while(!lock.compareAndSet(false, true)) {} |
| 信号量 | 控制同时访问资源的线程数量 | Semaphore sem = new Semaphore(3); 最多3个线程同时执行 |
| synchronized | Java内置锁,自动加锁解锁,保证线程安全 | public synchronized void method() { // 临界区 } |
| ReentrantLock | 可重入锁,支持公平/非公平、可中断、超时 | lock.lock(); try { // 临界区 } finally { lock.unlock(); } |
| CountDownLatch | 等待多个线程完成任务后继续执行 | latch.await(); 等待计数归零 |
| CyclicBarrier | 等待多个线程都到达屏障点后一起执行 | barrier.await(); 等待其他线程到达 |
| 分布式锁 (Redis) | 跨服务实例互斥,基于Redis原子操作 | SET lock_key uuid NX PX 30000 |
| 分布式锁 (ZooKeeper) | 跨服务实例互斥,基于临时顺序节点 | 创建临时顺序节点,序号最小获得锁 |
四、总结
- 线程锁:适合单机多线程场景,无法解决多服务实例的竞争问题。
- 分布式锁:适合微服务/分布式系统,但需考虑锁超时、误删、可重入、红锁等问题。
- 锁的选择:根据业务场景(并发量、是否跨服务、资源类型)选择合适的锁机制,避免性能下降或死锁。
- 线程锁解决单机多线程竞争,分布式锁解决多服务实例竞争;乐观锁适合读多写少,悲观锁适合写多读少;读写锁提升读并发,信号量实现限流。锁的本质是"串行化临界资源访问",需根据场景选择合适粒度。