zoukankan      html  css  js  c++  java
  • 分布式锁的实现(redis)

    1、单机锁

    考虑在并发场景并且存在竞态的状况下,我们就要实现同步机制了,最简单的同步机制就是加锁。

    加锁可以帮我们锁住资源,如内存中的变量,或者锁住临界区(线程中的一段代码),使得同一个时刻只有一个线程能访问某一个区域。

    如果是单实例(单进程部署),那么单机锁就可以满足我们的要求了,如synchronized,ReentrantLock。

    因为在一个进程中的不同线程可以共享这个锁。

    2、分布式锁

    但是如果场景来到了分布式系统呢?

    分布式系统部署在不同的机器上,或者只是简单的多进程部署。这样各个进程之间无法共享同一个锁。

    这时候我们要加分布式锁。

    分布式锁大概就是这么一个东西:通过共享的存储缓存一个状态值,用状态值的变化标识锁的占用和释放。

    可以通过mysql,redis,zk等实现分布式锁,这里我们实现一个redis的。如果你用java其实使用zk会很简单。

    3、为什么redis能用来实现分布式锁?

    1)Redis是单进程单线程模式

    redis实现为单进程单线程模式,这样多个客户端并不存在竞态关系。

    2)原子性原语

    redis提供了可以实现原子操作的原语如setnx、getset等。

    setnx

    1)SETNX key value
    
    将 key 的值设为 value ,当且仅当 key 不存在。
    
    若给定的 key 已经存在,则 SETNX 不做任何动作。
    
    SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
    
    可用版本:
    >= 1.0.0
    时间复杂度:
    O(1)
    返回值:
    设置成功,返回 1 。
    设置失败,返回 0

    getset

    GETSET key value
    
    将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
    
    当 key 存在但不是字符串类型时,返回一个错误。
    
    可用版本:
    >= 1.0.0
    时间复杂度:
    O(1)
    返回值:
    返回给定 key 的旧值。
    当 key 没有旧值时,也即是, key 不存在时,返回 nil 。

    4、实现

    package com.xiaoju.dqa.fusor.utils;
    
    import com.xiaoju.dqa.fusor.client.RedisClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    
    @Component
    public class DistributeLockUtil {
    
        // 锁超时时间, 防止死锁
        private static final long LOCK_TIMEOUT = 60;
    
        @Autowired
        private RedisClient redisClient;
    
        private boolean locked = false;
    
        public boolean lock(String key) {
            String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
            /*
            *   setnx 返回1
            *   说明: 1)key不存在, 2)成功写入锁, 并更新锁的生存时间
            *   也就是get锁
            * */
            if (redisClient.setnx(key, expireTime) == 1) {
                locked = true;
                return true;
            }
            /*
            *  没有get锁, 下面进入判断锁超时逻辑
            * */
            String currentExpireTime = redisClient.get(key);
            /*
            *   锁生存时间已经过了, 说明锁已经超时
            * */
            if (Long.parseLong(currentExpireTime) < System.currentTimeMillis()) {
                String oldValueStr = redisClient.getSet(key, expireTime);
                /*
                *   判断锁生存时间和你改的写那个时间是否相等
                *   相当于你竞争了一个更新锁
                * */
                if (oldValueStr.equals(currentExpireTime)) {
                    locked = true;
                    return true;
                }
            }
            return false;
        }
    
        public void release(String key) {
            if (locked) {
                redisClient.del(key);
                locked = false;
            }
        }
    
    }

    5、死锁

    为了解决死锁,这里设置了锁的超时时间。

        private static final long LOCK_TIMEOUT = 60;

    并通过setnx时更新锁生存时间来维护锁超时的判定。

    String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
    ...
    if (redisClient.setnx(key, expireTime) == 1) {
    ...
    }
    ...
    String oldValueStr = redisClient.getSet(key, expireTime);
    ...

    为什么要使用这种方式,而不是expire呢?

    因为setnx和expire不能作为一个原子性的操作存在,设想如果setnx之后,在执行expire之前出现了异常,那么锁将没有超时时间。也就是死锁。

    6、解决锁超时引入的竞态

    设想三个客户端,C0,C1,C2

    如果C0持有锁并且崩溃,锁没有释放。

    C1和C2同时发现了锁超时。

    然后都通过getset去拿到了旧值,在对比了旧值和之前值之后,如果相等,那么说明“我”成功修改了旧值,那么我就拿到了锁。

    7、 时钟同步

    我们看到foo.lock的value值为时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间,如果各服务器间,时间有差异。时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。
    锁的超时与否,严格依赖时间戳,时间戳本身也是有精度限制,假如我们的时间精度为秒,从加锁到执行操作再到解锁,一般操作肯定都能在一秒内完成。这样的话,我们上面的CASE,就很容易出现。所以,最好把时间精度提升到毫秒级。这样的话,可以保证毫秒级别的锁是安全的。

     

    8、一些处理不了的情况

    设想三个客户端,C0,C1,C2

    如果C0持有锁很长,锁已经超时。这时候有C1,C2判断锁超时了,然后通过超时竞争,C1拿到了锁。

    这时C0醒了过来,删除了C1的锁。

    这时,C1认为自己独占了锁,其他的进程也进入了竞争锁的情况

    对于这种情况,这里是没有提供解决办法的。

    思路是:你降级你的锁,比如给你的锁加上uuid,对不同的业务或者不同的session加上对应粒度的锁。

    可以看看这篇博客。

    http://www.cnblogs.com/kangoroo/p/6953187.html

  • 相关阅读:
    【服务总线 Azure Service Bus】ServiceBus 队列中死信(DLQ
    【API管理 APIM】APIM集成内部VNet时,常遇见的关于自定义DNS服务问题。
    【Azure云服务 Cloud Service】Cloud Service的实例(VM)中的服务描述Software Protection 与 Windows Defender, 如何设置Windows Defender Antivirus服务
    【事件中心 Azure Event Hub】关于EventHub中出现Error时候的一些问题(偶发错误,EventHub后台升级,用户端错误,Retry机制的重要性)
    【机器学习 Azure Machine Learning】使用VS Code登录到Linux VM上 (Remote-SSH), 及可直接通过VS Code编辑VM中的文件
    【机器学习 Azure Machine Learning】使用Aure虚拟机搭建Jupyter notebook环境,为Machine Learning做准备(Ubuntu 18.04,Linux)
    【API管理 APIM】APIM中对后端API服务的DNS域名缓存问题
    【机器学习 Azure Machine Learning】Azure Machine Learning 访问SQL Server 无法写入问题 (使用微软Python AML Core SDK)
    3、hive存储格式
    玄学搜索随稽化
  • 原文地址:https://www.cnblogs.com/kangoroo/p/7298370.html
Copyright © 2011-2022 走看看