分布式锁与 MySQL/Redis/Zookeeper 实现方案

问题来源

在一个 JVM 里我们要用锁,可以用 synchronized 和 Lock。如果是在多个 JVM 里,该怎么办呢?

解决思路

MySQL 里,对于同一数据库的同一张表,不能插入相同的数据。往数据库里插入数据可以当做上锁的过程。
Redis 里,setnx 可以仅在 key 不存在时插入数据。setnx 操作可以当做上锁的过程。
Zookeeper 也一样,同一 Znode 不能创建两次。创建 Znode 的过程可以当做上锁的过程。

方案比较

在这里插入图片描述

模板方法

在这里插入图片描述

public interface Lock {
    //获取到锁的资源
    void getLock();
    // 释放锁
    void unLock();
}
public abstract class AbstractLock  implements  Lock{

    public void getLock() {
    	//尝试获取
        if (tryLock()) {
        	//获取成功的操作
            System.out.println("##获取lock锁的资源####");
            //获取失败
        } else {
        	//等待一段时间
            waitLock();
            //再次获取
            getLock();
        }
    }

    public abstract boolean tryLock();

    public abstract void waitLock();
}

MySQL 方案

import javax.annotation.Resource;

public  class MysqlLock extends AbstractLock {
	
	@Resource
	private LockMapper mapper;
	
	//所有的线程都往数据库插入主键值相同的数据
	private static final int LOCK_ID = 1;

	//非阻塞式加锁
	public boolean tryLock() {
		try {
			mapper.insert(LOCK_ID);
		} catch (Exception e) {
			return false;
		}
		return true;
	}

