一、分布式锁到底是个啥玩意?

咱们先打个比方。想象一下你们公司只有一个厕所,但有好几百号人。这时候要是没有排队机制,那场面绝对乱成一锅粥。分布式锁就是解决类似问题的——在分布式系统里,多个服务实例要访问共享资源时,得有套机制来保证同一时间只有一个能进去"上厕所"。

举个实际场景:电商秒杀系统。10000个人同时抢10台iPhone,如果不加锁,很可能出现超卖。这时候就需要分布式锁来协调各个微服务实例的操作顺序。

二、Redis实现方案:轻量又快速

Redis实现分布式锁最常见的就是用SETNX命令(现在推荐用SET命令带NX参数)。来看个Go语言示例:

// Go语言示例 - 使用go-redis客户端
func acquireLock(client *redis.Client, lockKey string, requestId string, expireTime time.Duration) bool {
    // 关键参数说明:
    // NX - 只有key不存在时才设置
    // EX - 设置过期时间(秒)
    result, err := client.SetNX(lockKey, requestId, expireTime).Result()
    if err != nil {
        log.Printf("获取锁异常: %v", err)
        return false
    }
    return result
}

func releaseLock(client *redis.Client, lockKey string, requestId string) bool {
    // 使用Lua脚本保证原子性
    script := redis.NewScript(`
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
    `)
    result, err := script.Run(client, []string{lockKey}, requestId).Int64()
    return err == nil && result == 1
}

优点:

  • 性能炸裂,单节点轻松上10万QPS
  • 实现简单,几行代码就能搞定
  • 客户端支持丰富,所有语言都能用

坑点提醒:

  1. 一定要设置过期时间!否则服务崩溃会导致死锁
  2. 推荐用UUID当requestId,防止误删别人的锁
  3. 主从切换可能导致锁丢失(Redlock算法可以缓解)

三、ZooKeeper方案:可靠但有点重

ZooKeeper通过临时顺序节点实现锁,天然支持等待队列。Java示例走起:

// Java示例 - 使用Curator框架
public class ZkDistributedLock {
    private final InterProcessMutex lock;
    
    public ZkDistributedLock(CuratorFramework client, String lockPath) {
        this.lock = new InterProcessMutex(client, lockPath);
    }
    
    public boolean tryLock(long timeout, TimeUnit unit) throws Exception {
        return lock.acquire(timeout, unit);
    }
    
    public void unlock() throws Exception {
        lock.release();
    }
}

// 使用示例
try {
    if (zkLock.tryLock(3, TimeUnit.SECONDS)) {
        // 业务代码
    }
} finally {
    zkLock.unlock();
}

核心优势:

  • 强一致性保证,绝对不会出现脑裂问题
  • 自带Watch机制,能实现阻塞等待锁
  • 临时节点特性,客户端断开自动释放

劝退点:

  1. 写性能捉急,每秒几千次操作就到头了
  2. 需要维护ZooKeeper集群,运维成本高
  3. 客户端使用相对复杂(Curator帮了大忙)

四、etcd方案:中庸之道

etcd的锁基于Raft共识算法,比Redis可靠,比ZK轻量。Python示例:

# Python示例 - 使用etcd3客户端
import etcd3

def etcd_lock():
    client = etcd3.client()
    lock = client.lock('shop_lock', ttl=30)
    
    try:
        # 获取锁(最多等待5秒)
        if lock.acquire(timeout=5):
            print("抢到锁了!")
            # 业务处理...
    finally:
        lock.release()

闪光点:

  • 性能介于Redis和ZK之间(约1万QPS)
  • 自带租约(Lease)机制,自动续期很方便
  • 数据持久化可靠,适合金融级场景

注意事项:

  1. 需要合理设置TTL,太短会导致频繁续约
  2. 相比Redis内存消耗更大
  3. 客户端API设计有点反人类

五、三大方案PK:该选哪个?

性能排行榜: Redis > etcd > ZooKeeper

可靠性排名: ZooKeeper ≈ etcd > Redis

选型指南:

  1. 秒杀、缓存类场景 → Redis
  2. 金融交易、配置中心 → ZooKeeper
  3. 容器调度、服务发现 → etcd

特殊场景:

  • 需要公平锁 → ZooKeeper
  • 需要读写锁 → etcd
  • 超高并发 → Redis

六、高级玩家技巧

  1. 锁重入问题:可以在Redis里用Hash结构记录线程信息和重入次数

    -- Redis Lua脚本实现可重入锁
    local key = KEYS[1]
    local threadId = ARGV[1]
    local releaseTime = ARGV[2]
    
    if (redis.call('exists', key) == 0) then
        redis.call('hset', key, threadId, '1')
        redis.call('expire', key, releaseTime)
        return 1
    end
    
    if (redis.call('hexists', key, threadId) == 1) then
        redis.call('hincrby', key, threadId, '1')
        redis.call('expire', key, releaseTime)
        return 1
    end
    
    return 0
    
  2. 锁续约方案:对于长时间任务,可以起个后台线程定期续期

    // Java线程池实现锁续约
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    scheduler.scheduleAtFixedRate(() -> {
        if (lock.isAcquiredInThisProcess()) {
            client.refreshLockTime(lockKey, requestId, 30);
        }
    }, 10, 10, TimeUnit.SECONDS);
    

七、终极总结

  1. 没有银弹!根据业务特点选择最合适的方案
  2. Redis适合大多数互联网场景,但要处理好可靠性问题
  3. 分布式锁不是万能的,某些场景可以用乐观锁或CAS替代
  4. 一定要做好监控!包括锁等待时间、获取失败率等指标

记住:分布式系统的黄金法则——任何可能出错的地方最终都会出错。你的锁方案必须能应对网络分区、进程暂停、时钟漂移等各种异常情况。