分布式锁的一些理解

 在多线程并发的情况下,单个节点内的线程平安可以通过synchronized关键字和Lock接口来保证。

synchronized和lock的区别

  1. Lock是一个接口,是基于在语言层面实现的锁,而synchronized是Java中的关键字,是基于JVM实现的内置锁,Java中的每一个工具都可以使用synchronized添加锁。

  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁征象发生;而Lock在发生异常时,若是没有自动通过unLock()去释放锁,则很可能造成死锁征象,因此使用Lock时需要在finally块中释放锁;

  3. Lock可以让守候锁的线程响应中止,而synchronized却不行,使用synchronized时,守候的线程会一直守候下去,不能够响应中止;

  4. Lock可以提高多个线程举行读操作的效率。(可以通过readwritelock实现读写星散,一个用来获取读锁,一个用来获取写锁。)

  当开发的应用程序处于一个漫衍式的集群环境中,涉及到多节点,多历程共同完成时,若何保证线程的执行顺序是准确的。好比在高并发的情况下,许多企业都市使用Nginx反向代理服务器实现负载平衡的目的,这个时刻许多请求会被分配到差别的Server上,一旦这些请求涉及到对统一资源举行修改操作时,就会出现问题,这个时刻在漫衍式系统中就需要一个全局锁实现多个线程(差别历程中的线程)之间的同步。

  常见的处置设施有三种:数据库、缓存、漫衍式协调系统。数据库和缓存是对照常用的,然则漫衍式协调系统是不常用的。

  常用的漫衍式锁的实现包罗:

      Redis漫衍式锁Zookeeper漫衍式锁Memcached

基于 Redis 做漫衍式锁

 Redis提供的三种方式:

Redis 持久化

(1)锁 SETNX:只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 下令不做任何动作。SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写。下令在设置乐成时返回 1 , 设置失败时返回 0

redis> SETNX job "programmer"    # job 设置乐成
(integer) 1

redis> SETNX job "code-farmer"   # 实验笼罩 job ,失败

(2)解锁 DEL:删除给定的一个或多个 key

(3)锁超时 EXPIRE: 为给定 key 设置生计时间,当 key 过时时(生计时间为 0 ),它会被自动删除。

  每次当一个节点想要去操作临界资源的时刻,我们可以通过redis来的键值对来符号一把锁,每一历程首先通过Redis接见同一个key,对于每一个历程来说,若是该key不存在,则该线程可以获取锁,将该键值对写入redis,若是存在,则说明锁已经被其他历程所占用。详细逻辑的伪代码如下:

try{
	if(SETNX(key, 1) == 1){
		//do something ......
	}finally{
	DEL(key);
}

  然则此时,又会出现问题,由于SETNX和DEL操作并不是原子操作,若是程序在执行完SETNX后,而并没有执行EXPIRE就已经宕机了,这样一来,原先的问题依然存在,整个系统都将被壅闭。

  幸亏Redis又提供了SET key value timeout NX方式,可以以原子操作的方式完成SETNX和EXPIRE的操作。此时只需如下操作即可。

try{
	if(SET(key, 1, 30, timeout, NX) == 1){
		//do something ......
	}
}finally{
	DEL(key);
}

  解决了原子操作,仍然另有一点需要注重,例如,A节点的历程获取到锁的时刻,A历程可能执行的很慢,在do something未完成的情况下,30秒的时间片已经使用完,此时会将该key给深处掉,此时B历程发现这个key不存在,则去接见,并乐成的获取到锁,最先执行do something,此时A线程正好执行到DEL(key),会将B的key删除掉,此时相当于B线程在接见没有加锁的临界资源,而其余历程都有机遇同时去操作这个临界资源,会造成一些错误的效果。对于该问题的解决设施是历程在删除key之前可以做一个判断,验证当前的锁是不是本历程加的锁。

String threadId = Thread.currentThread().getId()
try{
	if(SET(key, threadId, 30, timeout, NX) == 1){
		//do something ......
	}
}finally{
    if(threadId.equals(redisClient.get(key))){
        DEL(key);
    }
}

   上面的改善虽然解决锁被差别的历程释放的危险,但并没有解决获取到锁的历程在指定的时间内未完成do something操作(上面的代码另有一点小问题,就是判断操作和释放锁是两个自力的操作,不具备原子性。假设线程A判断完确实是自己加的锁 , 这时还没del ,这时有用的时间用完了 , 紧接着线程B又马上抢到了锁 , 然后线程A才执行del下令 , 就会把B抢到的锁给误删了),使得卡住的历程有可能与厥后的历程同时同问临界资源,而出现问题,因此一旦某个历程无法在超时时间内完成对临界资源的操作,就需要延伸超时的时间。此时可以启动一个守护历程,监视指定时间内获取锁的历程是否完成操作,若是没有,则添加超时时间,让程序继续执行。

String threadId = Thread.currentThread().getId()
try{
	if(SET(key, threadId, 30, timeout, NX) == 1){
		new Thread(){
            @Override
            public void run() {
            	//start Daemon
            }
         }
		//do something ......
	}
}finally{
    if(threadId.equals(redisClient.get(key))){
        DEL(key);
    }
}

  基于以上的剖析,基本上可以通过Redis实现一个漫衍式锁,若是我们想提升该漫衍式的性能,我们可以对毗邻资源举行分段处置,将请求平均的漫衍到这些临界资源段中,好比一个买票系统,我们可以将100张票分为10 部门,每部门包罗10张票放在其他的服务节点上,这些请求可以通过Nginx被平均的涣散到这些处置节点上,可以加速对临界资源的处置。

参考资料

  1. 并发编程的锁机制:synchronized和lock

  2. B站视频上一部门解说

  3. 什么是漫衍式锁?

原创文章,作者:admin,如若转载,请注明出处:https://www.2lxm.com/archives/15174.html