分布式锁原理 --及常见实现方式的优劣势分析

本文介绍了分布式锁的概念,背景以及其基本原则。详细探讨了通过基本数据库、Redis和Zookeeper实现分布式锁的方法,并提及了其他第三方实现方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分布式锁

  • 什么是锁
  • 背景介绍
  • 分布式锁的基本原则
  • 常见的分布式锁实现方式

什么是锁

简单的来说就是对资源操作时的一种控制策略	
何为资源:能被程序访问的所有信息或是媒介;比如常见的文件,数据库,内存中的数据等
何为操作:像读,写,修改等

背景介绍

以Java开发为例子说明
	在单体应用或是单实例部署的情况下,所有的请求处理都是在同一个JVM中。可借助java 中的锁机制来控制对资源的访问比如:synchronized关键字,java.util.concurrent.* 中的api
但是如果应用程序是分布式部署或是集群部署,请求会别分发到不同的实例上来处理。这样就会带来一个问题:
如果资源是被一个JVM给锁定了(比如说通过synchronized),那么其它的JVM是如何知道资源的锁定状态呢?
	实际上java在处理请求时没有办法直接跨越JVM的,这个时候只能是借用一个"第三方"来饰演资源的公共角色?
这个“第三方”,就可以是我们常见的文件,数据库等外部的一切可访问资源

分布式锁的基本原则

那么在设计分布式锁的时候,需要考虑哪些因素呢:
1.互斥性:在同一时刻只能由一个客户端持久
2.独立性:在正常情况下,只有锁的拥有者才可以释放锁即可识别锁拥有者的身份
3.可用性:即在使用端出现异常的情况下,锁还是可以正常被使用的,即不会存在死锁或是长时间无资源可用的状态

常见的分布式锁实现方式

常见实现有三种
1.基于数据库
2.基于Redis
3.基于Zookeeper
4.其它第三实现
1.基本数据库
不做详细的说明,大体的思路就是一张表来记录资源的各种状态
2.基于Redis
查看代码实例:
public class CustomRedisLock implements  java.util.concurrent.locks.Lock {
private StringRedisTemplate stringRedisTemplate;
//资源的唯一key
private String lockKey=null;
//默认锁时间
private int defaultLockTimeout;
//锁ID标识
private AtomicReference<String> lockedID = new AtomicReference<>();
//本机JVM锁
private Lock lock=null;
//
ThreadLocal<String> threadLocal=new ThreadLocal();
/**
	stringRedisTemplate:
	lockKey:锁资源标识
	defaultLockTimeout:锁超时时间
*/
public CustomRedisLock(StringRedisTemplate stringRedisTemplate,String lockKey,int defaultLockTimeout){
    this.stringRedisTemplate=stringRedisTemplate;
    this.lockKey="Lock-"+lockKey;
    this.defaultLockTimeout=defaultLockTimeout;
    this.lock=new ReentrantLock();
}

@Override
public void lock() {
}

@Override
public void unlock() {
    String findRequestId=this.stringRedisTemplate.opsForValue().get(this.lockKey);
    //是当前线程获取到的锁
    if(!StringUtils.isEmpty(findRequestId) && findRequestId.equals(threadLocal.get())){
        //删除redis锁标识,只能释放锁定状态是当前线程操作的Key.即要满足原则的第三点
        String scriptText = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        //Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        List<String> keys=Collections.singletonList(lockKey);
        DefaultRedisScript<Integer> script =new DefaultRedisScript<>();
        script.setScriptText(scriptText);
        script.setResultType(Integer.class);
        Object result=stringRedisTemplate.execute(script,keys,findRequestId);
        log.info("Delete Result ="+result.getClass().getName()+",value="+((Long)result).longValue());
    }
    /*
        log.info("release lock:{key:{},uuid:{}}", key, uuid);
        return redisTemplate.execute(
                (RedisConnection connection) -> connection.eval(
                        RELEASE_LOCK_LUA_SCRIPT.getBytes(),
                        ReturnType.INTEGER,
                        1,
                        key.getBytes(),
                        uuid.getBytes())
        ).equals(RELEASE_LOCK_SUCCESS_RESULT);
     */
}

@Override
public void lockInterruptibly() throws InterruptedException {
}

@Override
public boolean tryLock() {
    try{            
        lock.tryLock();//JVM的锁
        String requestId=UUID.randomUUID().toString().replace("-","")+Thread.currentThread().getId();
        Boolean execute = this.stringRedisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                //??蛋疼: 1.5.12.RELEASE 版本的这个Set操作没有返回r操作结果
                //其它版本的,如果此RedisConnection接口可以返回操作结果,则无需要后的比较确认步骤
                //设置key 与 设置过滤时间必须是原子性
                 connection.set(lockKey.getBytes(Charset.forName("UTF-8")),requestId.getBytes(Charset.forName("UTF-8")),
                        Expiration.seconds(defaultLockTimeout),RedisStringCommands.SetOption.SET_IF_ABSENT);
                 log.info("------------------------------");
                threadLocal.set(requestId);
                return true;
            }
        });
        //鉴于上述的蛋疼点,比较redis中锁身份标识是不是当前线程的,如果是则表明当前线程获取到了锁
        String findRequestId=this.stringRedisTemplate.opsForValue().get(this.lockKey);
        log.info("execute result="+execute.booleanValue()+", value="+findRequestId);
        if(requestId.equals(findRequestId)){
            return true;
        }else {
            threadLocal.remove();
            return false;
        }
    }finally {
        lock.unlock();
    }
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    return false;
}
@Override
public Condition newCondition() {
    return null;
}
}

