分布式锁与线程锁的理解和使用
一、线程锁(本地锁,JVM级别)
理解:
线程锁用于同一进程内多线程对共享资源的互斥访问,保证线程安全。常见的有 synchronized、ReentrantLock、ReadWriteLock 等。
例子:
使用 ReentrantLock 保证库存扣减的线程安全(单服务场景)。
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 实现分布式锁,防止重复下单。
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); } }使用:
RedisDistributedLock lock = new RedisDistributedLock(); if (lock.tryLock(3000)) { try { // 执行业务(创建订单、扣减库存等) } finally { lock.unlock(); } }例子2:基于 ZooKeeper 的分布式锁实现
原理说明:
利用 ZooKeeper 的临时顺序节点特性,多个客户端在同一个锁节点下创建临时顺序子节点,节点序号最小的客户端获得锁,其他客户端监听前一个节点的删除事件,实现公平的分布式锁。
1. 获取锁的核心逻辑
// 创建临时顺序节点 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(); // 唤醒后重新尝试 }2. 释放锁的核心逻辑
// 删除当前节点即释放锁 zk.delete(currentPath, -1);3. 监听回调(唤醒等待线程)
@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) | 跨服务实例互斥,基于临时顺序节点 | 创建临时顺序节点,序号最小获得锁 |
四、总结
- 线程锁:适合单机多线程场景,无法解决多服务实例的竞争问题。
- 分布式锁:适合微服务/分布式系统,但需考虑锁超时、误删、可重入、红锁等问题。
- 锁的选择:根据业务场景(并发量、是否跨服务、资源类型)选择合适的锁机制,避免性能下降或死锁。
- 线程锁解决单机多线程竞争,分布式锁解决多服务实例竞争;乐观锁适合读多写少,悲观锁适合写多读少;读写锁提升读并发,信号量实现限流。锁的本质是"串行化临界资源访问",需根据场景选择合适粒度。