在单机开发过程中,对于线程并发问题我们可以通过加锁来限制执行
但是在分布式系统的开发过程中,单机锁对于不同机器实例不同jvm对同一业务或资源的操作却不能生效
因此我们需要使用分布式锁来解决分布式情况下的多进程并发问题
以下主要是记录基于Redis/Zookeeper实现的简单的自定义分布式锁


文章目录

  • 基于Redis的分布式锁
  • Redis优点
  • 基于redis的分布式锁
  • Redission实现的分布式锁
  • 基于Zookeeper的分布式锁
  • zookeeper作用
  • 基于zookeeper的分布式锁
  • 测试


基于Redis的分布式锁

Redis优点

redis是目前我们开发过程中经常用的缓存数据库,它的优点也十分明显:

  • 性能极高:基于内存的单线程操作,使得它的读写性能极高。
  • 数据类型丰富:除了简单的数据类型之外,还支持list,set,zset,hash等
  • 原子性:单个操作和多个操作(事务)都支持原子性
  • 持久化:可以将数据写入磁盘
  • 可备份:主从模式

基于redis的分布式锁

/**
 * 基于Redis的分布式锁
 * 使用后手动清除或者等待超时失效
 */
@Slf4j
@Component
@ConditionalOnProperty(prefix = "custom.dslock.redis", name = "enable", havingValue = "true")
public class RedisDSLock {

    protected static long INTERNAL_LOCK_LEASE_TIME = 3000;

    //过期时长设置及规则 key不存在时插入
    private static SetParams params = SetParams.setParams().nx().px(INTERNAL_LOCK_LEASE_TIME);

    //jedis配置
    static JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();

    static JedisPool jedisPool ;

    //初始化Jedis
    private static void initJedis(){
        if(null == jedisPool){

            //最大空闲数
            int maxIdle = 50;
            //最大连接数
            int maxTotal = 100;
            //最大等待时长
            int maxWaitMilis = 3000;
           
            String ip = "127.0.0.1";
            int port = 6379;
            
            //读取配置
            try {
                maxIdle =SpringContextUtils.containProperty("custom.dslock.redis.maxIdle",true)?(int) SpringContextUtils.getProperty("custom.dslock.redis.maxIdle"):maxIdle;

                maxTotal = SpringContextUtils.containProperty("custom.dslock.redis.maxTotal",true)?(int) SpringContextUtils.getProperty("custom.dslock.redis.maxTotal"):maxTota;
			    
			    maxWaitMilis = SpringContextUtils.containProperty("custom.dslock.redis.maxWaitMilis",true)?(int) SpringContextUtils.getProperty("custom.dslock.redis.maxWaitMilis"):maxWaitMilis;
			    
                if(SpringContextUtils.containProperty("custom.dslock.redis.ip",true)){
                    ip = SpringContextUtils.getProperty("custom.dslock.redis.ip").toString();
                }else {
                    log.warn("cannot get the property [ custom.dslock.redis.ip ] for redis ip, using default ip [ 127.0.0.1 ] ");
                }
                if(SpringContextUtils.containProperty("custom.dslock.redis.port",true)){
                    port = (int) SpringContextUtils.getProperty("custom.dslock.redis.port");
                }else{
                    log.warn("cannot get the property [ custom.dslock.redis.port ] for redis port, using default port [ 6379 ] ");
                }
            }catch (Exception ex){
                log.error("initJedis error",ex.getMessage());
            }
            //设置最大空闲数
            jedisPoolConfig.setMaxIdle(maxIdle);
            //最大连接数
            jedisPoolConfig.setMaxTotal(maxTotal);
            //最大等待毫秒数
            jedisPoolConfig.setMaxWaitMillis(maxWaitMilis);

            jedisPool = new JedisPool(jedisPoolConfig,ip, port);
        }
    }

