【微服务学习笔记】分布式锁与线程锁的理解和使用

分布式锁与线程锁的理解和使用

一、线程锁(本地锁,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)etcdConsul,通过日志复制和多数派确认实现强一致性锁,适用于金融交易等高一致性场景。 缺点是实现复杂度高,性能低于 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 的临时顺序节点特性,多个客户端在同一个锁节点下创建临时顺序子节点,节点序号最小的客户端获得锁,其他客户端监听前一个节点的删除事件,实现公平的分布式锁。

  1. 获取锁的核心逻辑
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();                      // 唤醒后重新尝试
}
  1. 释放锁的核心逻辑
java 复制代码
// 删除当前节点即释放锁
zk.delete(currentPath, -1);
  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) | 跨服务实例互斥,基于临时顺序节点 | 创建临时顺序节点,序号最小获得锁 |

四、总结

  1. 线程锁:适合单机多线程场景,无法解决多服务实例的竞争问题。
  2. 分布式锁:适合微服务/分布式系统,但需考虑锁超时、误删、可重入、红锁等问题。
  3. 锁的选择:根据业务场景(并发量、是否跨服务、资源类型)选择合适的锁机制,避免性能下降或死锁。
  4. 线程锁解决单机多线程竞争,分布式锁解决多服务实例竞争;乐观锁适合读多写少,悲观锁适合写多读少;读写锁提升读并发,信号量实现限流。锁的本质是"串行化临界资源访问",需根据场景选择合适粒度。