目录
什么是分布式锁?
基于数据库设计思路
基于Zookeeper设计思路一(非公平锁)
基于Zookeeper设计思路二(公平锁)
Curator可重入分布式锁
Curator可重入分布式锁工作流程
什么是分布式锁?
在单体的应用开发场景中涉及并发同步的时候,大家往往采用Synchronized(同步)或者其他同一个JVM内Lock机制来解决多线程间的同步问题。在分布式集群工作的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题,这种跨机器的锁就是分布式锁。目前分布式锁,比较成熟、主流的方案:
1. 基于数据库的分布式锁。这种方案使用数据库的事务和锁机制来实现分布式锁。虽然在某些场景下可以实现简单的分布式锁,但由于数据库操作的性能相对较低,并且可能面临锁表的风险,所以一般不是首选方案。
2. 基于Redis的分布式锁。Redis分布式锁是一种常见且成熟的方案,适用于高并发、性能要求高且可靠性问题可以通过其他方案弥补的场景。Redis提供了高效的内存存储和原子操作,可以快速获取和释放锁。它在大规模的分布式系统中得到广泛应用。
3. 基于ZooKeeper的分布式锁。这种方案适用于对高可靠性和一致性要求较高,而并发量不是太高的场景。由于ZooKeeper的选举机制和强一致性保证,它可以处理更复杂的分布式锁场景,但相对于Redis而言,性能可能较低。
基于数据库设计思路
可以利用数据库的唯一索引来实现,唯一索引天然具有排他性。
基于Zookeeper设计思路一(非公平锁)
使用临时znode来表示获取锁的请求,创建znode成功的用户拿到锁。
代码实现(后续代码实现均使用此依赖)
<!-- zkclient -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
<!-- zookeeper client -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.0</version>
</dependency>
<!--curator-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
public interface Lock {
void lock();
void unlock();
}
public class DistributedLockByEPHEMERAL implements Lock {
private static final String connectString = "localhost:2181";
private static final int sessionTimeout = 5000;
private static final String LOCK_PATH = "/lock";
private ZooKeeper zooKeeper;
private CountDownLatch lockAcquiredSignal = new CountDownLatch(1);
public DistributedLockByEPHEMERAL() {
try {
CountDownLatch connectLatch = new CountDownLatch(1);
zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 连接建立时, 打开latch, 唤醒wait在该latch上的线程
if (event.getState() == Event.KeeperState.SyncConnected) {
connectLatch.countDown();
}
// 发生了waitPath的删除事件
if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(LOCK_PATH)) {
lockAcquiredSignal.countDown();
}
}
});
// 等待连接建立
connectLatch.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public boolean tryLock() {
try {
// 创建临时节点/lock
zooKeeper.create(LOCK_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
}catch (Exception e){
//节点已经存在,创建失败
return false;
}
return true;
}
public void waitLock() {
try {
//判断是否存在,监听节点
Stat stat = zooKeeper.exists(LOCK_PATH, true);
if(null != stat){
lockAcquiredSignal.await();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void lock() {
//使用trylock方法加锁
}
@Override
public void unlock() {
try {
//删除临时节点
zooKeeper.delete(LOCK_PATH, -1);
System.out.println("-------释放锁------");
} catch (Exception e) {
}
}
}
如果所有的锁请求者都watch锁持有者,当代表锁持有者的znode被删除以后,所有的锁请求者都会通知到,但是只有一个锁请求者能拿到锁。
基于Zookeeper设计思路二(公平锁)
使用临时有序znode来表示获取锁的请求,创建最小后缀数字znode的用户成功拿到锁。
代码实现
public interface Lock {
void lock();
void unlock();
}
public class DistributedLock implements Lock {
// zookeeper server列表
private String connectString = "localhost:2181";
// 超时时间
private int sessionTimeout = 5000;
private ZooKeeper zk;
private String rootNode = "locks";
private String subNode = "seq-";
// 当前client等待的子节点
private String waitPath;
//ZooKeeper连接
private CountDownLatch connectLatch = new CountDownLatch(1);
//ZooKeeper节点等待
private CountDownLatch waitLatch = new CountDownLatch(1);
// 当前client创建的子节点
private String currentNode;
// 和zk服务建立连接,并创建根节点
public DistributedLock() {
try {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 连接建立时, 打开latch, 唤醒wait在该latch上的线程
if (event.getState() == Event.KeeperState.SyncConnected) {
connectLatch.countDown();
}
// 发生了waitPath的删除事件
if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
waitLatch.countDown();
}
}
});
// 等待连接建立
connectLatch.await();
//获取根节点状态
Stat stat = zk.exists("/" + rootNode, false);
//如果根节点不存在,则创建根节点,根节点类型为永久节点
if (stat == null) {
System.out.println("根节点不存在");
zk.create("/" + rootNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 加锁方法
@Override
public void lock() {
try {
//在根节点下创建临时顺序节点,返回值为创建的节点路径
currentNode = zk.create("/" + rootNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// wait一小会, 让结果更清晰一些
Thread.sleep(50);
// 注意, 没有必要监听"/locks"的子节点的变化情况
List<String> childrenNodes = zk.getChildren("/" + rootNode, false);
// 列表中只有一个子节点, 那肯定就是currentNode , 说明client获得锁
if (childrenNodes.size() == 1) {
return;
} else {
//对根节点下的所有临时顺序节点进行从小到大排序
Collections.sort(childrenNodes);
//当前节点名称
String thisNode = currentNode.substring(("/" + rootNode + "/").length());
//获取当前节点的位置
int index = childrenNodes.indexOf(thisNode);
if (index == -1) {
System.out.println("数据异常");
} else if (index == 0) {
// index == 0, 说明thisNode在列表中最小, 当前client获得锁
return;
} else {
// 获得排名比currentNode 前1位的节点
this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
// 在waitPath上注册监听器, 当waitPath被删除时, zookeeper会回调监听器的process方法
zk.getData(waitPath, true, new Stat());
//进入等待锁状态
waitLatch.await();
return;
}
}
} catch (Exception e) {
}
}
// 解锁方法
@Override
public void unlock() {
try {
zk.delete(this.currentNode, -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}
Curator可重入分布式锁
场景:分布式系统下,多个系统获取订单号。
public class OrderCodeGenerator {
private static int count = 0;
/**
* 生成订单号
*/
public String getOrderCode(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddhhmmss");
return simpleDateFormat.format(new Date()) + "-" + ++count;
}
}
public class CuratorLockTest implements Runnable{
final static CuratorFramework client= CuratorFrameworkFactory.builder().connectString("localhost:2181")
.retryPolicy(new ExponentialBackoffRetry(100,1)).build();
private OrderCodeGenerator orderCodeGenerator = new OrderCodeGenerator();
//可重入互斥锁
final InterProcessMutex lock=new InterProcessMutex(client,"/curator_lock");
public static void main(String[] args) throws InterruptedException {
client.start();
for(int i=0;i<30;i++){
new Thread(new CuratorLockTest()).start();
}
Thread.currentThread().join();
}
@Override
public void run() {
try {
// 加锁
lock.acquire();
String orderCode = orderCodeGenerator.getOrderCode();
System.out.println("生成订单号 "+orderCode);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
// 释放锁
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Curator可重入分布式锁工作流程
在高性能、高并发的应用场景下,不建议使用ZooKeeper的分布式锁。而由于ZooKeeper的高可靠性,因此在并发量不是太高的应用场景中,还是推荐使用ZooKeeper的分布式锁。