欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

分布式锁

程序员文章站 2022-07-05 12:31:02
...

分布式锁产生原因

       为了保证在分布式领域*享数据安全的问题。例如:在服务器集群的环境中,整个生成时间戳代码都是相同的,如果多个服务器在相同的时间内,使用时间戳方式生成全局id,id是相同的,不能保证全局id是唯一。在同一个时间点,只能保证有一台服务器生成全局id,这样就引入了分布式锁的概念。

解决方案

  • 数据库实现分布式锁(不推荐、效率特别低)
  • 基于Redis实现分布式锁setNx(考虑死锁、释放问题)
  • 基于Zookeeper实现分布式锁(强烈推荐)实现相对简单,使用临时节点释放锁(效率最高)、失效时间容易控制

数据库实现分布式锁

       原理很简单,多个服务器生成相同的id,看谁能在数据库中插入成功(id保证唯一),谁就能获得锁,没有成功的,就继续等待。这样的方式不推荐,效率很低,就不过多演示了。

基于Redis实现分布式锁

      原理:setNx也可以存入key,它的特殊点在于如果存入key成功,就返回1,如果已经存在该key值,就返回0。多个客户端,使用setNx命令方式,同事在redis上创建相同的key,因为redis的key不能够重复,只要谁能够创建key成功,谁就能获得锁,没有创建成功的,就会继续等待。

       这样方式有个弊端,有可能造成死锁现象。如果一个客户端创建key成功,但是在执行完操作的时候,没有删除对应的key,那其他的客户端就会一直等待下去。

       解决方案就是对每个对应的key设置自己的有效期,有效期的目的就是为了防止死锁现象。

       在基于Redis实现分布式锁之前,需要熟系Redis三个命令:

  • SETNX

SETNX key val当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

  • Expire

expire key timeout,为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

  • Delete

delete key,删除key。

1、pom文件引入

<dependencies>
	<dependency>
		<groupId>redis.clients</groupId>
		<artifactId>jedis</artifactId>
		<version>2.9.0</version>
	</dependency>
</dependencies>

2、后端相关代码

public class LockRedis {
	// redis线程池
	private JedisPool jedisPool;
	// 同时在redis上创建相同的一个key 相同key 名称
	private String redislockKey = "redis_lock";

	public LockRedis(JedisPool jedisPool) {
		this.jedisPool = jedisPool;
	}
	/**
	 * 获得锁
	 * @param acquireTimeout
	 * 			  在获取锁之前的超时时间
	 *在尝试获取锁的时候,如果在规定的时间内还没有获取锁,直接放弃。
	 * @param timeOut
	 * 			  在获取锁之后的超时时间
	 *当获取锁成功之后,对应的key 有对应有效期,对应的key 在规定时间内进行失效
	 * @return
	 */
	public String getRedisLock(Long acquireTimeout, Long timeOut) {
		Jedis conn = null;
		try {
			// 1.建立redis连接
			conn = jedisPool.getResource();
			// 2.定义 redis 对应key 的value值( uuid) 作用 释放锁 随机生成value
			String identifierValue = UUID.randomUUID().toString();
			// 3.定义在获取锁之后的超时时间
			int expireLock = (int) (timeOut / 1000);// 以秒为单位
			// 4.定义在获取锁之前的超时时间
			// 5.使用循环机制 如果没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁(乐观锁)
			// 使用循环方式重试的获取锁
			Long endTime = System.currentTimeMillis() + acquireTimeout;
			while (System.currentTimeMillis() < endTime) {
				// 获取锁
				// 6.使用setnx命令插入对应的redislockKey ,如果返回为1 成功获取锁
				if (conn.setnx(redislockKey, identifierValue) == 1) {
					// 设置对应key的有效期
					conn.expire(redislockKey, expireLock);
					return identifierValue;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (conn != null) {
				conn.close();
			}
		}
		return null;
	}

	/**
	 *释放redis锁
	 *释放锁有两种 key自动有有效期
	 * 整个程序执行完毕情况下,删除对应key
	 * 如果直接使用 conn.del(redislockKey); 保证对应是自己的创建redislockKey 删除对应自己的
	 * @param identifierValue
	 */
	public void unRedisLock(String identifierValue) {
		Jedis conn = null;
		// 1.建立redis连接
		conn = jedisPool.getResource();
		try {
			// 如果该锁的id 等于identifierValue 是同一把锁情况才可以删除
			if (conn.get(redislockKey).equals(identifierValue)) {
				System.out.println("释放锁..." + Thread.currentThread().getName() + ",identifierValue:" + identifierValue);
				conn.del(redislockKey);
			}
		} catch (Exception e){
		} finally {
			if (conn != null) {
				conn.close();
			}
		}
	}
}

基于Zookeeper实现分布式锁

1、pom文件引入

<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

2、后端相关代码

public class ZKLock {
    // 集群连接地址
    protected String CONNECTION = "127.0.0.1:2181";
    // zk客户端连接
    protected ZkClient zkClient = new ZkClient(CONNECTION);
    // path路径
    protected String lockPath = "/path";
    //信号量
    protected CountDownLatch countDownLatch = new CountDownLatch(1);

    //获得锁
    public void getLock() {
        //尝试获取锁
        boolean isGetLock = tryLock();
        if(isGetLock){
            System.out.println("####获取锁成功######");
        }else {
            //进入等待
            waitLock();
            //重新获取锁
            getLock();
        }
    }

    // 尝试获取锁
    private boolean tryLock(){
        try {
            zkClient.createEphemeral(lockPath);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    //进入等待
    private void waitLock() {
        //使用zk临时事件监听
        IZkDataListener iZkDataListener = new IZkDataListener() {
            //删除节点通知
            public void handleDataDeleted(String path) throws Exception {
                if (countDownLatch != null) {
                    countDownLatch.countDown();
                }
            }
            //更改节点通知
            public void handleDataChange(String arg0, Object arg1) throws Exception {

            }
        };
        // 注册事件通知
        zkClient.subscribeDataChanges(lockPath, iZkDataListener);
        if (zkClient.exists(lockPath)) {
            countDownLatch = new CountDownLatch(1);
            try {
                //只要计数器不为0,就一直等待
                countDownLatch.await();
            } catch (Exception e) {
                // TODO: handle exception
            }
        }
        // 监听完毕后,移除事件通知
        zkClient.unsubscribeDataChanges(lockPath, iZkDataListener);
    }

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

三种分布式锁的对比

  • 从实现的复杂性角度(从低到高)

       Zookeeper >= 缓存 > 数据库

  • 从性能角度(从高到低)

       缓存 > Zookeeper >= 数据库

  • 从可靠性角度(从高到低)

      Zookeeper > 缓存 > 数据库