基于redis的 / 基于zookeeper的分布式锁方案

分布式系统中,要保证某个资源在一时间段内只能有一个进程访问,需要使用分布式锁。

方案一:基于redis的分布式锁

我们首先介绍三个使用到的redis方法SETNX()、GET()、GETSET()。

setnx ( key, value ):SET if Not Exists,该方法是原子的。若 key 不存在,则设置当前 key 成功,返回 1;若 key 已存在,则设置当前 key 失败,返回 0。

getset ( key,newValue ):该方法是原子的,对 key 设置 newValue 新值,返回 key 原来的旧值。若原来无值,则返回null。

使用步骤

1、setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2

2、get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3

3、计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) ,返回当前 lockkey 的(旧)值currentExpireTime。
判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,则当前 getset 设置成功,获取到了锁;如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

4、在获取到锁之后,当前线程可以开始自己的业务处理,处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,无需处理。

//redis分布式锁
public final class RedisLockUtil {

	private static final int defaultExpire = 60;

	private RedisLockUtil() {
		//
	}

	public static boolean lock(String key) {
		return lock2(key, defaultExpire);
	}

	/**
	 * 加锁
	 * 
	 * @param key    redis key
	 * @param expire 过期时间,单位秒
	 * @return true:加锁成功,false,加锁失败
	 */
	public static boolean lock2(String key, int expire) {

		RedisService redisService = SpringUtils.getBean(RedisService.class);

		long value = System.currentTimeMillis() + expire;
		long status = redisService.setnx(key, String.valueOf(value));

		if (status == 1) {
			return true;
		}
		// 获取旧值
		long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
		if (oldExpireTime < System.currentTimeMillis()) {
			// 超时
			long newExpireTime = System.currentTimeMillis() + expire;
			// 当前旧值
			long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));
			if (currentExpireTime == oldExpireTime) {
				return true;
			}
		}
		return false;
	}

	public static void unLock2(String key) {
		RedisService redisService = SpringUtils.getBean(RedisService.class);
		long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
		if (oldExpireTime > System.currentTimeMillis()) {
			redisService.del(key);
		}
	}

	
	
	
	//——————————————附:使用SETNX()、EXPIRE()做分布式锁——————————————————
	// 此种方案不完善。比如在expire()命令执行成功前,发生了宕机的现象,那就会出现死锁的问题
	/**
	 * 加锁
	 * 
	 * @param key    redis key
	 * @param expire 过期时间,单位秒
	 * @return true:加锁成功,false,加锁失败
	 */
	public static boolean lock(String key, int expire) {

		RedisService redisService = SpringUtils.getBean(RedisService.class);
		long status = redisService.setnx(key, "1");

		if (status == 1) {
			redisService.expire(key, expire);
			return true;
		}

		return false;
	}

	public static void unLock(String key) {
		RedisService redisService = SpringUtils.getBean(RedisService.class);
		redisService.del(key);
	}
}

使用锁:

public void drawRedPacket(long userId) {
    String key = "draw.redpacket.userid:" + userId;

    boolean lock = RedisLockUtil.lock2(key, 50);
    if(lock) {
        try {
            //领取操作
        } finally {
            //释放锁
            RedisLockUtil.unLock2(key);
        }
    } else {
        new RuntimeException("重复领取奖励");
    }
}

优点: 性能高

缺点:失效时间设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。

方案二:基于zookeeper的分布式锁

1.在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。
2.判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
3.当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
4.取锁成功则执行代码,最后释放锁(删除该节点)。

public class DistributedLock implements Lock, Watcher {
	private ZooKeeper zk;
	private String root = "/locks";// 根
	private String lockName;// 竞争资源的标志
	private String waitNode;// 等待前一个锁
	private String myZnode;// 当前锁
	private CountDownLatch latch;// 计数器
	private int sessionTimeout = 30000;
	private List<Exception> exception = new ArrayList<Exception>();

