一、实现分布式锁
1、作用
分布式锁可以应用于分布式中选举 leader,即拥有锁的就是leader,也可以用于分布式中并发控制(如支付业务或下单业务等保证一致性),分布式系统中当使用TCC事务模型时,没有数据库资源锁的支撑,需要在业务层进行事务隔离,此时可以用zookeepe实现,获得锁的即可操作资源。常用的另外还有另外两种 基于数据库的锁、基于redis的锁,这里且不详述。
2、思路
- 保持独占
- 每个客户端都尝试创建同一个 znode,但只有一个可以成功创建,创建成功的客户端即获取到锁
- 当该客户端处理完业务删除该节点,释放锁
- 其他客户端观察 znode的删除,又回到刚开始一样,抢占创建同个 znode,最终获取锁
2.控制时序(引用石杉哥的一张图)
- 给需要得到锁的客户端各自创建一个临时有序的顺序znode,作为子节点,即顺序号最小的客户端便持有锁(之所以要临时是防止出现客户端宕机而该节点没有删除,一直持有锁又没有用,像废弃的节点一直拿锁但其他节点一直得不到锁形成了死锁)
- 当删除了前面顺序号小的znode节点,则释放锁,锁将留给后续排队的节点
- 客户端观察znode删除,判断自己是不是列表中序号最小,是则获得锁,不是则继续监听
3、问题
- 羊群效应:如果每个客户端都监听锁的释放,当锁释放时每个客户端都收到通知导致服务器压力大,解决办法是只需要通知排序号在后面一个的客户端
- 连接丢失:当连接丢失后哪些持久化的节点怎么重新匹配上子节点,解决办法是在znode名称嵌入一个sessionId,通过id匹配
4、自带的客户端实现方式(控制时序)
如果出现同个ip限制连接问题,可到zoo.cfg文件修改 maxClientCnxns 字段
如果是Error while calling watcher问题,可能是new Zookeeper() 时没有把watcher注册进去
- 引入依赖
<!-- 官方zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
<!--Apache Curator-->
<!--<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.9.0</version>
</dependency>-->
<!-- zkclient -->
<!--<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.9</version>
</dependency>-->
- 我们创建 ZookeeperLock 类,里面包括方法有节点创建、获取锁、释放锁、监听锁等
public class ZookeeperLock {
private ZooKeeper zooKeeper;
private final static String IP = "192.168.1.60:2181";
private final static int TIME_OUT = 5000;
private final static String ROOT_PATH = "/lock";
private final static String ROOT_LOCK_PATH = "/Lock";
private String lock = null;
public ZookeeperLock() throws Exception{
zooKeeper = new ZooKeeper(IP, TIME_OUT, watcher);
}
/**
* 节点创建
* @throws Exception
*/
public void createNode() throws Exception{
Stat stat = zooKeeper.exists(ROOT_PATH, false);
if(null == stat){
//创建持久化的无序根节点
zooKeeper.create(ROOT_PATH,new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//创建一个临时有序的子节点
String lock = zooKeeper.create(ROOT_PATH + ROOT_LOCK_PATH, Thread.currentThread().getName().getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName() + "客户锁创建成功:" + lock);
this.lock = lock;
}
/**
* 请求获取锁
* @throws Exception
*/
public void acquire() throws Exception{
List<String> childNode = zooKeeper.getChildren(ROOT_PATH,false);//获取所有子节点
Collections.sort(childNode);//排序
int index = childNode.indexOf(lock.substring(ROOT_LOCK_PATH.length() + 1));//判断当前客户端的子节点是在子节点列表里面的位置 /lock/Lock
if(index == 0){
System.out.println(Thread.currentThread().getName() + "客户获取到锁," + lock);
}else{
String preLockPath = childNode.get(index - 1);//当前节点的上一个节点
Stat stat = zooKeeper.exists(ROOT_PATH + "/" + preLockPath,watcher);
if(null == stat){
acquire();
}else{
System.out.println("等待上一个节点释放锁,当前为:" + lock + ",上一个节点为:" + preLockPath);
synchronized (watcher) {
watcher.wait();
}
acquire();
}
}
}
/**
* 释放锁
* @throws KeeperException
* @throws InterruptedException
*/
public void releaseLock() throws KeeperException, InterruptedException {
zooKeeper.delete(lock, -1);
zooKeeper.close();
System.out.println("释放锁");
}
/**
* 监听锁的释放
*/
public Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("我的竞争对手释放锁");
//唤起客户端线程
synchronized (this) {
notifyAll();
}
}
};
}
- 接着我们模拟使用上面提供的方法,使用过程如第二点讲的锁的“思路”
@Component
public class ZooKeeperClient {
public void service() throws Exception {
//模拟多个获取锁的客户端
for(int i=0;i<300;i++){
ZookeeperLock zookeeperLock = new ZookeeperLock();
zookeeperLock.createNode();//先各自创建一个有序节点
zookeeperLock.acquire();//请求获取锁
/**业务执行**/
System.out.println("执行业务!");
Thread.sleep(10000);
/**业务执行**/
zookeeperLock.releaseLock();//处理完业务释放锁
}
}
}
- 最后我们写个控制器,模拟多个用户请求,在浏览器调用多次,我这里的访问地址是 http://localhost:8090/index
@RestController
public class IndexController {
@Autowired
ZooKeeperClient zooKeeperClient;
@RequestMapping("/index")
public String index(){
try {
zooKeeperClient.service();
} catch (Exception e) {
e.printStackTrace();
}
return "index:content";
}
}
- 结果如下
5、使用curator框架,帮我们封装了很多东西,不需要我们自己实现(控制时序)
public class ZooKeeperClient{
@Value("${zookeeper.url}")
String url;
String path = "/curatorLock";
public void service() throws Exception {
//创建客户端
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient(url, new ExponentialBackoffRetry(1000, 3));
curatorFramework.start();
//创建锁节点,根节点路径 /zkLock
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, path);
interProcessMutex.acquire();//请求获取锁
/**业务执行**/
Thread.sleep(5000);
/**业务执行**/
interProcessMutex.release();//完成业务,释放锁
curatorFramework.close();//关闭客户端
}
}
6、使用zkClint框架(保持独占)