项目中操作redis改brpop阻塞模式为订阅模式的实现-java实习笔记二
更改项目需求以及项目之前阻塞模式问题的叙述已经在上一篇说过了,详情可参考: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实习笔记二的更多相关文章
- Django项目中使用Redis
Django项目中使用Redis DjangoRedis 1 redis Redis 是一个 key-value 存储系统,常用于缓存的存储.django-redis 基于 BSD 许可, 是一个使 ...
- Spring-Boot项目中配置redis注解缓存
Spring-Boot项目中配置redis注解缓存 在pom中添加redis缓存支持依赖 <dependency> <groupId>org.springframework.b ...
- Redis的安装以及在项目中使用Redis的一些总结和体会
第一部分:为什么我的项目中要使用Redis 我知道有些地方没说到位,希望大神们提出来,我会吸取教训,大家共同进步! 注册时邮件激活的部分使用Redis 发送邮件时使用Redis的消息队列,减轻网站压力 ...
- Redis入门教程(三)— Java中操作Redis
在Redis的官网上,我们可以看到Redis的Java客户端众多 其中,Jedis是Redis官方推荐,也是使用用户最多的Java客户端. 开始前的准备 使用jedis使用到的jedis-2.1.0. ...
- 在项目中部署redis的读写分离架构(包含节点间认证口令)
#### 在项目中部署redis的读写分离架构(包含节点间认证口令) ##### 1.配置过程 --- 1.此前就是已经将redis在系统中已经安装好了,redis utils目录下,有个redis ...
- Redis学习笔记之二 :在Java项目中使用Redis
成功配置redis之后,便来学习使用redis.首先了解下redis的数据类型. Redis的数据类型 Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set( ...
- 在express项目中使用redis
在express项目中使用redis 准备工作 安装redis 安装redis桌面管理工具:Redis Desktop Manager 项目中安装redis:npm install redis 开始使 ...
- 阶段5 3.微服务项目【学成在线】_day05 消息中间件RabbitMQ_8.RabbitMQ研究-工作模式-发布订阅模式-生产者
Publish/subscribe:发布订阅模式 发布订阅模式: 1.每个消费者监听自己的队列. 2.生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将 ...
- Python中操作Redis
一 Rdis基本介绍 redis是一个key-value存储系统.它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorted set -- ...
随机推荐
- AlwaysOn数据同步暂停及回退技术
随着AlwaysOn技术的流行,关于AlwayOn的问题也越来越多,某企业搭建有三副本的AlwaysOn一套,现想修改主节点上某张表的某个数据,看看会出现什么后果,如果结果正常,就同步到其他节点上:如 ...
- Docker笔记03-docker 网络模式
docker网络模式分为5种 Nat (Network Address Translation) Host other container none overlay 第一种 Nat模式 docker的 ...
- 用 eric6 与 PyQt5 实现python的极速GUI编程(35篇PyQT和200多篇Python)
[题记] 我是一个菜鸟,这个系列是我的学习笔记. PyQt5 出来有一段时间了, PyQt5 较之 PyQt4 有一些变化,而网上流传的几乎都是 PyQt4 的教程,照搬的话大多会出错. eric6 ...
- 虚拟机安装 ubuntu 后,更新源无效,以及无法联网安装软件的问题
问题: 虚拟机安装 ubuntu 后,更新源无效,以及无法联网安装软件: 错误提示: Err http://security.ubuntu.com/ubuntu/ trusty-security/un ...
- Microsoft Development Platform Technologies
- SecureCRT的安装以及破破解(内含安装包)
1.百度网盘连接:链接:https://pan.baidu.com/s/13i8sblGthYtj2SbUTrbmsg 提取码:8cw1 2.解压前先关闭电脑防护软件,否则会杀掉破解软件的 3.压缩 ...
- Linux常用实用命令
Linux是我们开发人员必不可少的系统,也是经常接触到的.然而,Linux命令比较多,有些不常用也难记住.那么,我们如何更高效的使用Linux命令,而又不必全面地学习呢?今天就给大家分享一下我在开发过 ...
- RequestMapping原理分析和RequestMappingHandlerMapping
转载https://juejin.im/post/5cbeadb96fb9a031ff0d18b5 源码版本spring-webmvc-4.3.7.RELEASE 使用Spring MVC的同学一般都 ...
- Azkaban学习之路(二)—— Azkaban 3.x 编译及部署
一.Azkaban 源码编译 1.1 下载并解压 Azkaban 在3.0版本之后就不提供对应的安装包,需要自己下载源码进行编译. 下载所需版本的源码,Azkaban的源码托管在GitHub上,地址为 ...
- spring 5.x 系列第18篇 —— 整合websocket (代码配置方式)
源码Gitub地址:https://github.com/heibaiying/spring-samples-for-all 一.说明 1.1 项目结构说明 项目模拟一个简单的群聊功能,为区分不同的聊 ...