更改项目需求以及项目之前阻塞模式问题的叙述已经在上一篇说过了,详情可参考:https://www.cnblogs.com/darope/p/10276213.html文章的介绍。

关于Agent数据采集相关内容介绍可以参考华中科技大学的这篇硕士论文,说的比较详细:http://www.docin.com/p-131767044.html 。

一,关于brpop为什么要更改,这里简单分析一下原版本的阻塞代码。

 @Override
public void readyForControl(Service.ControlRequest request, StreamObserver<Service.ControlResponse> responseObserver) {
byte[] uuidByte = request.getH().getId().toByteArray();
JUUID juuid = new JUUID(uuidByte);
String uuid = juuid.toString();
logger.info("readyForControl uuid: " + uuid);
// agent上线
Long onlineTime = System.currentTimeMillis();
redisService.set(ONLINE_PREFIX + uuid, String.valueOf(System.currentTimeMillis()));
onlineAgent(uuid); while (true) {
try {
//暂时没有更好的办法处理,降低两个while同时守护任务redis的可能性
if (needBreak(uuid, onlineTime)) {
break;
}
List<Task> tasks = taskRedisMap.brpop(uuid);
if (Objects.isNull(tasks)) {
continue;
}
for(Task task : tasks) {
//agent 重启后丢失一个任务;老的rpc通道收到任务放回机制
if (needBreak(uuid, onlineTime)) {
taskRedisMap.pushTask(task);
continue;
}
logger.info("task get uuid: " + uuid + " nodeId: " + task.getNodeId()); Service.ControlResponse.ControlCmd controlCmd = Service.ControlResponse.ControlCmd.forNumber(task.getTaskType());
Service.ControlResponse response = null;
assert controlCmd != null;
// 根据任务类型分配任务
response = getControlResponseOption(task, controlCmd, null);
logger.info("cmd: " + controlCmd + " nodeId " + task.getNodeId());
if (Objects.isNull(response)) {
logger.info("empty response. nodeId " + task.getNodeId());
return;
} // 通知业务调用方
readyForControlEvent(task);
logger.info("readyForControlEvent");
task.setTaskStatus(TaskStatusEnum.BUSY);
task.setStartExecTimeout(System.currentTimeMillis());
task.setReceiveEvent(true);
taskRedisMap.update(task);
logger.info("onNext ...");
responseObserver.onNext(response);
logger.info("onNext OK...");
}
} catch (Throwable e) {
logger.error("readyForControl异常, uuid={}", e, uuid);
}
}
}

客户端在服务端注册好自己传送过来的数据后,调用readyForControl,请求服务端下发命令,有几个agent客户端主机,就会调用几次。相同agent再次上线这里就会出现一个很大的问题,原来的agent没有下线,相同的agent再次上线,这里会再次调用readyForControl。意味着相同的agent调用了两次,而且新上线的agent后调用readyForControl。如果采用brpop的方式,意味着一开始上线的agent调用readyForControl已经拿走了消息列队的task任务,后来的只能拿不到,空指针异常。这里采用了一个不是办法的办法,就是写一个死循环,监听agent上线动作,比对一下,如果这个agent是后来上线的,就会break掉,杜绝了异常的发生。但是这个操作会显得很臃肿,而且效率不太好。

二,更改为订阅模式或许会解决以上问题,原因如下:

  a.  readyForControl中,只有一个订阅方法,简洁很多

  b.  不需要判断是不是相同agent上线的问题,虽然新上线的agent跟之前的agent是同一个agent,但是跟redis的发布订阅模式不冲突,老的agent也会订阅到消息,新的agent也会订阅到消息。避免了一个大的用于判断agent新旧问题的死循环。

  c.  效率更高,redis底层是c语言实现的,借助redis的机制来解决问题,往往比自己实现逻辑来解决问题,从本质上看来要可取。

三,更改的过程中遇到的坑:

很遗憾,很多坑是我想当然的以为造成的,并没有严谨的考虑软件工程的思想以及大型程序运行的理论情形。对此只会让我以为我还有很多的东西要学,现在的出错,只是为了记忆更深刻吧。下面由浅入深做简单总结:

  a. 从简单订阅模式,到多线程订阅模式。

  订阅模式本身是redis自带的方法,但是订阅模式是恒阻塞的,一旦进入订阅的方法,就会一直监听发布方是否发布了消息,导致监听阻塞,无法使调用方程序顺序执行。虽然订阅方法父类有onMessage方法可以终止订阅,但是不满足需要监听agent上线的逻辑策略。对此需要增加多线程实现,把订阅方法写到线程空间中去。

     @Override