	/**
	 * 创建分布式锁,使用前请确认config配置的zookeeper服务可用
	 * 
	 * @param config   127.0.0.1:2181
	 * @param lockName 竞争资源标志,lockName中不能包含单词lock
	 */
	public DistributedLock(String config, String lockName) {
		this.lockName = lockName;
		// 创建一个与服务器的连接
		try {
			zk = new ZooKeeper(config, sessionTimeout, this);
			Stat stat = zk.exists(root, false);
			if (stat == null) {
				// 创建根节点
				zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
			}
		} catch (IOException e) {
			exception.add(e);
		} catch (KeeperException e) {
			exception.add(e);
		} catch (InterruptedException e) {
			exception.add(e);
		}
	}

	/**
	 * zookeeper节点的监视器
	 */
	public void process(WatchedEvent event) {
		if (this.latch != null) {
			this.latch.countDown();
		}
	}

	public void lock() {
		if (exception.size() > 0) {
			throw new LockException(exception.get(0));
		}
		try {
			if (this.tryLock()) {
				System.out.println("Thread-" + Thread.currentThread().getName() + " " + myZnode + " get lock true");
				return;
			} else {
				waitForLock(waitNode, sessionTimeout);// 等待锁
			}
		} catch (KeeperException e) {
			throw new LockException(e);
		} catch (InterruptedException e) {
			throw new LockException(e);
		}
	}

	public boolean tryLock() {
		try {
			String splitStr = "_lock_";
			if (lockName.contains(splitStr))
				throw new LockException("lockName can not contains \\u000B");
			// 创建临时子节点
			myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
					CreateMode.EPHEMERAL_SEQUENTIAL);
			System.out.println(myZnode + " is created ");
			// 取出所有子节点
			List<String> subNodes = zk.getChildren(root, false);
			// 取出所有lockName的锁
			List<String> lockObjNodes = new ArrayList<String>();
			for (String node : subNodes) {
				String _node = node.split(splitStr)[0];
				if (_node.equals(lockName)) {
					lockObjNodes.add(node);
				}
			}

			Collections.sort(lockObjNodes);

			if (myZnode.equals(root + "/" + lockObjNodes.get(0))) {
				// 如果是最小的节点,则表示取得锁
				return true;
			}
			// 如果不是最小的节点,找到比自己小1的节点
			String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
			System.out.println("subMyZnode=" + subMyZnode);
			waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
			System.out.println("waitNode=" + waitNode);
		} catch (KeeperException e) {
			throw new LockException(e);
		} catch (InterruptedException e) {
			throw new LockException(e);
		}
		return false;
	}

	public boolean tryLock(long time, TimeUnit unit) {
		try {
			if (this.tryLock()) {
				return true;
			}
			return waitForLock(waitNode, time);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

	private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
		// 判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
		Stat stat = zk.exists(root + "/" + lower, true);
		if (stat != null) {
			System.out.println("Thread-" + Thread.currentThread().getName() + " waiting for " + root + "/" + lower);
			this.latch = new CountDownLatch(1);
			this.latch.await(waitTime, TimeUnit.MILLISECONDS);
			this.latch = null;
		}
		return true;
	}

	public void unlock() {
		try {
			System.out.println(Thread.currentThread().getName() + " unlock " + myZnode);
			zk.delete(myZnode, -1);
			myZnode = null;
			zk.close();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (KeeperException e) {
			e.printStackTrace();
		}
	}

	public void lockInterruptibly() throws InterruptedException {
		this.lock();
	}

	public Condition newCondition() {
		return null;
	}

	public class LockException extends RuntimeException {
		private static final long serialVersionUID = 1L;

		public LockException(String e) {
			super(e);
		}

		public LockException(Exception e) {
			super(e);
		}
	}

	public static void main(String[] args) {

		final DistributedLock client1 = new DistributedLock("127.0.0.1", "lName");
		final DistributedLock client2 = new DistributedLock("127.0.0.1", "lName");

		new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				client1.lock();
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				client1.unlock();

			}
		}, "t1").start();

		// 确保t1启动
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}

		new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				client2.lock();
				System.out.println("do something");
				client2.unlock();
			}
		}, "t2").start();
	}

}

优点:
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

缺点:
性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。还需要对 ZK的原理有所了解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章