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();
}
}
});
}
}
}