6
社区成员
发帖
与我相关
我的任务
分享我也学了一点RPC的知识,现在想做超时重试这一块,用的是Netty跟CompletableFuture来实现,不过我设置了超时get结果,在服务执行时间超过get设置的超时时长客户端会抛出这样的异常
21:28:16.957 cn.fyupeng.net.netty.client.NettyClientHandler [nioEventLoopGroup-3-4] - error occurred while invoking, error information:
java.lang.NullPointerException: null
at cn.fyupeng.net.netty.client.UnprocessedRequests.complete(UnprocessedRequests.java:43)
at cn.fyupeng.net.netty.client.NettyClientHandler.channelRead0(NettyClientHandler.java:48)
at cn.fyupeng.net.netty.client.NettyClientHandler.channelRead0(NettyClientHandler.java:25)
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.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.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:286)
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:1412)
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:943)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:141)
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:886)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
21:28:16.963 cn.fyupeng.net.netty.client.NettyClient [nioEventLoopGroup-3-4] - Error occurred while sending message: {}
java.nio.channels.ClosedChannelException: null
at io.netty.channel.AbstractChannel$AbstractUnsafe.write(...)(Unknown Source)
Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
at com.sun.proxy.$Proxy0.sayHello(Unknown Source)
at cn.fyupeng.Client.main(Client.java:36)
Caused by: java.util.concurrent.ExecutionException: java.nio.channels.ClosedChannelException
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1915)
at cn.fyupeng.proxy.RpcClientProxy.invoke(RpcClientProxy.java:145)
... 2 more
Caused by: java.nio.channels.ClosedChannelException
at io.netty.channel.AbstractChannel$AbstractUnsafe.write(...)(Unknown Source)
客户端读取核心代码:
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponse msg) throws Exception {
try {
log.info(String.format("customer has received message: %s", msg));
/**
* 1. 取出 缓存在 AttributeKey 中 常量池ConstantPool 的 ConcurrentMap<String, RpcResponse>
* key 为 "rpcResponse"的 AttributeKey<RpcResponse>
* 2. 底层原理是 putIfAbsent(name, tempConstant), 只第一次有效,下次不会 put, 只返回 第一次的值
* 3. 一个 AttributeKey 类 分配 一个 常量池,多个AttributeKey 共享
* 4. 多线程环境下 常量池中的 ConcurrentHashMap<String,T > 是共享的,并且是 线程安全的
*/
//AttributeKey<RpcResponse> key = AttributeKey.valueOf(msg.getRequestId());
//ctx.channel().attr(key).set(msg);
System.out.println("unprocessedRequests" + unprocessedRequests);
unprocessedRequests.complete(msg);
//ctx.channel().close();
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("error occurred while invoking, error information:", cause);
ctx.close();
}
实现非阻塞手段
public class UnprocessedRequests {
/**
* k - request id
* v - 可将来获取 的 response
*/
private static ConcurrentMap<String, CompletableFuture<RpcResponse>> unprocessedResponseFutures = new ConcurrentHashMap<>();
/**
* @param requestId 请求体的 requestId 字段
* @param future 经过 CompletableFuture 包装过的 响应体
*/
public void put(String requestId, CompletableFuture<RpcResponse> future) {
unprocessedResponseFutures.put(requestId, future);
}
/**
* 移除 CompletableFuture<RpcResponse>
* @param requestId 请求体的 requestId 字段
*/
public void remove(String requestId) {
unprocessedResponseFutures.remove(requestId);
}
public void complete(RpcResponse rpcResponse) {
System.out.println("complete " + rpcResponse);
CompletableFuture<RpcResponse> completableFuture = unprocessedResponseFutures.remove(rpcResponse.getRequestId());
completableFuture.complete(rpcResponse);
}
}
服务端一切正常
服务端读取和对客户端请求超时重发同一个请求包的处理
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) throws Exception {
try {
/**
* 心跳包 只 作为 检测包,不做处理
*/
if (msg.getHeartBeat()) {
log.trace("receive hearBeatPackage from customer...");
return;
}
log.info("server has received request: {}", msg);
// 到了这一步,如果请求包在上一次已经被 服务器成功执行,接下来要做幂等性处理,也就是客户端设置超时重试处理
/**
* 这里要防止重试
* 分为两种情况
* 1. 如果是 客户端发送给服务端 途中出现问题,请求包之前 服务器未获取到,也就是 唯一请求id号 没有重复
* 2. 如果是 服务端发回客户端途中出现问题,导致客户端触发 超时重试,这时服务端会 接收 重试请求包,也就是有 重复请求id号
*/
// 请求id 为第一次请求 id
Object result = null;
if (timeoutRetryRequestIdSet.add(msg.getRequestId())) {
result = requestHandler.handler(msg);
resMap.put(msg.getRequestId(), result);
//请求id 为第二次或以上请求
} else {
result = resMap.get(msg.getRequestId());
}
// 生成 校验码,客户端收到后 会 对 数据包 进行校验
if (ctx.channel().isActive() && ctx.channel().isWritable()) {
/**
* 这里要分两种情况:
* 1. 当数据无返回值时,保证 checkCode 与 result 可以检验,客户端 也要判断 result 为 null 时 checkCode 是否也为 null,才能认为非他人修改
* 2. 当数据有返回值时,校验 checkCode 与 result 的 md5 码 是否相同
*/
String checkCode = "";
// 这里做了 当 data为 null checkCode 为 null,checkCode可作为 客户端的判断 返回值 依据
if(result != null) {
checkCode = new String(DigestUtils.md5(result.toString().getBytes("UTF-8")));
} else {
checkCode = null;
}
RpcResponse rpcResponse = RpcResponse.success(result, msg.getRequestId(),checkCode);
ChannelFuture future = ctx.writeAndFlush(rpcResponse);
/**
* 及时清除不用的请求 id
* 大于 1000 条请求id 后将请求
* 保存此时 服务接收的请求 id
* 考虑多线程中 对其他 线程刚添加的请求id 进行清除的影响
*/
if (timeoutRetryRequestIdSet.size() >= 1000) {
synchronized (this) {
if (timeoutRetryRequestIdSet.size() >= 1000) {
timeoutRetryRequestIdSet.clear();
resMap.clear();
timeoutRetryRequestIdSet.add(msg.getRequestId());
resMap.put(msg.getRequestId(), result);
}
}
}
} else {
log.error("channel is not writable");
}
/**
* 1. 通道关闭后,对于 心跳包 将不可用
* 2. 由于客户端 使用了 ChannelProvider 来 缓存 channel,这里关闭后,无法 发挥 channel 缓存的作用
*/
//future.addListener(ChannelFutureListener.CLOSE);
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("error occurred while invoking!,info: ", cause);
ctx.close();
}
客户端时请求执行远程方法100次的,因为异常到13次就中断了,这样业务损失太高了,怎么做呀