分布式锁详解

在高并发和分布式系统中,为保证资源的一致性和互斥访问,分布式锁是必不可少的技术手段。本文将系统地讲解分布式锁的概念、场景、实现原理、使用方式,并附 PHP 示例。

1. 什么是分布式锁

定义

分布式锁(Distributed Lock)是用于在 多台机器或多个服务实例中控制共享资源访问的机制。它保证在同一时间,只有一个客户端可以操作某个资源,从而避免并发冲突。

特点:

  • 互斥性:同一时间只有一个客户端持有锁。

  • 可重入性(可选):同一客户端可以重复获取锁。

  • 可靠性:锁过期或客户端异常可自动释放,避免死锁。

  • 可扩展性:适用于分布式系统和微服务架构。

2. 分布式锁的应用场景

分布式锁在企业场景非常常见,典型使用场景包括:

  • 库存扣减

    • 秒杀或抢购场景,防止库存超卖。
  • 订单号生成

    • 保证全局唯一订单号。
  • 任务调度

    • 多实例系统中,保证任务只被单实例执行。
  • 缓存更新

    • 防止缓存击穿时,多实例同时重建缓存。
  • 支付或转账操作

    • 防止重复扣款或资金异常。

3. 分布式锁的实现原理

3.1 数据库锁

  • 原理

    • 利用数据库唯一索引或事务机制创建锁记录。
  • 示例

    INSERT INTO locks(name, create_time) VALUES('order_123', NOW());
    -- 成功表示获得锁,失败表示锁已被占用
    

    优缺点

    • 优点:简单易用,直接使用现有数据库。

    • 缺点:高并发性能较差,数据库可能成为瓶颈。

3.2 Redis 锁

  • 原理

    • 利用 Redis 的 SETNX 或 SET key value NX PX 原子操作。

    • 设置锁过期时间,防止死锁。

    • (RedLock红锁可以解决在 Redis ​主从复制或哨兵模式下,使用单实例 Redis 锁可能遇到的锁失效问题。)

  • 释放锁

    • 需要验证当前客户端持有锁,防止误删他人锁。
  • 优点

    • 高性能,适合秒级或毫秒级操作。
  • 缺点

    • 锁过期可能导致业务执行未完成就释放,需要设计安全释放策略。

3.3 ZooKeeper 锁

  • 原理

    • 使用 ZooKeeper 的 临时顺序节点实现锁。

    • 创建节点表示加锁,最小顺序节点获得锁。

    • 节点释放或客户端宕机,其他节点继续竞争。

  • 优点

    • 强一致性,可靠。
  • 缺点

    • 部署复杂,开发成本高。

4. 分布式锁使用注意事项

  • 锁过期时间

    • 避免死锁,锁持有时间需大于业务执行时间。
  • 持有者验证

    • 释放锁前需确认自己是持有者。
  • 重试机制

    • 获取锁失败时可等待重试或立即返回错误。
  • 业务幂等性

    • 即使锁失败或重复执行,业务结果保持一致。
  • 减少锁粒度

    • 尽量缩短锁持有时间,提高并发性能。

5. 实践示例 (PHP Redis 分布式锁示例)

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$lockKey = 'order_123';
$lockValue = uniqid();   // 锁持有者标识
$expire = 3000;          // 锁过期时间:3秒(毫秒)

// 尝试获取锁
$acquired = $redis->set($lockKey, $lockValue, ['nx', 'px' => $expire]);

if ($acquired) {
    try {
        // 执行业务逻辑,比如扣减库存
        echo "获得锁,执行业务\n";
    } finally {
        // 释放锁(验证持有者)
        $script = "
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
        ";
        $redis->eval($script, [$lockKey, $lockValue], 1);
        echo "释放锁\n";
    }
} else {
    echo "获取锁失败,稍后重试\n";
}
  • 说明

    • NX:只有锁不存在时才设置成功。

    • PX:设置锁自动过期时间。

    • eval Lua 脚本保证释放锁操作的原子性。

    • uniqid() 确保锁持有者标识唯一。