《过时不候》

最漫长的莫过于等待

我们不可能永远等一个人

就像请求

永远等待响应

超时处理

java 从零开始手写 RPC (01) 基于 socket 实现

java 从零开始手写 RPC (02)-netty4 实现客户端和服务端

java 从零开始手写 RPC (03) 如何实现客户端调用服务端?

java 从零开始手写 RPC (04) 序列化

java 从零开始手写 RPC (05) 基于反射的通用化实现

必要性

前面我们实现了通用的 rpc,但是存在一个问题,同步获取响应的时候没有超时处理。

如果 server 挂掉了,或者处理太慢,客户端也不可能一直傻傻的等。

当外部的调用超过指定的时间后,就直接报错,避免无意义的资源消耗。

思路

调用的时候,将开始时间保留。

获取的时候检测是否超时。

同时创建一个线程,用来检测是否有超时的请求。

实现

思路

调用的时候,将开始时间保留。

获取的时候检测是否超时。

同时创建一个线程,用来检测是否有超时的请求。

超时检测线程

为了不影响正常业务的性能,我们另起一个线程检测调用是否已经超时。

package com.github.houbb.rpc.client.invoke.impl;

import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.rpc.common.rpc.domain.RpcResponse;
import com.github.houbb.rpc.common.rpc.domain.impl.RpcResponseFactory;
import com.github.houbb.rpc.common.support.time.impl.Times; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; /**
* 超时检测线程
* @author binbin.hou
* @since 0.0.7
*/
public class TimeoutCheckThread implements Runnable{ /**
* 请求信息
* @since 0.0.7
*/
private final ConcurrentHashMap<String, Long> requestMap; /**
* 请求信息
* @since 0.0.7
*/
private final ConcurrentHashMap<String, RpcResponse> responseMap; /**
* 新建
* @param requestMap 请求 Map
* @param responseMap 结果 map
* @since 0.0.7
*/
public TimeoutCheckThread(ConcurrentHashMap<String, Long> requestMap,
ConcurrentHashMap<String, RpcResponse> responseMap) {
ArgUtil.notNull(requestMap, "requestMap");
this.requestMap = requestMap;
this.responseMap = responseMap;
} @Override
public void run() {
for(Map.Entry<String, Long> entry : requestMap.entrySet()) {
long expireTime = entry.getValue();
long currentTime = Times.time(); if(currentTime > expireTime) {
final String key = entry.getKey();
// 结果设置为超时,从请求 map 中移除
responseMap.putIfAbsent(key, RpcResponseFactory.timeout());
requestMap.remove(key);
}
}
} }

这里主要存储请求,响应的时间,如果超时,则移除对应的请求。

线程启动

在 DefaultInvokeService 初始化时启动:

final Runnable timeoutThread = new TimeoutCheckThread(requestMap, responseMap);
Executors.newScheduledThreadPool(1)
.scheduleAtFixedRate(timeoutThread,60, 60, TimeUnit.SECONDS);

DefaultInvokeService

原来的设置结果,获取结果是没有考虑时间的,这里加一下对应的判断。

设置请求时间

  • 添加请求 addRequest

会将过时的时间直接放入 map 中。

因为放入是一次操作,查询可能是多次。

所以时间在放入的时候计算完成。

@Override
public InvokeService addRequest(String seqId, long timeoutMills) {
LOG.info("[Client] start add request for seqId: {}, timeoutMills: {}", seqId,
timeoutMills);
final long expireTime = Times.time()+timeoutMills;
requestMap.putIfAbsent(seqId, expireTime);
return this;
}

设置请求结果

  • 添加响应 addResponse
  1. 如果 requestMap 中已经不存在这个请求信息,则说明可能超时,直接忽略存入结果。

  2. 此时检测是否出现超时,超时直接返回超时信息。

  3. 放入信息后,通知其他等待的所有进程。