    /**
     * 申请锁 
     * @param key  主键
     * @param value 值
     * @return 
     */
    public static boolean lock(String key, String value) {
        initJedis();
        try( Jedis jedis = jedisPool.getResource()){
            //三秒后过期
            String lock = jedis.set(key, value,params);
            if ("OK".equals(lock)) {
                return true;
            }else{
                return false;
            }
        }
    }

    /**
     * 申请并等待锁
     * @param key  主键
     * @param value 值
     * @param waitTime 等待时长 毫秒
     * @return
     */
    public static boolean trylock(String key, String value,long waitTime) {
        initJedis();
        Long start = System.currentTimeMillis();
        for (; ; ) {

            if(System.currentTimeMillis()-start>waitTime){
                log.error("wait for lock out of time");
                return false;
            }

            if(tryLock(key, value))
                return true;
        }
    }

    /**
     * 释放锁
     * @param key   主键
     * @param value 值
     */
    public static boolean unlock(String key, String value) {
        initJedis();
        try(Jedis jedis = jedisPool.getResource()){
            String script =
                    "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                            "   return redis.call('del',KEYS[1]) " +
                            "else" +
                            "   return 0 " +
                            "end";
            try {
                String result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)).toString();
                return "1".equals(result) ? true : false;
            } finally {
                jedis.close();
            }
        }
    }
}

Redission实现的分布式锁

引入依赖

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.8.2</version>
</dependency>

初始化redissionClient(支持单机模式、哨兵模式、集群模式)

//单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379")
			.setPassword("zdm371326")
			.setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
//哨兵模式
//哨兵模式的分布式锁可能会出现脏数据:
//当程序1在master节点获得了锁,并向slave节点同步的时master节点宕机,slave节点成为新的master
//程序2向新的master节点申请锁,这样程序1和2都获得了锁
Config config = new Config();
config.useSentinelServers().addSentinelAddress(
        "redis://127.0.0.1:6379","redis://127.0.0.1:6378", "redis://127.0.0.1:6377")
        .setConnectTimeout(30000)//连接超时时长
        .setReconnectionTimeout(10000)//连接断开后等待连接的时间间隔
        .setTimeout(10000)//等待返回信息的超时时长
        .setRetryAttempts(5)//发送命令的最大尝试次数
        .setRetryInterval(3000)//每次重试的时间间隔
        .setMasterName("redismaster")
        .setPassword("zdm371326").setDatabase(0);
//集群模式
Config config = new Config();
config.useClusterServers().addNodeAddress(
        "redis://127.0.0.1:6379","redis://127.0.0.1:6378", "redis://127.0.0.1:6377")
        .setScanInterval(5000)//设置集群状态扫描时间
        .setMasterConnectionPoolSize(10000)//设置Master节点最大连接数
        .setSlaveConnectionPoolSize(10000)//设置Slave节点最大连接数
        .setReconnectionTimeout(10000)//连接断开后等待连接的时间间隔
        .setTimeout(10000)//等待返回信息的超时时长
        .setRetryAttempts(5)//发送命令的最大尝试次数
        .setRetryInterval(3000)//每次重试的时间间隔
        .setPassword("zdm371326");

redission内部封装好了各种锁,而且均支持支持自动解锁。

  1. 可重入锁
RLock lock = redisson.getLock("lock");
//lock.lock();
//lock.lock(10, TimeUnit.SECONDS);//加锁10秒后自动解锁
//尝试加锁,等待100秒,10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
//lock.lockAsync();//异步加锁
//lock.lockAsync(10, TimeUnit.SECONDS);//异步加锁10秒后自动解锁
//Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
lock.unlock();
  1. 公平锁
RLock fairLock = redisson.getFairLock("lock");
  1. 联锁和红锁
//可以是不同redis实例的锁
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock2 = redisson.getLock("lock3");
//联锁
RedissonMultiLock multiLock= new RedissonMultiLock(lock1,lock2,lock3);
//当lock1 lock2 lock3都获取到时,加锁成功
multiLock.lock();
multiLock.unlock();
//红锁
RedissonRedLock redLock= new RedissonRedLock (lock1,lock2,lock3);
//当获取到大部分锁时,加锁成功
redLock.lock();
redLock.unlock();
  1. 读写锁