缺点:为什么一定要设置Key的超时时间呢?
	原因:是防止客户端在获取到了锁之后没有释放锁或是客户端在获致到锁之后宕机了,锁不能被释放。
	加入超时时间设置,也是一种被动的检测机制
	虽然引入了超时时间的检测机制,但会引发另外一个问题就是:这个超时时间该设置多长时间的问题?
	有方案说可以自动延续key的超时时间,但是这种方法也不能全面覆盖拟所有的场景(比如:java在触发了GC动作后)
	也就是说这个问题最终还是由业务使用方来确定一个合理的时间值
3.基于Zookeeper
实现方案: 
	第三方实现->curator-recipes
	 <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-client</artifactId>
        <version>2.11.1</version>
    </dependency>

    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>2.11.1</version>
    </dependency>
	其中有几种锁的实现方式:
	InterProcessMutex
	InterProcessReadWriteLock
	InterProcessSemaphoreMutex
	InterProcessMultiLock
	实现原理为:
		在Lock-Key节点下创建临时有序列节点
		获致Lock-key下所有的子节点,按照有序规则排序,如果最小的节点是当前连接创建的则代表获取到了
		监听Lock-Key下所有比自己大的节点的删除事件
		备注:因为是临时节点,连接使用完/断开之后节点就会被自动清除

在这里插入图片描述