@Override
public InvokeService addResponse(String seqId, RpcResponse rpcResponse) {
// 1. 判断是否有效
Long expireTime = this.requestMap.get(seqId);
// 如果为空,可能是这个结果已经超时了,被定时 job 移除之后,响应结果才过来。直接忽略
if(ObjectUtil.isNull(expireTime)) {
return this;
} //2. 判断是否超时
if(Times.time() > expireTime) {
LOG.info("[Client] seqId:{} 信息已超时,直接返回超时结果。", seqId);
rpcResponse = RpcResponseFactory.timeout();
} // 这里放入之前,可以添加判断。
// 如果 seqId 必须处理请求集合中,才允许放入。或者直接忽略丢弃。
// 通知所有等待方
responseMap.putIfAbsent(seqId, rpcResponse);
LOG.info("[Client] 获取结果信息,seqId: {}, rpcResponse: {}", seqId, rpcResponse);
LOG.info("[Client] seqId:{} 信息已经放入,通知所有等待方", seqId);
// 移除对应的 requestMap
requestMap.remove(seqId);
LOG.info("[Client] seqId:{} remove from request map", seqId);
synchronized (this) {
this.notifyAll();
}
return this;
}

获取请求结果

  • 获取相应 getResponse
  1. 如果结果存在,直接返回响应结果

  2. 否则进入等待。

  3. 等待结束后获取结果。

@Override
public RpcResponse getResponse(String seqId) {
try {
RpcResponse rpcResponse = this.responseMap.get(seqId);
if(ObjectUtil.isNotNull(rpcResponse)) {
LOG.info("[Client] seq {} 对应结果已经获取: {}", seqId, rpcResponse);
return rpcResponse;
}
// 进入等待
while (rpcResponse == null) {
LOG.info("[Client] seq {} 对应结果为空,进入等待", seqId);
// 同步等待锁
synchronized (this) {
this.wait();
}
rpcResponse = this.responseMap.get(seqId);
LOG.info("[Client] seq {} 对应结果已经获取: {}", seqId, rpcResponse);
}
return rpcResponse;
} catch (InterruptedException e) {
throw new RpcRuntimeException(e);
}
}

可以发现获取部分的逻辑没变,因为超时会返回一个超时对象:RpcResponseFactory.timeout();

这是一个非常简单的实现,如下:

package com.github.houbb.rpc.common.rpc.domain.impl;

import com.github.houbb.rpc.common.exception.RpcTimeoutException;
import com.github.houbb.rpc.common.rpc.domain.RpcResponse; /**
* 响应工厂类
* @author binbin.hou
* @since 0.0.7
*/
public final class RpcResponseFactory { private RpcResponseFactory(){} /**
* 超时异常信息
* @since 0.0.7
*/
private static final DefaultRpcResponse TIMEOUT; static {
TIMEOUT = new DefaultRpcResponse();
TIMEOUT.error(new RpcTimeoutException());
} /**
* 获取超时响应结果
* @return 响应结果
* @since 0.0.7
*/
public static RpcResponse timeout() {
return TIMEOUT;
} }

响应结果指定一个超时异常,这个异常会在代理处理结果时抛出:

RpcResponse rpcResponse = proxyContext.invokeService().getResponse(seqId);
Throwable error = rpcResponse.error();
if(ObjectUtil.isNotNull(error)) {
throw error;
}
return rpcResponse.result();

测试代码

服务端

我们故意把服务端的实现添加沉睡,其他保持不变。

public class CalculatorServiceImpl implements CalculatorService {

    public CalculateResponse sum(CalculateRequest request) {
int sum = request.getOne()+request.getTwo(); // 故意沉睡 3s
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} return new CalculateResponse(true, sum);
} }

客户端

设置对应的超时时间为 1S,其他不变:

public static void main(String[] args) {
// 服务配置信息
ReferenceConfig<CalculatorService> config = new DefaultReferenceConfig<CalculatorService>();
config.serviceId(ServiceIdConst.CALC);
config.serviceInterface(CalculatorService.class);
config.addresses("localhost:9527");
// 设置超时时间为1S
config.timeout(1000); CalculatorService calculatorService = config.reference();
CalculateRequest request = new CalculateRequest();
request.setOne(10);
request.setTwo(20); CalculateResponse response = calculatorService.sum(request);
System.out.println(response);
}

日志如下:

