分布式架构-基于Curator分布式锁及基本使用
一、Curator
Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。Patrixck Hunt(Zookeeper)以一句“Guava is to Java that Curator to Zookeeper”给Curator予高度评价。
Curator官网 http://curator.apache.org/
二、Curator分布式锁实现方案
Curator已经实现分布式锁的核心部分,其中关于分布式锁的核心API:
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
获取锁:interProcessMutex.acquire()
释放锁:interProcessMutex.release();
引入依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
引入依赖需要注意curator和Zk版本对应问题:
curator3和4 支持 zk3.5,
curator2 支持 zk3.4 和 3.5。
分布式锁基本使用:
InterProcessMutex interProcessMutex = null;
try {
interProcessMutex = new InterProcessMutex(curatorFramework, lockPath);
interProcessMutex.acquire();
log.info("<获取锁成功....>");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (interProcessMutex != null){
log.info("<释放锁....>");
interProcessMutex.release();
}
}
Curator关于分布式锁的api挺明了的,如果想使用zk自己实现分布式锁,也可以参考下面写我写的一篇博客。
https://blog.csdn.net/qq_43692950/article/details/112409116
下面就说下如何基于Curator简单封装成项目开发中使用的样子。为了方便调用将它封装在了AOP中,切入点为自定义注解,方便统一调用:
其中自动意注解为:
第一个参数为是否包含事物,如果有的话,会自动在方法执行完提交事物或异常回滚,
因为在Curator的分布式锁解决方案中,提供了线程等待超时的设置,但没有持有锁超时的设置,如果某个线程长时间的持有锁不释放,其他线程便处于一直等待中,因此在获得锁的时候将锁的信息存在了缓存中,在定时任务中判断持有锁是否超时,超时则中断线程释放锁资源,如果含有事物,则通过设置事物的超时和锁的超时时间一致(注意:Spring 的事物超时时间是针对SQL的执行时间,比如设置事物超时为5s,先插入数据库后sleep 10s,是不会回滚的,而反过来先sleep 后执行sql则会回滚,所以如果不确定哪个可能执行时间较长,可以在方法最后写一个固定的查询或其他db操作语句来延长事物时间),这样锁超时中断时事物也会回滚。第二个便是一次等待锁超时时间,在封装中获取锁超时直接抛出异常终止程序执行。后两个参数是时间单位和超时重试次数。满足超时时间和重复获取次数才算获取锁超时。
先将Curator注入到Spring容器中:
@Bean
public CuratorFramework curator(){
// 重试策略
// 初始休眠时间为 1000ms, 最大重试次数为 3
log.info("Curator初始化");
RetryPolicy retry = new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries);
// 创建一个客户端, 60000(ms)为 session 超时时间, 15000(ms)为连接超时时间
CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl, sessionTimeoutMs, connectionTimeoutMs, retry);
client.start();
log.info("Curator初始化成功");
return client;
}
下面是我AOP的封装
@Component
@Slf4j
@Aspect
public class ExtAsyncAop {
@Autowired
CuratorFramework curatorFramework;
private String lockPath = "/lock";
@Autowired
private TransactionUtils transactionUtils;
@Pointcut("@annotation(com.bxc.zkcuratordemo.LockFz.annotation.BxcZkLock)")
public void BrokerAspect(){
}
@AfterThrowing(value = "BrokerAspect()",throwing = "e")
public void doAfterThrowingGame(JoinPoint jp, Exception e) {
String name = jp.getSignature().getName();
System.out.println(name+"方法异常通知:"+e.toString());
}
@Around("BrokerAspect()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
BxcZkLock declaredAnnotation = ((MethodSignature) pjp.getSignature()).getMethod().getDeclaredAnnotation(BxcZkLock.class);
TransactionStatus transactionStatus = null;
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework,lockPath);
int count = declaredAnnotation.retriesNum();
long timeOut = declaredAnnotation.timeOut();
TimeUnit t = declaredAnnotation.t();
try {
int i = 0;
while (i < count){
boolean acquire = interProcessMutex.acquire(timeOut, t);
if (acquire){
log.info("Thread:"+Thread.currentThread().getId()+" 获取锁成功 ");
break;
}
i++;
}
if (i >= count){
throw new Exception("获取锁失败");
}
} catch (Exception e) {
log.error("获取锁失败 = "+e.toString());
throw e;
}
String serviceId = UUID.randomUUID().toString();
if (isTransaction(declaredAnnotation)){
transactionStatus = transactionUtils.begin();
LockScheduled.lockInfoMap.put(serviceId, new LockInfo(serviceId, Thread.currentThread(), LockStatusEnum.START,interProcessMutex,System.currentTimeMillis()));
}else {
LockScheduled.lockInfoMap.put(serviceId, new LockInfo(serviceId, Thread.currentThread(), LockStatusEnum.START,interProcessMutex,System.currentTimeMillis()));
}
try{
Object obj = pjp.proceed();
System.out.println("方法执行结束");
if (transactionStatus != null){
transactionUtils.commit(transactionStatus);
}
interProcessMutex.release();
LockScheduled.lockInfoMap.remove(serviceId);
return obj;
}catch (Exception e){
if (isTransaction(declaredAnnotation)){
transactionUtils.rollback(transactionStatus);
}
interProcessMutex.release();
LockScheduled.lockInfoMap.remove(serviceId);
throw e;
}
}
public boolean isTransaction(JoinPoint pjp){
BxcZkLock declaredAnnotation = ((MethodSignature) pjp.getSignature()).getMethod().getDeclaredAnnotation(BxcZkLock.class);
if (LockStatusEnum.Translation == declaredAnnotation.translation()){
return true;
}
return false;
}
public boolean isTransaction(BxcZkLock bxcZkLock){
if (LockStatusEnum.Translation == bxcZkLock.translation()){
return true;
}
return false;
}
}
在Aop的环绕通知中,默认使用的可重入锁,当然这个也可以在注解中再加个变量判断使用哪种锁,首先拿到自定义注解的参数,然后开了一个while循环,目的就是为了重复获取锁,如果一直没获取到锁,肯定i>=count,在这个时候抛出异常,停止下面的执行,如果获取到锁,则将锁的信息,如果开启事物也会将事物对象一并存入Map中,交给定时任务。下面为定时任务的逻辑。
@Slf4j
@Component
public class LockScheduled {
//锁的超时时间为5秒(可配置配置文件中)
public Long locktimeout = 5000L;
public static Map<String, LockInfo> lockInfoMap = new ConcurrentHashMap<>();
/**
* 检测超时未释放的锁
*/
@Scheduled(cron = "0/2 * * * * *")
public void taskService() {
try {
lockInfoMap.forEach((k, lockInfo) -> {
log.info("分布式锁超时检测");
if ((System.currentTimeMillis() - lockInfo.getCreateTime()) >= locktimeout) {
log.info("持有锁超时关闭-----> " + lockInfo.getLockId());
try {
lockInfo.getZkLock().release();
} catch (Exception e) {
e.printStackTrace();
}
//直接停止线程,事物超过时间没有提交会自动回滚
lockInfo.getLockThread().interrupt();
//移除队列
lockInfoMap.remove(k);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
在定时任务中,判断锁的创建时间是否大于秒如果大于,则将锁释放,有事物也会回滚事物,并强制停止线程,其实此时还需要优化,应该将信息存到其他某个地方,人工补偿此次数据。
三、Curator的基本使用
学习了上面的分布式锁,其实Curator还是个不错的zk客户端工具,附带学下一些其他的api吧:
-
创建一个初始内容为空的持久节点
client.create().forPath(path);
-
创建一个包含内容的持久节点
client.create().forPath(path,"aaa".getBytes());
-
创建临时节点,并递归创建父节点,递归创建父节点时,父节点为持久节点。
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
-
删除一个子节点
client.delete().forPath(path);
-
删除节点并递归删除其子节点
client.delete().deletingChildrenIfNeeded().forPath(path);
-
指定版本进行删除如果此版本已经不存在,则抛出异常
client.delete().withVersion(1).forPath(path);
-
强制保证删除一个节点
client.delete().guaranteed().forPath(path);
-
普通查询
client.getData().forPath(path);
-
包含状态查询
Stat stat = new Stat(); client.getData().storingStatIn(stat).forPath(path);
-
普通更新
client.setData().forPath(path,"bbb".getBytes());
-
指定版本更新
client.setData().withVersion(1).forPath(path);
-
监听事件
byte[] content = client.getData().usingWatcher(new Watcher() {
@SneakyThrows
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("监听器watchedEvent:" + watchedEvent);
System.out.println(new String(client.getData().forPath(path)));
}
}).forPath(path);
System.out.println("监听节点内容:" + new String(content));
或者:
NodeCache cache = new NodeCache(client, path, false);
cache.start(true);
cache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("Node data update, new data: " + new String(cache.getCurrentData().getData()));
}
});
- 监听子节点动作事件
String path = "/bxc";
PathChildrenCache cache = new PathChildrenCache(client, path, true);
cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
cache.getListenable().addListener(new PathChildrenCacheListener(){
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception{
switch(event.getType()) {
case CHILD_ADDED :
System.out.println("添加节点" + event.getData().getPath());
break;
case CHILD_UPDATED :
System.out.println("修改节点" + event.getData().getPath());
break;
case CHILD_REMOVED :
System.out.println("删除节点," + event.getData().getPath());
break;
default:
break;
}
}
});
下面造作均会触发上面监听:
client.create().withMode(CreateMode.PERSISTENT).forPath(path+"/tem");
client.setData().forPath(path+"/tem","1111".getBytes());
client.delete().forPath(path+"/tem");