4.其它第三方实现
redisson  / https://github.com/redisson/redisson
<think>好的,用户之前让我介绍了高并发请求的整个链路,现在他们问到了Zookeeper的分布式锁具体是怎么实现的。我需要先回顾一下之前的内容,确保这次回答能够衔接上。 用户可能是在设计高并发系统时遇到了分布式锁的问题,特别是在处理资源竞争时需要确保一致性。他们可能已经了解了一些基本的分布式锁概念,比如Redis的RedLock,但现在想深入了解Zookeeper的实现方式。 首先,我需要解释Zookeeper的基本机制,比如临时节点和顺序节点,这些是Zookeeper实现分布式锁的关键。然后,详细描述获取锁和释放锁的过程,以及如何处理异常情况,比如会话中断导致的临时节点删除。 用户可能关心Zookeeper锁的优势和劣势,比如与Redis相比,Zookeeper的CP特性确保了强一致性,但可能在性能上稍逊。需要对比说明,帮助用户根据场景选择合适的方案。 另外,用户可能想知道实际应用中需要注意的问题,比如惊群效应和锁的公平性,以及如何通过监听前一个节点来优化。这部分需要详细说明,确保用户理解如何避免常见陷阱。 最后,可能需要提供一个简单的代码示例,帮助用户更直观地理解实现方式。不过要注意代码的简洁性和语言的中立性,避免过于依赖特定编程语言。 总结下来,回答需要结构清晰,涵盖原理、流程、优缺点、注意事项和示例,同时与之前的高并发内容衔接,帮助用户全面理解Zookeeper分布式锁的应用场景和实现细节。</think>Zookeeper 的分布式锁是基于其 **临时顺序节点(Ephemeral Sequential Nodes)** 和 **Watcher 机制** 实现的,能够保证在分布式系统中对共享资源的互斥访问。以下是具体实现原理和流程: --- ### **1. 核心机制** - **临时节点(Ephemeral Node)**: 客户端创建的节点会在会话(Session)结束时自动删除,避免锁持有者宕机后锁无法释放。 - **顺序节点(Sequential Node)**: 节点名末尾自动追加递增序号(如 `/lock-0000000001`),便于判断锁获取顺序。 - **Watcher 监听**: 客户端可监听节点的创建、删除事件,实现阻塞等待锁的释放。 --- ### **2. 加锁流程** #### **步骤1:创建临时顺序节点** - 客户端在 Zookeeper 的指定路径(如 `/locks`)下创建一个 **临时顺序节点**,例如: ```bash /locks/lock-0000000001 ``` #### **步骤2:判断是否获取锁** - 客户端获取 `/locks` 下所有子节点,并 **按序号排序**。 - **锁获取规则**: - 若当前客户端创建的节点序号是 **最小的**,则成功获取锁。 - 若序号非最小,则监听 **前一个序号节点** 的删除事件(即等待前一个客户端释放锁)。 **示例**: 假设三个客户端创建了三个节点: - ClientA: `/locks/lock-0000000001` - ClientB: `/locks/lock-0000000002` - ClientC: `/locks/lock-0000000003` 此时,ClientA 的节点序号最小,直接获得锁;ClientB 监听 `lock-0000000001`,ClientC 监听 `lock-0000000002`。 #### **步骤3:阻塞等待锁** - 若未获得锁,客户端通过 Watcher 监听前一个节点的删除事件,进入阻塞状态。 - 当监听到前一个节点被删除时(即锁被释放),重新执行步骤2,判断自己是否成为最小节点。 --- ### **3. 释放锁** - **主动释放**:客户端完成任务后,主动删除自己创建的临时节点。 - **被动释放**:若客户端宕机或会话超时,Zookeeper 自动删除临时节点,避免死锁。 --- ### **4. 具体实现(以 Curator 客户端为例)** Apache Curator 封装了 Zookeeper 分布式锁实现,简化使用: ```java InterProcessMutex lock = new InterProcessMutex(client, "/locks"); try { // 尝试加锁(支持超时) if (lock.acquire(10, TimeUnit.SECONDS)) { // 执行业务逻辑... } } finally { lock.release(); // 释放锁 } ``` --- ### **5. 优缺点分析** #### **优点**: - **强一致性**:基于 Zookeeper 的 CP 特性,锁状态全局一致,无脑裂问题。 - **避免死锁**:临时节点自动清理,确保锁最终释放。 - **公平锁**:节点按顺序排队,先到先得。 #### **缺点**: - **性能较低**:频繁的节点创建、删除和 Watcher 通知在高并发场景下可能成为瓶颈。 - **复杂度高**:需维护 Zookeeper 集群,增加运维成本。 --- ### **6. 适用场景** - **强一致性需求**:如金融交易、库存扣减等需要严格互斥的场景。 - **长事务场景**:锁持有时间较长时,Zookeeper 的临时节点机制更可靠。 --- ### **7. 对比其他分布式锁方案** | **方案** | **实现原理** | **优点** | **缺点** | |----------------|----------------------------|-------------------------|---------------------------| | **Zookeeper** | 临时顺序节点 + Watcher | 强一致性、公平锁 | 性能较低、复杂度高 | | **Redis** | SETNX + Lua脚本(RedLock) | 高性能、简单易用 | 弱一致性(需容忍脑裂) | | **Etcd** | Lease(租约)+ Revision版本 | 高可用、支持TTL自动释放 | 需维护额外组件 | --- ### **8. 注意事项** 1. **惊群效应**: 若所有客户端监听同一节点,节点释放时可能触发大量请求。应通过 **监听前一个节点** 避免。 2. **重试机制**: 网络抖动可能导致 Watcher 丢失,需结合重试逻辑。 3. **会话管理**: 确保客户端与 Zookeeper 的心跳正常,防止因会话过期导致锁意外释放。 --- 通过 Zookeeper 的分布式锁,可以实现对共享资源的安全访问,但需根据业务场景权衡一致性与性能需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值