//读写锁 一写多读
RReadWriteLock rwLock = redisson.getReadWriteLock("lock");
rwLock.readLock().lock();
rwLock.writeLock().lock();
rwLock.unlock();

基于Zookeeper的分布式锁

zookeeper作用

ZooKeeper 是一个典型的分布式数据一致性解决方案,作为Hadoop项目下的一个子项目,其也是一个相当成熟优秀的应用。分布式应用程序可以基于 ZooKeeper 实现数据发布/订阅、负载均衡、分布式锁、分布式协调/通知、集群管理、Master 选举和分布式队列等功能。

作分布式锁的缺点:
ZK通过对节点的动态新增并判断当前线程所拥有的node编号是不是最小的编号来确定是否获取到锁,因此性能远不如Redis。ZK节点之间的网络异常可能会导致并发现象(可能性很小)。

作分布式锁的优点:
高可用,解决失效导致的死锁。避免某个线程获得锁之后应用宕掉而导致的死锁,客户端断开连接之后,ZK会删除相应节点,这样其他应用可以立即获取锁资源。

基于zookeeper的分布式锁

写一个函数式接口,@FunctionalInterface 注解只允许有一个抽象方法存在。

@FunctionalInterface
public interface ZKDSWorker {
    void todo();
}

分布式锁实现类

@Slf4j
public class ZookeeperDSLock implements MediaDisposer.Disposable {
    private CountDownLatch cdl = new CountDownLatch(1);

    //IP PORT
    private String IP_PORT ;
    //父节点
    private String PARENT_NODE ;
    //连接超时
    private int connectionTimeout;
    //会话超时
    private int sessionTimeout;

    private ZkClient zkClient ;

    //记录紧前节点
    private volatile String beforePath;

    //记录当前节点
    private volatile String path;

    //记录节点目录
    private volatile List<String> children = new ArrayList<>();

    public static class Builder {
        //初始化节点和zk地址
        private String ipPort= "127.0.0.1:2181";
        private String pNode= "/LOCKNODE";
        private int connectionTimeout = 3000;
        private int sessionTimeout = 3000;
        public Builder() {
        }

        public void setIpPort(String ipPort){
            this.ipPort=ipPort;
        }

        public void setPNode(String pNode){
            this.pNode=pNode;
        }

        public void setConnectionTimeout(int connectionTimeout){
            this.connectionTimeout=connectionTimeout;
        }

        public void setSessionTimeout(int sessionTimeout){
            this.sessionTimeout=sessionTimeout;
        }

        public ZookeeperDSLock build(){
            return new ZookeeperDSLock(this);
        }
    }

    private ZookeeperDSLock(@NotNull Builder builder) {
        //初始化信息
        this.IP_PORT=builder.ipPort;
        this.PARENT_NODE=builder.pNode;
        this.connectionTimeout = builder.connectionTimeout;
        this.sessionTimeout = builder.sessionTimeout;

        try {
            zkClient = new ZkClient(IP_PORT, sessionTimeout, connectionTimeout);
        }catch (Exception ex){
            throw new RuntimeException("ZkClient 初始化失败:"+ex.getMessage());
        }

        //父节点不存在则先创建
        if (!zkClient.exists(PARENT_NODE)) {
            try {
                zkClient.createPersistent(PARENT_NODE);
            }catch (Exception ex){
                throw new RuntimeException("ZkClient 父节点创建失败:");
            }
        }
    }

    //获得锁
    public synchronized boolean lock(String lockObj) {
        // 当前的是最小节点就返回加锁成功
        if (getLock(lockObj)) {
            return true;
        } else {
            //删掉当前节点 没有获取锁的话 删除临时节点
            zkClient.deleteRecursive(path);
            return false;
        }
    }
    
