阅读 51

分布式锁的实现

本文来讲分布式锁的两种实现,一种是用Redis实现,另一个种使用Zookeeper实现。

1. 分布式系统

分布式系统(distributed system)是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。

2. CAP理论

  • C:Consistency一致性
  • A:Availability可用性
  • P:Partition tolerance分区容错性

CAP理论是指,在一个分布式系统中,这三个要素最多只能同时实现两点,不可能三者兼顾。

既然是分布式系统,那么在系统中会存在很多节点,这些节点通过网络进行通信。如果网络发生故障,比如A节点和B节点的网络无法通信了,那么系统中就存在A分区和B分区,那么分区容错性就是指当出现了分区问题,整个系统还需要继续运行。所以一般在设计分布式框架或系统时,一定要会满足P,会在AP和CP之间选择,本文介绍的Redis就是满足AP,而Zookeeper是满足CP。

3. Redis分布式锁

Redis提供了setnx指令来实现分布式锁。

3.1 代码实现

String lockKey = "lockKey";
String clientId = UUID.randomUUID().toString();
    try {
        //设置锁超时时间
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId,30, TimeUnit.MILLISECONDS);
        //锁还没有被释放,直接返回
        if (!result){
             return "error";
        }

        int account = Integer.parseInt(stringRedisTemplate.opsForValue().get("account"));
        if (account > 0){
        int realAccount = account - 1;
        stringRedisTemplate.opsForValue().set("account",realAccount + "");
        System.out.println("扣减成功,剩余库存:"  + realAccount + "");
        }else {
            System.out.println("扣减失败,库存不足");
        }
    }finally {
        //判断该锁是不是当前线程加的
        if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
         //业务代码执行完,释放锁
         stringRedisTemplate.delete(lockKey);
        }
    }

   return "end";
复制代码

为每一个线程设置线程ID,并将该ID作为锁Key信息的value值,在删除锁的时候,进行判断,如果是本线程自己加的锁,可以删除,如果不是,不能删除。

具体思路如下:

  • 现获取锁的key
  • 为当前线程设置线程ID
  • 在try代码块里,设置锁并设置超时时间
  • 获取锁的状态,如果没有设置成功,返回
  • 拿到设置锁成功的状态后,获取具体业务的value值
  • value减一,并更新缓存
  • 在finally代码块中判断,线程ID是不是加锁的ID
  • 释放锁

3.2 存在问题

  • 如果业务代码执行的时间大于锁的过期时间,没有对锁进行续期
  • 由于Redis一般是集群部署的,所以会出现由于主节点挂掉的话,从节点会取而代之。这时候如果客户端在主节点上申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂了,然后从节点变为主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了,这样就导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。

4. Zookeeper分布式锁

4.1 原理

获取锁: 1.首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。

Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

2.再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

3.又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。 Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。

于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。

释放锁:

任务完成,客户端显示释放 任务执行过程中,客户端崩溃

4.2 存在问题

  • 由于Zookeeper中ZAB协议(下次介绍)的存在,每次加锁/释放锁都会由leader同步给集群的所有节点,导致性能不高
  • 当加锁过程中leader节点挂了,由于Zookeper要先选举一个leader,才能对外提供服务,这个时候服务是不可用的,既,满足的是CP。
文章分类
后端
文章标签