.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2021-10-05 14:59:40.974] [main] [c.g.h.r.c.c.RpcClient.connect] - RPC 服务开始启动客户端
...
[INFO] [2021-10-05 14:59:42.504] [main] [c.g.h.r.c.c.RpcClient.connect] - RPC 服务启动客户端完成,监听地址 localhost:9527
[INFO] [2021-10-05 14:59:42.533] [main] [c.g.h.r.c.p.ReferenceProxy.invoke] - [Client] start call remote with request: DefaultRpcRequest{seqId='62e126d9a0334399904509acf8dfe0bb', createTime=1633417182525, serviceId='calc', methodName='sum', paramTypeNames=[com.github.houbb.rpc.server.facade.model.CalculateRequest], paramValues=[CalculateRequest{one=10, two=20}]}
[INFO] [2021-10-05 14:59:42.534] [main] [c.g.h.r.c.i.i.DefaultInvokeService.addRequest] - [Client] start add request for seqId: 62e126d9a0334399904509acf8dfe0bb, timeoutMills: 1000
[INFO] [2021-10-05 14:59:42.535] [main] [c.g.h.r.c.p.ReferenceProxy.invoke] - [Client] start call channel id: 00e04cfffe360988-000004bc-00000000-1178e1265e903c4c-7975626f
...
Exception in thread "main" com.github.houbb.rpc.common.exception.RpcTimeoutException
at com.github.houbb.rpc.common.rpc.domain.impl.RpcResponseFactory.<clinit>(RpcResponseFactory.java:23)
at com.github.houbb.rpc.client.invoke.impl.DefaultInvokeService.addResponse(DefaultInvokeService.java:72)
at com.github.houbb.rpc.client.handler.RpcClientHandler.channelRead0(RpcClientHandler.java:43)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:105)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.handler.logging.LoggingHandler.channelRead(LoggingHandler.java:241)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:310)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:284)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1359)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:935)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:138)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:645)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:580)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:497)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:459)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:138)
at java.lang.Thread.run(Thread.java:748)
...
[INFO] [2021-10-05 14:59:45.615] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] seqId:62e126d9a0334399904509acf8dfe0bb 信息已超时,直接返回超时结果。
[INFO] [2021-10-05 14:59:45.617] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] 获取结果信息,seqId: 62e126d9a0334399904509acf8dfe0bb, rpcResponse: DefaultRpcResponse{seqId='null', error=com.github.houbb.rpc.common.exception.RpcTimeoutException, result=null}
[INFO] [2021-10-05 14:59:45.617] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] seqId:62e126d9a0334399904509acf8dfe0bb 信息已经放入,通知所有等待方
[INFO] [2021-10-05 14:59:45.618] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] seqId:62e126d9a0334399904509acf8dfe0bb remove from request map
[INFO] [2021-10-05 14:59:45.618] [nioEventLoopGroup-2-1] [c.g.h.r.c.c.RpcClient.channelRead0] - [Client] response is :DefaultRpcResponse{seqId='62e126d9a0334399904509acf8dfe0bb', error=null, result=CalculateResponse{success=true, sum=30}}
[INFO] [2021-10-05 14:59:45.619] [main] [c.g.h.r.c.i.i.DefaultInvokeService.getResponse] - [Client] seq 62e126d9a0334399904509acf8dfe0bb 对应结果已经获取: DefaultRpcResponse{seqId='null', error=com.github.houbb.rpc.common.exception.RpcTimeoutException, result=null}
...

可以发现,超时异常。

不足之处

对于超时的处理可以拓展为双向的,比如服务端也可以指定超时限制,避免资源的浪费。

小结

为了便于大家学习,以上源码已经开源:

https://github.com/houbb/rpc

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次重逢。