	//让当前线程休眠一段时间
	public void waitLock() {
		try {
			Thread.currentThread().sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void unLock() {
		mapper.deleteByPrimaryKey(LOCK_ID);
	}
}
package com.mysql.mapper;

public interface LockMapper {

    //删除数据解锁
    int deleteByPrimaryKey(int id);

    //新增数据加锁,id为同一个值
    int insert(int id);

}

LockMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mysql.mapper.LockMapper" >
  <delete id="deleteByPrimaryKey" parameterType="INTEGER" >
    delete from t_lock
    where id = #{code,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="INTEGER" >
    insert into t_lock (id)
    values (#{id,jdbcType=INTEGER})
  </insert>
</mapper>

Redis 方案

public class RedisLock extends AbstractLock {

    private static Jedis getJedis(){
        Jedis jedis = new Jedis("192.168.100.14");
        jedis.auth("12345678");
        return jedis;
    }

    private static final String  KEY = "LOCK_KEY";

    private ThreadLocal<String> local = new ThreadLocal();

    //阻塞式加锁,使用setNx命令返回OK的加锁成功,并生产随机值
    public boolean tryLock() {
        //产生随机值,标识本次锁编号
        String uuid = UUID.randomUUID().toString();

        /**
         * key:我们使用key来当锁
         * uuid:唯一标识,这个锁是我加的,属于我
         * NX:设入模式【SET_IF_NOT_EXIST】--仅当key不存在时,本语句的值才设入
         * PX:给key加有效期
         * 1000:有效时间为 1 秒
         */
        Jedis jedis = getJedis();
        String ret = jedis.set(KEY, uuid,"NX","PX",1000);

        //设值成功--抢到了锁
        if("OK".equals(ret)){
            local.set(uuid);//抢锁成功,把锁标识号记录入本线程--- Threadlocal
            return true;
        }

        //key值里面有了,我的uuid未能设入进去,抢锁失败
        return false;
    }

    public void waitLock() {
        try {
            Thread.currentThread().sleep(10);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }

    public void unLock() {
        //读取lua脚本
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then \n" +
                "    return redis.call(\"del\",KEYS[1]) \n" +
                "else \n" +
                "    return 0 \n" +
                "end";
        //通过原始连接连接redis执行lua脚本
        Jedis jedis = getJedis();
        jedis.eval(script, Arrays.asList(KEY), Arrays.asList(local.get()));
    }
}

Zookeeper 方案

public abstract class ZookeeperAbstractLock extends AbstractLock {
    // zk连接地址
    private static final String CONNECTSTRING = "192.168.100.14:2181,192.168.100.15:2181,192.168.100.16:2181";
    // 创建zk连接
    protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
    protected static final String PATH = "/lock";
    protected static final String PATH2 = "/lock2";
}

方案一

在这里插入图片描述

public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
    private CountDownLatch countDownLatch = null;

    @Override
    //尝试获得锁
    public  boolean tryLock() {
        try {
            zkClient.createEphemeral(PATH);
            return true;
        } catch (Exception e) {
            //如果创建失败报出异常
            return false;
        }
    }

    @Override
    public void waitLock() {
        IZkDataListener izkDataListener = new IZkDataListener() {

            public void handleDataDeleted(String path) throws Exception {
                // 唤醒被等待的线程
                if (countDownLatch != null) {
                    countDownLatch.countDown();
                }
            }
            public void handleDataChange(String path, Object data) throws Exception {

            }
        };
        // 注册事件
        zkClient.subscribeDataChanges(PATH, izkDataListener);

        //如果节点存
        if (zkClient.exists(PATH)) {
            countDownLatch = new CountDownLatch(1);
            try {
                //等待,一直等到接受到事件通知
                countDownLatch.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 删除监听
        zkClient.unsubscribeDataChanges(PATH, izkDataListener);
    }

    public void unLock() {
        //释放锁
        if (zkClient != null) {
            zkClient.delete(PATH);
            zkClient.close();
            System.out.println("释放锁资源...");
        }
    }
}

方案二

方案一会出现羊群效应:锁释放资源的时候,会出现多个线程同时竞争锁,造成不必要的消耗。
我们可以改进一下:创建零时顺序节点,按序号来获取锁,这样就不会造成哄抢。
在这里插入图片描述

public class ZookeeperDistributeLock2 extends ZookeeperAbstractLock {
    private CountDownLatch countDownLatch= null;

    private String beforePath;//当前请求的节点前一个节点
    private String currentPath;//当前请求的节点

    public ZookeeperDistributeLock2() {
        if (!this.zkClient.exists(PATH2)) {
            this.zkClient.createPersistent(PATH2);
        }
    }


    @Override
    public boolean  tryLock() {
        //如果currentPath为空则为第一次尝试加锁,第一次加锁赋值currentPath
        if(currentPath == null || currentPath.length()<= 0){
            //创建一个临时顺序节点
            currentPath = this.zkClient.createEphemeralSequential(PATH2 + '/',"lock");
        }
        //获取所有临时节点并排序,临时节点名称为自增长的字符串如:0000000400
        List<String> childrens = this.zkClient.getChildren(PATH2);
        Collections.sort(childrens);

        if (currentPath.equals(PATH2 + '/'+childrens.get(0))) {//如果当前节点在所有节点中排名第一则获取锁成功
            return true;
        } else {//如果当前节点在所有节点中排名中不是排名第一,则获取前面的节点名称,并赋值给beforePath
            int wz = Collections.binarySearch(childrens,
                    currentPath.substring(7));
            beforePath = PATH2 + '/'+childrens.get(wz-1);
        }
        return false;

    }

    @Override
    public void waitLock() {
        IZkDataListener listener = new IZkDataListener() {

            public void handleDataDeleted(String dataPath) throws Exception {

                if(countDownLatch!=null){
                    countDownLatch.countDown();
                }
            }

            public void handleDataChange(String dataPath, Object data) throws Exception {

            }
        };
        //给排在前面的的节点增加数据删除的watcher,本质是启动另外一个线程去监听前置节点
        this.zkClient.subscribeDataChanges(beforePath, listener);

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


    public void unLock() {
        //删除当前临时节点
        zkClient.delete(currentPath);
        zkClient.close();
    }
}

相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>enjoy</groupId>
    <artifactId>lock</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.10</version>
        </dependency>
		<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.8.1</version>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.18</version>
        </dependency>

        <!-- 数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.20</version>
        </dependency>

        <!-- mybatis相关依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!-- mybatis与spring对接依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</version>
        </dependency>

        <!-- spring核心依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>4.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.3.RELEASE</version>
        </dependency>
        <!-- spring jdbc和事务的支持 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.3.3.RELEASE</version>
        </dependency>



        <!-- 单元测试相关依赖 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>4.3.2.RELEASE</version>
        </dependency>

         <!--日志相关依赖-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.10</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.2</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.1.2</version>
        </dependency>


    </dependencies>
</project>

参考:deer——Zookeeper

发布了130 篇原创文章 · 获赞 233 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_44367006/article/details/102950407