并发锁的一些东西 基于 jdk,zookeeper , redis实现分布式锁


基于jdk lock的并发锁

在JDK1.5以后,添加了Lock接口,它用于实现与Synchronized关键字相同的锁操作,来实现多个线程控制对共享资源的访问。但是能提供更加灵活的结构,可能具有完全不同的属性,并且可能支持多个相关的Condition对象


public interface Lock {
    // 获得锁资源
    void lock();
    // 尝试获得锁,如果当前线程被调用了interrupted则中断,并抛出异常,否则就获得锁
    void lockInterruptibly() throws InterruptedException;
    // 判断能否获得锁,如果能获得,则获得锁,并返回true(此时已经获得了锁)
    boolean tryLock();
    // 保持给定的等待时间,如果期间能拿到锁,则获得锁,同样如果期间被中断,则抛异常
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 释放锁
    void unlock();
    // 返回与此Lock对象绑定Condition实例
    Condition newCondition();
}

其中,tryLock只会尝试一次,如果返回false,则走false的流程,不会一直让线程一直等待



package com.snjx.common.utils;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;

import org.apache.log4j.Logger;

/**
 *
 * @ClassName: zklock
 * @Description:TODO(zk 分布式并发锁)
 * @author: snjx
 * @date: 2017年10月24日 下午7:50:52
 */
public class  JdkLock implements  Runnable{
    private  Logger log = Logger.getLogger(JdkLock.class);
    //并发数
    private static final int num =10;
    //倒计数器
    private static CountDownLatch cdLatch=new CountDownLatch(num);
    
    OrderCodeGeneratr orderCodeGeneratr=new OrderCodeGeneratr();
    //jdk并发锁
    private  static Lock lock= new ReentrantLock();
  
    //创建订单接口
    public void createOrder(){
        String OrderCode=null;
        //加锁
        lock.lock();
        //获取订单编号
        try {
            OrderCode=orderCodeGeneratr.getOrderCode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            //解锁
            lock.unlock();
        }
        log.info(Thread.currentThread().getName()+"----------->>>>   订单号 : "+OrderCode);
    }
    @Override
    public void run() {
        try {
            //等待其他线程初始化
            cdLatch.await();
         } catch (Exception e) {
            e.printStackTrace();
         }
        //创建订单
        createOrder();
     }
    public static void main(String[] args) {
        for (int i = 0; i <=num; i++) {
            //按照线程数迭代实例化线程
            new Thread(new JdkLock()).start();
            //创建一个线程,倒计数器减一
            cdLatch.countDown();
        }
    }

}


-----------------------------------------------------------------------------------------------------------------------------

应用场景

当多个机器(多个进程)会对同一条数据进行修改时,并且要求这个修改是原子性的。这里有两个限定:(1)多个进程之间的竞争,意味着JDK自带的锁失效;(2)原子性修改,意味着数据是有状态的,修改前后有依赖。

实现方式

  • 基于Redis实现,主要基于redis的setnx(set if not exist)命令;
  • 基于Zookeeper实现;
  • 基于version字段实现,乐观锁,两个线程可以同时读取到原有的version值,但是最终只有一个可以完成操作;

这三种方式中,我接触过第一和第三种。基于redis的分布式锁功能更加强大,可以实现阻塞和非阻塞锁。



基于zookeeper的分布式锁

ZooKeeper机制规定同一个目录下只能有一个唯一的文件名,zookeeper上的一个znode看作是一把锁,通过createznode的方式来实现。所有客户端都去创建/lock/${lock_name}_lock节点,最终成功创建的那个客户端也即拥有了这把锁,创建失败的可以选择监听继续等待,还是放弃抛出异常实现独占锁


利用临时顺序节点控制时序实现
/lock已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除,依次方便。
算法思路:对于加锁操作,可以让所有客户端都去/lock目录下创建临时顺序节点,如果创建的客户端发现自身创建节点序列号是/lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点),进入等待。
对于解锁操作,只需要将自身创建的节点删除即可。


package com.snjx.common.utils;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.SerializableSerializer;
import org.apache.log4j.Logger;