    //排队等到锁 执行任务
    public void doOnWaitLock(String lockObj,ZKDSWorker zkdsWorker) {

        if(getLock(lockObj)){
            // 获得锁
            zkdsWorker.todo();
            //释放锁
            this.unlock(lockObj);
        }else{
            //未得到锁 对节点进行监听
            waitForLock(lockObj,zkdsWorker);
        }
    }

    //释放锁
    public void unlock(String lockObj) {
        if (Strings.isBlank(path)) {
            path = zkClient.createEphemeralSequential(PARENT_NODE + "/", lockObj);
        }
        //遍历子节点删除所有子节点后再删除目标目录
        zkClient.deleteRecursive(path);
    }

    //资源释放
    @Override
    public void dispose() {
        zkClient.close();
    }

    //判断是不是可以获得到锁
    private boolean getLock(String lockObj){

        // 创建自己的临时节点
        if (Strings.isBlank(path)) {
            path = zkClient.createEphemeralSequential(PARENT_NODE + "/", lockObj);
        }

        //最小节点可以获得锁
        return isMinNode();
    }

    //判断当前任务创建的节点是不是最小节点
    private boolean isMinNode(){

        // 对节点排序
        children = zkClient.getChildren(PARENT_NODE);
        Collections.sort(children);

        return path.equals(PARENT_NODE + "/" + children.get(0));
    }

    //等待锁
    private void waitForLock(String lockObj,ZKDSWorker zkdsWorker) {

        // 不是最小节点 就找到自己的前一个 依次类推 释放也是一样
        int i = Collections.binarySearch(children, path.substring(PARENT_NODE.length() + 1));
        beforePath = PARENT_NODE + "/" + children.get(i - 1);

        IZkDataListener listener = new IZkDataListener() {
            public void handleDataChange(String s, Object o) throws Exception {
            }

            public void handleDataDeleted(String s) throws Exception {
                cdl.countDown();
            }
        };
        // 监听 监听到变化之后 需要对节点重新建立监听
        this.zkClient.subscribeDataChanges(beforePath, listener);
        if (zkClient.exists(beforePath)) {
            try {
                //等待加锁
                cdl.await();

                //再次判断是不是最小节点
                if(isMinNode()){
                    zkdsWorker.todo();
                    // 最后释放监听
                    zkClient.unsubscribeDataChanges(beforePath, listener);
                    this.unlock(lockObj);
                }else{
                    // 释放监听
                    zkClient.unsubscribeDataChanges(beforePath, listener);
                    //对节点重新进行监听
                    waitForLock(lockObj,zkdsWorker);
                }
            } catch (InterruptedException e) {
                log.error("加锁失败",e);
            }
        }
    }
}

测试

创建多个线程同时加锁

IntStream.rangeClosed(1,10).parallel().boxed().forEach(
                e->{
                    try {
                        testService.zkLock(e);
                    }catch (Exception ex){
                        log.error(ex.getMessage());
                    }
                });

testService.zkLock()实现内容

//1、加锁
ZookeeperDSLock zookeeperDSLock= new ZookeeperDSLock.Builder().build();
if( zookeeperDSLock.lock("testLock")){
    System.out.println(inr+" locked"  );
  	try {
         TimeUnit.MILLISECONDS.sleep(3000);
     }catch (Exception ex){
         log.error(ex.getMessage());
     }
     zookeeperDSLock.unlock("testLock");
}else{
    System.out.println(inr+" lock failed"  );
}

//2、多线程获取锁失败时 等待锁
SimpleDateFormat sf=new SimpleDateFormat("yyyy-dd-MM HH:mm:ss:SSS");
try {
    zookeeperDSLock.doOnWaitLock("locktest",()->{
        String time =sf.format(new Date());
        System.out.println(inr+" "+time+" i am working");
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}catch (Exception ex){
    ex.printStackTrace();
}