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

第 228 章:分布式锁

学习目标

  • 理解分布式锁需求
  • 掌握 Redis + Redisson 实现
  • 了解 ZooKeeper 锁
  • 学会 MySQL 乐观锁

一、为什么需要分布式锁

场景:

  • 同一用户被两台服务同时扣款(余额超扣)
  • 库存同时被两台机器减,导致负数
  • 定时任务多实例同时执行,重复扣款

二、分布式锁要求

维度要求
互斥性任意时刻只一个客户端持锁
不死锁持锁客户端崩溃,锁可释放
可重入同一线程可多次获取同锁
谁加谁解不能解别人锁
可等待拿不到可选择阻塞或立即返回

三、Redis 分布式锁

3.1 SET NX 原理

bash
SET key value NX PX 30000
  • NX:不存在才设置
  • PX:过期时间(毫秒)
java
public class SimpleRedisLock {

    private final StringRedisTemplate redis;

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

    // 用 Lua 脚本保证"检查并删除"原子性
    private static final String UNLOCK_LUA = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """;

    public void unlock(String key, String value) {
        redis.execute(
            RedisScript.of(UNLOCK_LUA, Long.class),
            Collections.singletonList(key),
            value
        );
    }
}

3.2 价值(value)的意义

解决:value 用唯一标识(线程 UUID + 时间戳),解锁前 Lua 检查。

3.3 Redisson(推荐)

引入依赖

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.4</version>
</dependency>

配置

yaml
spring:
  redis:
    host: 127.0.0.1
    port: 6379

redisson:
  address: redis://127.0.0.1:6379
  password: ${REDIS_PASS}
  database: 0

使用

java
@Autowired
private RedissonClient redisson;

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

    try {
        // 等待 100 ms,持锁 30 秒自动释放
        boolean acquired = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
        if (!acquired) {
            throw new BizException("系统繁忙,请稍后再试");
        }

        // 业务逻辑(锁内)
        int stock = inventoryRepo.getStock(skuId);
        if (stock <= 0) {
            throw new BizException("库存不足");
        }
        inventoryRepo.deduct(skuId, 1);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        // 必须解锁,只在持锁线程中解锁
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

Watch Dog 自动续期

java
// 默认 lockWatchdogTimeout = 30 秒
// 每 10 秒自动续期到 30 秒
// 业务未完成锁不会失效
lock.tryLock(0, 30000, TimeUnit.MILLISECONDS);  // 显示指定则关 WatchDog

可重入

同线程可多次加锁,计数 +1:

java
public void a() {
    lock.lock();
    b();
    lock.unlock();
}
public void b() {
    lock.lock();    // 可重入,成功
    lock.unlock();
}

3.4 公平锁 / 读锁 / 联锁

java
// 公平锁:按等待顺序加锁
RLock fairLock = redisson.getFairLock("lock");

// 读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("rw");
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();

// 联锁:多把锁同时加
RLock lock1 = redisson.getLock("a");
RLock lock2 = redisson.getLock("b");
RedissonMultiLock multiLock = redisson.getMultiLock(lock1, lock2);
// 任一失败,所有释放

// 红锁:MultiLock + Redis Sentinel/Cluster

3.5 RedLock(多 Redis 节点)

java
RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

// 多数节点加锁成功 → 真正加锁成功
redLock.lock();
try {
    // ...
} finally {
    redLock.unlock();
}

四、ZooKeeper 分布式锁

4.1 原理

利用临时顺序节点 + 监听。

bash
# 1. 客户端 A 在 /lock 下创建临时顺序节点 → /lock/node-001
# 2. 客户端 B 创建 → /lock/node-002
# 3. B 拿到所有子节点,判断自己不是最小 → 监听比自己小一号的节点(node-001)
# 4. node-001 删除(释放锁) → 触发 B 的监听
# 5. B 重新检查,自己成最小 → 获锁

4.2 Curator

xml
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
</dependency>
java
public class ZkLockDemo {

    public static void main(String[] args) throws Exception {
        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .retryPolicy(retry)
            .build();
        client.start();

        // 互斥锁
        InterProcessMutex mutex = new InterProcessMutex(client, "/mylock");
        if (mutex.acquire(5, TimeUnit.SECONDS)) {
            try {
                System.out.println("获锁");
                Thread.sleep(5000);
            } finally {
                mutex.release();
            }
        }
    }
}

4.3 ZK 锁 vs Redis 锁

维度RedisZK
性能
可靠性主从切换可能丢锁ZAB 协议强一致
复杂度
锁粒度不公平公平(FIFO)
适用高性能场景强一致场景

五、MySQL 分布式锁

5.1 基于唯一索引

sql
CREATE TABLE distributed_lock (
    id BIGINT PRIMARY KEY,
    lock_key VARCHAR(64) UNIQUE,
    holder VARCHAR(128),
    expire_at TIMESTAMP
);
java
public boolean tryLock(String key, String holder, long ttlMs) {
    int rows = jdbc.update(
        "INSERT IGNORE INTO distributed_lock(lock_key, holder, expire_at) " +
        "VALUES(?, ?, NOW() + INTERVAL ? SECOND)",
        key, holder, ttlMs / 1000
    );
    return rows > 0;
}

public boolean unlock(String key, String holder) {
    return jdbc.update(
        "DELETE FROM distributed_lock WHERE lock_key = ? AND holder = ?",
        key, holder
    ) > 0;
}

5.2 乐观锁

sql
UPDATE inventory
SET stock = stock - 1,
    version = version + 1
WHERE sku_id = ?
  AND stock > 0
  AND version = ?       -- 乐观锁,失败时重试
java
public boolean deduct(String skuId) {
    int maxRetry = 3;
    for (int i = 0; i < maxRetry; i++) {
        Inventory inv = inventoryRepo.findBySku(skuId);
        if (inv == null || inv.getStock() <= 0) return false;

        int rows = jdbc.update(
            "UPDATE inventory SET stock = stock - 1, version = version + 1 " +
            "WHERE sku_id = ? AND version = ?",
            skuId, inv.getVersion()
        );
        if (rows > 0) return true;
    }
    return false;
}

5.3 SELECT FOR UPDATE

java
@Transactional
public void deductStock(String skuId, int qty) {
    // 悲观锁:行锁
    Inventory inv = jdbc.queryForObject(
        "SELECT * FROM inventory WHERE sku_id = ? FOR UPDATE",
        new Object[]{skuId},
        new BeanPropertyRowMapper<>(Inventory.class)
    );

    if (inv.getStock() < qty) {
        throw new BizException("库存不足");
    }
    jdbc.update("UPDATE inventory SET stock = stock - ? WHERE sku_id = ?", qty, skuId);
}

六、对比与选型

维度Redis(Redisson)ZKMySQL
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐
可靠性⭐⭐⭐⭐⭐⭐⭐⭐
公平性
重入
Watch Dog
实现复杂度
适用高性能强一致简单场景

七、实战:防止重复下单

java
@Service
@RequiredArgsConstructor
public class OrderService {

    private final RedissonClient redissonClient;
    private final OrderRepository orderRepo;

    public void createOrder(CreateOrderRequest req) {
        // 防重(用户维度)
        String lockKey = "order:create:user:" + req.getUserId();

        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 等 1 秒,持 5 秒
            if (!lock.tryLock(1, 5, TimeUnit.SECONDS)) {
                throw new BizException("请求过于频繁,请稍后");
            }

            // 业务幂等:订单号已存在直接返回
            if (orderRepo.existsByOrderNo(req.getOrderNo())) {
                return;
            }

            // 下单流程
            orderRepo.save(new Order(req));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BizException("下单失败");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

八、Redis 锁的坑

8.1 锁过期 vs 业务慢

场景:锁 5 秒,业务要 7 秒。

java
// 解决 1:长业务拆短,中途续期(WatchDog 自动做)
// 解决 2:业务带锁外核,业务结束再释放
// 解决 3:定时任务系统,慎用长持锁

8.2 主从切换

主 Redis 挂,从 Redis 升级,新主无锁信息 → 锁失效。

解决:

  1. RedLock:N 个独立 Redis,多数加锁成功才算成功
  2. Redisson RedissonRedLock:类似实现

8.3 集群模式

java
Config config = new Config();
config.useClusterServers()
    .addNodeAddress("redis://node1:6379", "redis://node2:6379", "redis://node3:6379")
    .setPassword("xxx");
RedissonClient client = Redisson.create(config);

8.4 锁泄漏

java
// 业务异常但 finally 内仍要释放
try {
    lock.lock();
    // ... 业务
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

九、可重入原理

Redis 中存储的 hash:

HSET mylock
  <threadId:UUID> <count>

每次 unlock 计数 -1,= 0 时真正删除 key。

十、本章小结

工具优点适用
Redisson功能丰富,Watch Dog高性能首选
RedLock高可用关键路径
ZK强一致金融
MySQL实现简单已有 DB,简单场景

动手练习

  1. 用 Redisson 实现"用户 5 秒内只能请求 1 次"的限流
  2. 模拟锁超时,观察 Watch Dog 自动续期
  3. 写两段并发代码,用分布式锁保证一致性
  4. 用 Curator 写一个 ZK 互斥锁

推荐阅读


下一章:第 229 章:链路追踪

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