Redis和ZooKeeper的分布式锁实现

github地址: Redis和ZooKeeper对于分布式锁的实现

Redis分布式锁

客户端在读写redis之前必须先从redis获取锁, 只有获取到锁的客户端才能读写redis, 而其他没有获取到锁的客户端, 会以每秒一次的频率不断地去尝试获取锁.

(1) 获取锁

SET my_lock 随机值 PX 5000 NX

PX是设置过期时间, 单位毫秒. NX是仅当key不存在时才设置值.

(2) 删除锁

只有提供的value值相同才能删除锁, 因为我们不能让客户端删除别人的锁. 因为涉及到条件判断, 为了保证事务特性, 必须使用Lua脚本.

if redis.call("get",KEYS[1]) == ARGV[1] then
	return redis.call("del", KEYS[1])
else
    return 0
end

(3) Java实例

public class RedisDistributeLock {
	private static Logger logger = LoggerFactory.getLogger(RedisDistributeLock.class);

	private Jedis jedis;

	public RedisDistributeLock(String host) {
		JedisPool jedisPool = new JedisPool(host, 6379);
		jedis = jedisPool.getResource();
	}

	/**
	 * 获取锁
	 * @param key
	 * @param value
	 * @param expireTime 过期时间, 单位毫秒
	 */
	public void getLock(String key, String value, long expireTime) {
		try {
			SetParams params = new SetParams();
			params.px(expireTime);
			params.nx();
			while (true) {
				// 旧版本的Jedis使用命令: String result = jedis.set(key, value, "NX", "PX", 100);
				String result = jedis.set(key, value, params);
				if ("OK".equals(result)) {
					return;
				}
				Thread.sleep(100L);
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 释放锁
	 * @param key
	 * @param value
	 */
	public void releaseLock(String key, String value) {
		String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
	}

	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 5; i++) {
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					RedisDistributeLock redisDistributeLock = new RedisDistributeLock("xxx.xx.xx.xxx");

					//获取锁, 没有获取到锁就继续尝试获取锁
					String key = "my_lock";
					String value = UUID.randomUUID().toString();
					redisDistributeLock.getLock(key, value, 200L);
					try {
						logger.info(Thread.currentThread().getName() + " 进行扣减库存操作...");
					} catch (Exception e) {
						logger.error("处理业务逻辑报错", e);
					}finally {
						//释放锁
						redisDistributeLock.releaseLock(key, value);
					}
				}
			});
		}
	}
}

ZooKeeper分布式锁

(1) 使用临时节点实现

  • 所有客户端都去/exclusive_lock节点下创建临时子节点/exclusive_lock/myLock.
  • 只有一个客户端能创建成功, 表示该客户端拿到了锁.
  • 其他没有创建成功的客户端在/exclusive_lock/myLock节点上注册一个监听器
  • 当获取到锁的客户端宕机或正常完成业务逻辑后, 临时节点/exclusive_lock/myLock会被删除.
  • 其他客户端都会收到通知, 重新去创建临时节点/exclusive_lock/myLock.

这种实现有两个问题: 羊群效应和锁公平性问题, 即每次当临时节点被删除后, 其他客户端都会去获取锁, 且上一次获取锁的顺序无效.

(2) 使用临时顺序节点实现

我们可以ZooKeeper的临时顺序节点来解决上面的两个问题.

  • 所有客户端都去/exclusive_lock节点下创建临时顺序子节点/exclusive_lock/myLock.
  • 然后再对这些临时顺序节点按字典序进行排序.
  • 排在第一个的临时顺序节点对应的客户端获取到锁.
  • 其他客户端在排自己前面的临时顺序节点上注册一个监听器.
  • 当获取到锁的客户端的释放锁之后, ZK会通过监听器通知下一个临时顺序节点对应的客户端获取到锁.

下面我们来看下具体实现:

public class ZkDistributeLock {
	private static Logger logger = LoggerFactory.getLogger(ZkDistributeLock.class);

	/**
	 * 分布式锁的根节点路径
	 */
	private String rootLockPath = "/exclusive_lock";
	/**
	 * 分布式锁节点路径
	 */
	private String lockPath;
	/**
	 * 分布式锁名
	 */
	private String lockName;
	private ZooKeeper zk;

	/**
	 * 连接zk, 并创建分布式锁的根节点
	 * @param host zk服务地址
	 * @param lockName 分布式锁名
	 */
	public ZkDistributeLock(String host, String lockName) {
		try {
			CountDownLatch connectedSignal = new CountDownLatch(1);
			zk = new ZooKeeper(host, 5000, new Watcher() {
				@Override
				public void process(WatchedEvent event) {
					if (event.getState() == Event.KeeperState.SyncConnected) {
						connectedSignal.countDown();
					}
				}
			});
			//因为监听器是异步操作, 要保证监听器操作先完成, 即要确保先连接上ZooKeeper再返回实例.
			connectedSignal.await();

			//创建锁的根节点(持久节点)
			if (zk.exists(rootLockPath, false) == null) {
				zk.create(rootLockPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
			}

			//指定分布式锁节点路径
			this.lockName = lockName;
		} catch (Exception e) {
			logger.error("connect zookeeper server error.", e);
		}
	}

	/**
	 * 获取锁
	 * 在业务中获取到锁后才能继续往下执行, 否则堵塞, 直到获取到锁
	 */
	public void getLock() {
		try {
			//创建分布式锁的临时顺序节点
			lockPath = zk.create(rootLockPath + "/" + lockName, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

			//取出所有分布式锁的临时顺序节点, 然后排序
			List<String> children = zk.getChildren(rootLockPath, false);
			TreeSet<String> sortedChildren = new TreeSet<>();
			for (String child : children) {
				sortedChildren.add(rootLockPath + "/" + child);
			}

			//如果当前客户端创建的顺序节点是第一个, 则获取到锁
			String firstNode = sortedChildren.first();
			if (firstNode.equals(lockPath)) {
				return;
			}

			//如果当前客户端没有获取到锁, 则在前一个临时顺序节点上加一个监听器
			String lowerNode = sortedChildren.lower(lockPath);
			CountDownLatch latch = new CountDownLatch(1);
			if (StringUtils.isBlank(lowerNode)) {
				return;
			}
			Stat stat = zk.exists(lowerNode, new Watcher() {
				@Override
				public void process(WatchedEvent event) {
					//当前一个临时顺序节点被删除后, 当前客户端就获取到锁(这样就保证了锁的公平性)
					if (event.getType() == Event.EventType.NodeDeleted) {
						latch.countDown();
					}
				}
			});
			if (stat != null) {
				latch.await();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 释放锁
	 */
	public void releaseLock() {
		try {
			zk.delete(lockPath, -1);
		} catch (Exception e) {
			logger.error("release lock error.", e);
		}
	}

	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 5; i++) {
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					ZkDistributeLock zkDistributeLock = new ZkDistributeLock("xxx.xx.xx.xxx:2181", "myLock");

					//获取锁, 没有获取到锁就一直等待
					zkDistributeLock.getLock();
					try {
						logger.info(Thread.currentThread().getName() + " 进行扣减库存操作...");
					} catch (Exception e) {
						logger.error("处理业务逻辑报错", e);
					}finally {
						//释放锁
						zkDistributeLock.releaseLock();
					}
				}
			});
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值