public void readyForControl(Service.ControlRequest request, StreamObserver<Service.ControlResponse> responseObserver) {
byte[] uuidByte = request.getH().getId().toByteArray();
JUUID juuid = new JUUID(uuidByte);
String uuid = juuid.toString();
Long agentId = taskRedisMap.getIdByUuid(uuid);
// 调用订阅者线程
SubThread subThread;
subThread = new SubThread(redisService.getJedisPool(), agentId, responseObserver, taskRedisMap, applicationContext);
subThread.start();
logger.info("readyForControl uuid: " + uuid); // agent上线
redisService.set(ONLINE_PREFIX + uuid, String.valueOf(System.currentTimeMillis()));
onlineAgent(uuid);
}

更改之后的代码采用多线程开启订阅方法,删除死循环维护agent上线的问题。当多agent上线时,会为每一个agent客户端开启一个属于自己的订阅方法,由于brpop方式采用的是uuid转化为agentId对比任务agentId的方式,以此来保证任务下发的准确性,我就把频道更改为uuid,保证了任务下发的准确性。

  b.从专用频道订阅模式,到通用频道订阅模式。

  企业级项目必须考虑到资源的损耗和浪费情况,如果每一个上线agent客户端均使用专用频道,会增加redis的负荷,严重会让redis睡觉。如此看来为每一个agent开一个以agent的id相关的字符串为该agent的通道的话,是绝对不可取的。在师兄的引导下,为此我折腾了一个下午,目的就是不采用专用通道,采用通用通道,即所有任务shell的发布和订阅都在一个频道,是谁的谁自己来领取。但是怎么领取,最后我通过把uuid传到订阅线程中,从onMessage中转化为任务序列对比发布中的任务序列号,取到我需要的task然后return到调用方。看起来还不错,我比较满意。

  c.程序运行并不满足预期

  如我所想,数据我是拿到了,接着我在readyForControl调用这个线程后,取到agentId对应的所有任务列表,这样我就可以使用这个任务列表onNext到客户端啦,像下面这样:

  // 通知业务调用方
readyForControlEvent(task);
logger.info("readyForControlEvent");
task.setTaskStatus(TaskStatusEnum.BUSY);
task.setStartExecTimeout(System.currentTimeMillis());
task.setReceiveEvent(true);
//不通
taskRedisMap.update(task);
logger.info("onNext ...");
responseObserver.onNext(response);
logger.info("onNext OK...");

但是下面的方法是取不到我的task任务列表的所有数据的,原因是,当我进入到我的线程后,我执行订阅方法,对比我传入的uuid拿到属于该agent的一个task。然后调用这个线程的方法就会顺序执行了。线程仍然存在,只是再也没人调用了,readyForControl代码程序一旦顺序执行,就回不到调用线程的那个代码位置了。尴尬的是,理论上,我的task列表里面只会有一条task。

  d.没法在readyForControl中拿到所有task的列表,我必须在线程里面单个处理,仔细想想,效率好像还提升了

逆行思维真的是很好的方式,他会使你在向左走不通的情况下会考虑向右走一走,最终走出这个死胡同。程序封装的目的在于统一处理,正常的方式是我所有task存入到我的list列表中,return到调用方,在readyForControl中统一onNext到agent客户端。线程方式这种走不通,只能把接下来所有操作task的代码传到线程中去,在线程中一个一个onNext到客户端。首先要做的是把需要用到的类实例传到线程中去,该传进去的传进去,该注入的注入到线程空间中去。然后每次收到订阅消息message,我都把这个message转化为对应agent的task最后onNext下发到客户端。看起来还不错,但是即将迎来一个大坑。

  e.程序没报错,为什么线程空间中的实例,会频繁的报空指针?

代码看着已经没什么问题,逻辑上也是可行的,但测试的时候,老是空指针。查阅资料,发现Spring为了安全,禁止向线程空间中注入bean。网上的解决办法很多,我需要注入的就是两个操作task任务流的bean,所以就采用了最简单的传递参数的方式,外层先注入我需要的bean,然后当成调用线程的方法的参数。线程方使用私有变量初始化类,不采用注入的方式,然后通过构造方法拿到传进来的类实例。

  f.或许你认为最不应该有问题的地方出现了问题

