分布式锁的实现方式

分布式锁的实现方式

分布式锁实现方式

分布式锁一般可以通过Redis和Zookeeper两种中间件来实现。

1. Redis

1.1 原生Redis指令

SETNX指令(set if not exists)
【加锁方法】

SET key value NX EX timeout

【解锁方法】
如果场景简单无需value对比,那么可以直接del指令,否则需要通过一段lua脚本进行解锁

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

问题一:死锁问题,当客户端获取到锁后,客户端崩溃等问题导致失效时间没有设置成功就会导致一直持有锁
解决:保证SETNX命令和EXPIRE命令以原子的方式执行, 可以通过lua(包含SETNX + EXPIRE两条指令)或者SETNX支持失效时间

问题二:锁被其他线程释放
解决:锁必须要有一个拥有者的标记(可以通过value设置为uuid或者线程号等方式),并且也需要保证释放锁的原子性操作。或者使用不同的key

问题三:支持可重入
解决: 在加锁的时候记录加锁次数,在释放锁的时候减少加锁次数,这个就需要记录的value值使用hash数据结构了。使用lua脚本。
【加锁方法】

//锁不存在
if (redis.call('exists', key) == 0) 
then    
    redis.call('hset', key, uuid, 1);     
    redis.call('expire', key, time);     
    return 1;
end;
//锁存在,判断是否是自己的锁
if (redis.call('hexists', key, uuid) == 1) 
then    
    redis.call('hincrby', key, uuid, 1);     
    redis.call('expire', key, uuid);    
    return 1; 
end; 
//否则返回加锁失败
return 0;

【解锁方法】

//判断是否是自己的锁
if (redis.call('hexists', key,uuid) == 0) 
then
    return 0;
end;
//锁是自己的,则加锁次数-1
local counter = redis.call('hincrby', key, uuid, -1);
if (counter > 0) 
then     
    //剩余加锁次数大于0,则不能释放锁,重新设置过期时间    
    redis.call('expire', key, uuid);     
    return 1;
else
    //等于0,释放锁    
    redis.call('del', key);     
    return 1; 
end; 

问题四:自动续期,由于设置的过期时间太短或者业务执行时间太长导致锁过期,
解决: 在加锁成功时,开启定时任务,自动刷新Redis加锁key的超时时间。原生的方式越来越复杂了, 建议使用Redisson一劳永逸。

1.2 Redisson

通过引入Redisson实现分布式锁。Redisson源码中加锁/释放锁操作都是用Lua脚本完成的,还支持自动续期等功能封装的非常完善,开箱即用。
后期有时间单独写一篇源码解析

2. Zookeeper

2.1 原生ZooKeeper

大致思路是,首先创建一个持久节点/locks,请求进来时首先在/locks创建临时有序节点,然后判断当前创建得节点是不是/locks路径下面最小的节点,如果是,那么获取锁,不是则阻塞线程,同时设置监听器,监听前一个节点。获取到锁并处理完成业务逻辑后释放锁,删除掉当前节点。后一个节点就会收到通知,唤起线程,重复上面的判断。
注:只监听前一个节点主要是为了避免羊群效应。否则一个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来巨大压力。

//加锁路径
String lockPath;
//阻塞
CountDownLatch latch = new CountDownLatch(1);
//创建锁节点的路径
Sting LOCK_ROOT_PATH = "/locks"

//创建锁
public void createLock(){
    lockPath = zkClient.create(LOCK_ROOT_PATH+"/lock_", CreateMode.EPHEMERAL_SEQUENTIAL);
}

//获取锁
public boolean acquireLock(){
    //获取加锁路径下所有的锁节点
    List<String> allLocks = zkClient.getChildren("/locks");
    //按节点顺序大小排序
    Collections.sort(allLocks);
    //判断是否是第一个节点
    int index = allLocks.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));
    //如果是第一个节点,则加锁成功
    if (index == 0) {
        // 获得锁成功
        return true;
    } else {
        //不是序号最小的节点,则监听前一个节点
        String preLockPath = allLocks.get(index - 1);
        //创建监听器
        Stat status = zkClient.exists(LOCK_ROOT_PATH + "/" + preLockPath, watchedEvent -> {
             //监听到前一个节点释放锁,唤醒当前线程
             latch.countDown();
        });
        // 前一个节点不存在了,则重新获取锁
        if (status == null) {
            return acquireLock();
        } else { 
            // 阻塞当前线程,直到前一个节点释放锁
            latch.await();
            // 获得锁成功
            return true;
        }
    }
}

2.2 Curator

引入curator包

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.3.0</version>
</dependency>

业务代码使用

InterProcessMutex lock = new InterProcessMutex(client, "/locks");
// 获取锁
// 获取锁有两种方式,一种是使用acquire()方法,该方法会一直阻塞直到获取到锁
lock.acquire();

// 另一种方式是使用tryAcquire()方法,该方法会尝试获取锁,如果获取成功则返回true,否则返回false

if (lock.tryAcquire()) {
    // 获取锁成功
}

// 释放锁
// 使用release()方法释放锁:
lock.release();

ZooKeeper分布式锁虽然能有效的解决分布式锁问题,但是性能并不高。因为每次在加锁解锁的过程中,都要通过创建、销毁瞬时节点来实现。而ZooKeeper中创建和删除节点只能通过Leader服务器来执行,然后Leader服务器还需要将数据同不到所有的Follower机器上,这样频繁的网络通信,性能的短板是非常突出的。

总结

  1. 在高性能、高并发的场景下,由于ZooKeeper分布式锁性能不高不建议使用,推荐使用Redis分布式锁。
  2. 在并发量不高的场景,由于ZooKeeper的高可用特性,推荐使用ZooKeeper分布式锁。
  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值