Skip to content
第 12 章 架构 ⏱ 12 分钟阅读

第 12 章:分布式锁 ​

学习目标 ​

  • 理解分布式锁的核心问题
  • 掌握 Redis 分布式锁(Redlock)和 ZK 锁
  • 学会用 Redisson 简化锁的实现
  • 避免锁误删、锁失效、并发的坑

一、为什么需要分布式锁? ​

java
// ❌ 单机锁在多节点下失效
public synchronized void deductStock() {
    if (stock > 0) {
        stock--;
    }
}
// 3 个 JVM 各有不同的锁,同一商品被扣 3 次
text
[商品库存 = 1]
   ├─ 节点 A:加锁(进程 A 的锁) → 扣
   ├─ 节点 B:加锁(进程 B 的锁) → 扣
   └─ 节点 C:加锁(进程 C 的锁) → 扣
   实际库存 = -2 ❌

二、Redis SET NX 实现 ​

java
public class RedisLock {
    @Resource
    private StringRedisTemplate redis;

    public boolean tryLock(String key, String value, long expireSeconds) {
        Boolean ok = redis.opsForValue()
            .setIfAbsent(key, value, Duration.ofSeconds(expireSeconds));
        return Boolean.TRUE.equals(ok);
    }

    public void unlock(String key, String value) {
        // Lua 脚本:先校验再删除
        String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                     "return redis.call('del', KEYS[1]) " +
                     "else return 0 end";
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
        redis.execute(script, List.of(key), value);
    }
}
java
// 使用
public void deductStock(Long productId) {
    String lockKey = "lock:stock:" + productId;
    String lockValue = UUID.randomUUID().toString();

    if (lock.tryLock(lockKey, lockValue, 30)) {
        try {
            int stock = jdbc.queryForObject("SELECT stock FROM product WHERE id = ?", Integer.class, productId);
            if (stock > 0) {
                jdbc.update("UPDATE product SET stock = stock - 1 WHERE id = ?", productId);
            }
        } finally {
            lock.unlock(lockKey, lockValue);  // 删自己的锁
        }
    }
}

⚠️ 坑 1:unlock 不校验 value 直接 del,删了别人的锁。必须用 Lua 脚本保证"Get + Del"原子。

三、Redisson 简化 ​

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
java
@Service
public class StockService {
    @Resource
    private RedissonClient redisson;

    public void deductStock(Long productId) {
        String lockKey = "lock:stock:" + productId;
        RLock lock = redisson.getLock(lockKey);

        try {
            // 最多等 5 秒,锁 30 秒自动过期
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                try {
                    // 业务逻辑
                    int stock = jdbc.queryForObject("SELECT stock FROM product WHERE id = ?", Integer.class, productId);
                    if (stock > 0) {
                        jdbc.update("UPDATE product SET stock = stock - 1 WHERE id = ?", productId);
                    }
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

⚠️ 坑 2:lock.lock(30, TimeUnit.SECONDS) 客户端断网时,锁过期被别的线程拿到,你的代码继续 unlock 删了别人的锁。用 tryLock,业务超时自动退出。

四、可重入锁 ​

java
// Redisson 可重入
public void methodA() {
    RLock lock = redisson.getLock("lock:order");
    lock.lock();
    try {
        methodB();   // 内部再次获取同一把锁,可重入
    } finally {
        lock.unlock();
    }
}

public void methodB() {
    RLock lock = redisson.getLock("lock:order");
    lock.lock();   // ✅ 重入成功
    try {
        // ...
    } finally {
        lock.unlock();   // 配对释放
    }
}

底层用 hincrby 计数,加一次 +1,释放一次 -1,锁到 0 才真释放。

五、读写锁 ​

java
// 读写分离场景:商品详情
// 读多写少,用读写锁提升性能
public void updateProduct(Long productId, ProductDto dto) {
    RReadWriteLock rwLock = redisson.getReadWriteLock("lock:product:" + productId);
    RLock writeLock = rwLock.writeLock();

    writeLock.lock();
    try {
        productRepository.update(productId, dto);
    } finally {
        writeLock.unlock();
    }
}

public Product getProduct(Long productId) {
    RReadWriteLock rwLock = redisson.getReadWriteLock("lock:product:" + productId);
    RLock readLock = rwLock.readLock();

    readLock.lock();
    try {
        return productRepository.findById(productId);   // 多个 reader 可并发
    } finally {
        readLock.unlock();
    }
}

六、公平锁 ​

java
// 默认非公平锁:谁先抢到谁拿
// 公平锁:按请求顺序排队
RLock fairLock = redisson.getFairLock("lock:order");
fairLock.lock();
// 100 个线程按顺序拿到锁,不会出现"饿死"

⚠️ 坑 3:高并发场景用公平锁,性能急剧下降(1000 QPS 降到 100)。默认非公平锁足矣,用业务幂等保证正确性。

七、Redlock(多 Redis 节点) ​

java
// 单 Redis 节点不可靠,挂了就锁失效
// Redlock:在 N 个独立 Redis 节点上加锁,过半成功才算获取
Config config = new Config();
config.useClusterServers()
    .addNodeAddress("redis://redis1:6379")
    .addNodeAddress("redis://redis2:6379")
    .addNodeAddress("redis://redis3:6379");

RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock("lock:order");
lock.lock();
// 至少 2/3 节点加锁成功才算有效

Martin 大佬的 Redlock 算法争议较大,生产慎用。推荐用 Zookeeper 或 DB 锁 用于强一致场景。

八、Zookeeper 分布式锁 ​

java
// Curator 框架
@Bean
public CuratorFramework curatorFramework() {
    return CuratorFrameworkFactory.builder()
        .connectString("zk1:2181,zk2:2181,zk3:2181")
        .retryPolicy(new ExponentialBackoffRetry(1000, 3))
        .build();
}

public void withLock(String key, Runnable task) {
    InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/lock/" + key);
    try {
        if (lock.acquire(5, TimeUnit.SECONDS)) {
            try {
                task.run();
            } finally {
                lock.release();
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
text
Zookeeper vs Redis
├── ZK: 强一致,临时顺序节点,业务可用性略低
└── Redis: 高可用,SET NX,极端情况下可能锁失效

本章小结 ​

方案适用
Redis SET NX性能优先,容忍偶发失效
Redisson锁的瑞士军刀,推荐
Zookeeper强一致,适合金融
数据库性能差,适合低频
关键点经验
超时业务执行时间的 3 倍
值UUID 唯一,防误删
续期Redisson Watchdog 自动续期
锁粒度越细越好,不要锁整个商品库

动手练习 ​

  1. SET NX 锁:实现 RedisLock,带 Lua 脚本原子解锁
  2. Redisson 接入:用 Redisson 改写商品扣库存,验证并发安全
  3. 可重入:写一个 methodA → methodB 的调用链,验证可重入
  4. 读写锁:商品详情接口加上读写锁,模拟 100 并发读 + 1 写

下一章:第 13 章:分布式 ID →

本站基于 VitePress 构建 · 由 StackHub 团队维护