java 从零开始手写 RPC (07)-timeout 超时处理的更多相关文章

  1. java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端

    通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...

  2. java 从零开始手写 RPC (03) 如何实现客户端调用服务端?

    说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...

  3. java 从零开始手写 RPC (04) -序列化

    序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...

  4. java 从零开始手写 RPC (01) 基于 websocket 实现

    RPC 解决的问题 RPC 主要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑. 这一节我们来学习下如何基于 we ...

  5. 手写RPC框架指北另送贴心注释代码一套

    Angular8正式发布了,Java13再过几个月也要发布了,技术迭代这么快,框架的复杂度越来越大,但是原理是基本不变的.所以沉下心看清代码本质很重要,这次给大家带来的是手写RPC框架. 完整代码以及 ...

  6. 看了这篇你就会手写RPC框架了

    一.学习本文你能学到什么? RPC的概念及运作流程 RPC协议及RPC框架的概念 Netty的基本使用 Java序列化及反序列化技术 Zookeeper的基本使用(注册中心) 自定义注解实现特殊业务逻 ...

  7. 手写RPC框架(六)整合Netty

    手写RPC框架(六)整合Netty Netty简介: Netty是一个基于NIO的,提供异步,事件驱动的网络应用工具,具有高性能高可靠性等特点. 使用传统的Socket来进行网络通信,服务端每一个连接 ...

  8. 从零开始手写 dubbo rpc 框架

    rpc rpc 是基于 netty 实现的 java rpc 框架,类似于 dubbo. 主要用于个人学习,由渐入深,理解 rpc 的底层实现原理. 前言 工作至今,接触 rpc 框架已经有很长时间. ...

  9. java - day015 - 手写双向链表, 异常(续), IO(输入输出)

    类的内存分配 加载到方法区 对象在堆内存 局部变量在栈内存 判断真实类型,在方法区加载的类 对象.getClass(); 类名.class; 手写双向链表 package day1501_手写双向链表 ...

随机推荐

  1. cmd关闭端口占用

    netstat -nao |findStr "8080" taskkill /pid 15406  /f

  2. opengl中标准矩形像素点手动网格化为三角形条带的实现

    这里以一张矩形图片为例进行说明: 一张图片的像素点是孤立的,导入opengl中进行绘制出来,看起来没问题,但是当我们放大图片时候,显示的就是一个个孤立的点,而没有像看图软件放大图片那样看起来还是连续的 ...

  3. Linux系统的vsftpd服务配置

    概述: FTP ( 文件传输协议 ) 是 INTERNET 上仍常用的最老的网络协议之一 , 它为系统提供了通过网络与远程服务器进行传输的简单方法FTP 服务器包的名称为 VSFTPD , 它代表 V ...

  4. golang error错误处理

    error定义 数据结构 go语言error是一普通的值,实现方式为简单一个接口. // The error built-in interface type is the conventional i ...

  5. [考试总结]noip模拟40

    最近真的是爆炸啊... 到现在还是有不少没改出来.... 所以先写一下 \(T1\) 的题解.... 送花 我们移动右端点,之后我们用线段树维护全局最大值. 之后还要记录上次的位置和上上次的位置. 之 ...

  6. noip模拟测试50

    考试过程:开题顺序1,2,3,做T1的时候我想到了要求的东西,就是分成尽量少的段使得每段之和>=k,但是我不会求,就打了个暴力走了,然后看T2,这题我觉得和之前做过的一道题比较像,因为我觉得\( ...

  7. Selenium系列(22) - 通过selenium控制浏览器滚动条的几种方式

    如果你还想从头学起Selenium,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1680176.html 其次,如果你不懂前端基础知识, ...

  8. SpringBoot异步使用@Async原理及线程池配置

    前言 在实际项目开发中很多业务场景需要使用异步去完成,比如消息通知,日志记录,等非常常用的都可以通过异步去执行,提高效率,那么在Spring框架中应该如何去使用异步呢 使用步骤 完成异步操作一般有两种 ...

  9. shell--目录通配符

    符号 说明 ? 匹配任一字符 * 匹配一个或多个字符 [a-z0-9] 类似于正则表达式, 若想匹配?可用[?] [!a-z] 类似于正则表达式[^a-z], 不匹配中括号中的内容 {string1, ...

  10. Vue中使用 iview 之-踩坑日记

    导航列表: 一.iview单选框Select验证问题 二.iview表单v-if引起的问题 三.Upload 手动上传组件 使用是出现的问题 四.Tabs嵌套使用时的问题 五.Tooltip 换行问题 ...