分布式锁的一些理解

 在多线程并发的状况下,单个节点内的线程安全能够经过synchronized关键字和Lock接口来保证。java

synchronized和lock的区别redis

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

  2. synchronized在发生异常时,会自动释放线程占有的锁,所以不会致使死锁现象发生;而Lock在发生异常时,若是没有主动经过unLock()去释放锁,则极可能形成死锁现象,所以使用Lock时须要在finally块中释放锁;编程

  3. Lock可让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不可以响应中断;缓存

  4. Lock能够提升多个线程进行读操做的效率。(能够经过readwritelock实现读写分离,一个用来获取读锁,一个用来获取写锁。)安全

  当开发的应用程序处于一个分布式的集群环境中,涉及到多节点,多进程共同完成时,如何保证线程的执行顺序是正确的。好比在高并发的状况下,不少企业都会使用Nginx反向代理服务器实现负载均衡的目的,这个时候不少请求会被分配到不一样的Server上,一旦这些请求涉及到对统一资源进行修改操做时,就会出现问题,这个时候在分布式系统中就须要一个全局锁实现多个线程(不一样进程中的线程)之间的同步。bash

  常见的处理办法有三种:数据库、缓存、分布式协调系统。数据库和缓存是比较经常使用的,可是分布式协调系统是不经常使用的。服务器

  经常使用的分布式锁的实现包含:多线程

      Redis分布式锁Zookeeper分布式锁Memcached并发

基于 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. 什么是分布式锁?

相关文章
相关标签/搜索