/**
 *
 * @ClassName: ZookImproveLock
 * @Description:TODO(描述)
 * @author: snjx
 * @date: 2017年10月25日 下午7:38:02
 */
public class ZookeeperImproveLock implements Lock {
    private static Logger log = Logger.getLogger(ZookeeperDistributedLock.class);
    private static final String ZK_Adder = "localhost:2181";
    private static final String Lock_Node = "/lock";
    private ZkClient client = new ZkClient(ZK_Adder, 1000, 1000, new SerializableSerializer());
    private CountDownLatch cdl = null;
    private String beforePath;// 当前请求节点
    private String currentPath;// 当前请求的节点前一个节点
    // 判断有没有lock目录,没有则创建

    public ZookeeperImproveLock() {
        if (!this.client.exists(Lock_Node)) {
            this.client.createPersistent(Lock_Node);
        }
    }

    @Override
    public void lock() {
        if (!tryLock()) {
            waitForLock();
            lock();
        } else {
            log.info(Thread.currentThread().getName() + "-----------获得分布式锁---------");
        }
    }
    private void waitForLock() {
        // 给节点添加监听
        IZkDataListener listener = new IZkDataListener() {
            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                log.info(Thread.currentThread().getName() + "----------获得handleDataDeleted事件-------------");
                if (cdl != null) {
                    cdl.countDown();
                }
            }
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {
            }
        };
        client.subscribeDataChanges(beforePath, listener);

        if (client.exists(beforePath)) {
            try {
                cdl = new CountDownLatch(1);
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        client.unsubscribeDataChanges(beforePath, listener);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        // TODO Auto-generated method stub

    }

    @Override
    public boolean tryLock() {
        // 如果currentPath为空 则为第一次尝试加锁,第一次加锁赋值currentPath
        if (currentPath == null || currentPath.length() <= 0) {
            // 创建一个临时顺序节点
            currentPath = this.client.createEphemeralSequential(Lock_Node + '/', "lock");
            log.info("-------------------->>>>currentPath  : >>" + currentPath);
        }
        // 获取所有零时节点的并排序,临时节点名称为自增长的字符串:0000000002
        List<String> childernList = this.client.getChildren(Lock_Node);
        Collections.sort(childernList);
        // 如果当前节点在所有节点中的排名第一,则获取锁成功
        if (currentPath.equals(Lock_Node + '/' + childernList.get(0))) {
            return true;
        } else {
            // 如果当前节点在所有节点中的排名中不是排名第一(二分查找法),则获取前面节点的名称,并赋值给beforePath
            int wz = Collections.binarySearch(childernList, currentPath.substring(6));
            beforePath = Lock_Node + '/' + childernList.get(wz - 1);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void unlock() {
        // 删除当前节点
        client.delete(currentPath);
    }

    @Override
    public Condition newCondition() {
        // TODO Auto-generated method stub
        return null;
    }

}

-----------------------------------------------------------------------------------------------------------------------------

基于redis实现的分布式锁

先来看看一些redis的基本命令:
SETNX key value
如果key不存在,就设置key对应字符串value。在这种情况下,该命令和SET一样。当key已经存在时,就不做任何操作。SETNX是”SET if Not eXists”。
expire KEY seconds
设置key的过期时间。如果key已过期,将会被自动删除。
del KEY
删除key

可以参考redis官网

锁的实现

  • 锁的key为目标数据的唯一键,value为锁的期望超时时间点;
  • 首先进行一次setnx命令,尝试获取锁,如果获取成功,则设置锁的最终超时时间(以防在当前进程获取锁后奔溃导致锁无法释放);如果获取锁失败,则检查当前的锁是否超时,如果发现没有超时,则获取锁失败;如果发现锁已经超时(即锁的超时时间小于等于当前时间),则再次尝试获取锁,取到后判断下当前的超时时间和之前的超时时间是否相等,如果相等则说明当前的客户端是排队等待的线程里的第一个尝试获取锁的,让它获取成功即可



猜你喜欢

转载自blog.csdn.net/sinat_24798023/article/details/80349739