最终代码已经差不多可以使用了,但是偶尔会抛异常,检查了一晚上发现是jdk中操作list的问题。至今不是很明白,也希望有读到的大神给与评论原因。一开始的逻辑,在对比是不是我这个上线agent的task的时候,我采用一个if判断。在任务列表tasks不可能为空的情况下,if( 上线agentId.compareToIgnoreCase(发布方发布的Task中的agentId) != 0 ) 从tasks列表中移除这个不匹配的task,采用tasks.remove(task)的方式,else下发这个任务到客户端    ------------》  更改为if( 上线agentId.compareToIgnoreCase(发布方发布的Task中的agentId) == 0 )下发任务到客户端,else不做处理。就解决了异常问题,看似两个逻辑是一样的,或许是remove操作列表有什么需要注意的吧。

最终所有操作都在线程空间中处理,订阅线程继承的的onMessage方法中,分布对订阅到的task单独处理,肢解了圆来readyForControl的代码:

 @Override
public void onMessage(String channel, String message) { //收到消息会调用
logger.info("收到了发布者的消息,频道为: {}, 消息为: {}", channel, message); tasks.add(message); key = TASK_PENDING_PREFIX + agentId; List<Map> taskList = tasks.stream().map(k -> Json2.fromJson(k, Map.class)).collect(Collectors.toList());
if (taskList.size() == 0) {
return;
}
// 筛选出ShellTask
List<ShellTask> shellTaskList = taskList.stream().filter(t -> Objects.equals(t.get("execType"), ExecScriptType.SHELL.getCode())).map(t -> Json2.fromJson(Json2.toJson(t), ShellTask.class)).collect(Collectors.toList());
if (shellTaskList.size() == 0) {
return;
}
List<Task> task_ = shellTaskList.stream().filter(t -> Objects.equals(t.getTaskStatus(), TaskStatusEnum.NOT_OPERATED)).collect(Collectors.toList());
logger.info("task list : {}", task_); //返回携带特定uuid订阅者agent的task for (Task task : task_) {
String keyPub = TASK_PENDING_PREFIX + task.getAgentId();
logger.info("keyPub {}", keyPub);
if (key.compareToIgnoreCase(keyPub) == 0){
logger.info("task get uuid: " + key + " nodeId: " + task.getNodeId()); Service.ControlResponse.ControlCmd controlCmd = Service.ControlResponse.ControlCmd.forNumber(task.getTaskType());
Service.ControlResponse response = null;
assert controlCmd != null;
// 根据任务类型分配任务
response = getControlResponseOption(task, controlCmd, null);
logger.info("cmd: " + controlCmd + " nodeId " + task.getNodeId());
if (Objects.isNull(response)) {
logger.info("empty response. nodeId " + task.getNodeId());
return;
} // 通知业务调用方
readyForControlEvent(task);
logger.info("readyForControlEvent");
task.setTaskStatus(TaskStatusEnum.BUSY);
task.setStartExecTimeout(System.currentTimeMillis());
task.setReceiveEvent(true);
//不通
taskRedisMap.update(task);
logger.info("onNext ...");
responseObserver.onNext(response);
logger.info("onNext OK...");
}
}
}

接下来的优化策略是,判断agent上线时间,如果是相同agent再次上线,可以考虑让以前的agent下线,而非继续订阅,虽然继续订阅不会影响程序正常使用,也不需要像brpop的方式来维护消息列队中的task,但是当agent某个客户端反复上线下线,也会造成不必要的订阅资源浪费,所以程序还是需要判断哪些agent需要下线处理。

因为是实习第一阶段,自己还算个小白,很多思考不到的地方,踩了不少坑,特此记录。

项目中操作redis改brpop阻塞模式为订阅模式的实现-java实习笔记二的更多相关文章

  1. Django项目中使用Redis

    Django项目中使用Redis DjangoRedis 1 redis Redis 是一个 key-value 存储系统,常用于缓存的存储.django-redis 基于 BSD 许可, 是一个使 ...

  2. Spring-Boot项目中配置redis注解缓存

    Spring-Boot项目中配置redis注解缓存 在pom中添加redis缓存支持依赖 <dependency> <groupId>org.springframework.b ...

  3. Redis的安装以及在项目中使用Redis的一些总结和体会

    第一部分:为什么我的项目中要使用Redis 我知道有些地方没说到位,希望大神们提出来,我会吸取教训,大家共同进步! 注册时邮件激活的部分使用Redis 发送邮件时使用Redis的消息队列,减轻网站压力 ...

  4. Redis入门教程(三)— Java中操作Redis

    在Redis的官网上,我们可以看到Redis的Java客户端众多 其中,Jedis是Redis官方推荐,也是使用用户最多的Java客户端. 开始前的准备 使用jedis使用到的jedis-2.1.0. ...

  5. 在项目中部署redis的读写分离架构(包含节点间认证口令)

    #### 在项目中部署redis的读写分离架构(包含节点间认证口令) ##### 1.配置过程 ---  1.此前就是已经将redis在系统中已经安装好了,redis utils目录下,有个redis ...

  6. Redis学习笔记之二 :在Java项目中使用Redis

    成功配置redis之后,便来学习使用redis.首先了解下redis的数据类型. Redis的数据类型 Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set( ...

  7. 在express项目中使用redis

    在express项目中使用redis 准备工作 安装redis 安装redis桌面管理工具:Redis Desktop Manager 项目中安装redis:npm install redis 开始使 ...

  8. 阶段5 3.微服务项目【学成在线】_day05 消息中间件RabbitMQ_8.RabbitMQ研究-工作模式-发布订阅模式-生产者

    Publish/subscribe:发布订阅模式 发布订阅模式: 1.每个消费者监听自己的队列. 2.生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将 ...

  9. Python中操作Redis

    一 Rdis基本介绍 redis是一个key-value存储系统.它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorted set -- ...

随机推荐

  1. Win10《芒果TV》跨年邀你嗨唱,同步直播《湖南卫视2017-2018跨年演唱会》

    由天天兄弟.快本家族联袂主持,不容错过的年度盛典<湖南卫视2017-2018跨年演唱会>将于2017年12月31日19:30起由芒果TV同步直播,果妈备上礼物邀您一起跨年嗨唱. 跨年邀你嗨 ...

  2. vista忘记用户名密码的修改方法(使用PE进入系统,用cmd.exe冒充虚拟键盘,然后就可以mmc组策略,或者命令行添加用户并提升权限)

    1. 准备Windows Vista安装光盘,进入BIOS将光驱设为第一启动,在出现的安装界面依次单击"修复计算机","命令提示符". 2.输入以下命令: co ...

  3. scons编译mongodb(vs2008版本)遇到的问题总结

    OS:win7 64 boost:1.49 mongodb:2.4.6(推荐64位版本,当然如果你系统是32位的,只能使用32的版本了) IDE:vs2008(2010的同学请跳过吧,因为官网提供的就 ...

  4. Delphi结束进程模块

    function KillTask(ExeFileName: string): integer; const PROCESS_TERMINATE = $0001; var ContinueLoop: ...

  5. Qt学习虚拟机--基于MSYS2-MinGW环境并带有各种开源的软件库!

    Qt学习虚拟机--基于MSYS2-MinGW环境并带有各种开源的软件库!虚拟机地址,VM10和以上:http://pan.baidu.com/s/1slcTA49包含两个分卷压缩包,加起来5GB多. ...

  6. 剖析Qt的事件机制原理(源代码级别)

    在用Qt写Gui程序的时候,在main函数里面最后依据都是app.exec();很多书上对这句的解释是,使Qt程序进入消息循环.下面我们就到exec()函数内部,来看一下他的实现原理.Let's go ...

  7. .Net上传文件处理三大范式,及开发注意事项

    最近工作内容涉及到一点前端的内容,把学习到的内容记录下来,在今后的开发过程中,不要犯错.本篇只针对一些刚入职的小白及前端开发人员,大牛请绕道!~ 刚开始我们先不讲上传文件的防范问题,先通过一个例子,让 ...

  8. HDFS Java API 的基本使用

    一. 简介 二.API的使用         2.1 FileSystem         2.2 创建目录         2.3 创建指定权限的目录         2.4 创建文件,并写入内容 ...

  9. spring boot 2.x 系列 —— spring boot 整合 dubbo

    文章目录 一. 项目结构说明 二.关键依赖 三.公共模块(boot-dubbo-common) 四. 服务提供者(boot-dubbo-provider) 4.1 提供方配置 4.2 使用注解@Ser ...

  10. spring 5.x 系列第2篇 —— springmvc基础 (代码配置方式)

    文章目录 一.搭建hello spring工程 1.1 项目搭建 1.2 相关注解说明 二.配置自定义拦截器 三.全局异常处理 四.参数绑定 4.1 参数绑定 4.2 关于日期格式转换的三种方法 五. ...