在前两个版本中,每次发起请求一次就新建一个netty的channel连接,如果在高并发情况下就会造成资源的浪费,这时实现异步请求就十分重要,当有多个请求线程时,需要设计一个线程池来进行管理。除此之外,当前方法过于依赖注册中心,在高并发情况下对注册中心造成了压力;另外如果注册中心出现宕机等情况,那么整合系统就崩溃了,为了解决这个问题,添加了一个适合高并发的服务缓存机制。以上为该版本的新增内容。

异步请求和线程池

这里就不具体介绍异步请求的概念了。用一个通俗的例子解释,如你在饭店点餐,当你点好餐后,会得到一个点餐号,但是饭菜并不会立即做好送过,需要你等待一段时间,在这个时间段中,你可以做其他的事情,当饭菜做好后,会根据点餐号进行广播,通知你去拿饭菜。这就是一个典型的异步处理。

在项目中涉及到异步的主要有三个自定义类,即ChannelHolder,LwRequestPoolLwRequestManager

ChannelHolder中定义的变量:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChannelHolder {
private Channel channel;
private EventLoopGroup eventLoopGroup;
}

LwRequestManager中的变量:

private static final ConcurrentHashMap<String, ChannelHolder> channelHolderMap = new ConcurrentHashMap<>();
private static ExecutorService requestExecutor = new ThreadPoolExecutor(30, 100, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(30),
new BasicThreadFactory.Builder().namingPattern("request-service-connector-%d").build()); private static LwRequestPool requestPool = SpringBeanFactory.getBean(LwRequestPool.class);

LwRequestPool中定义的变量:

private final ConcurrentHashMap<String, Promise<LwResponse>> requestPool = new ConcurrentHashMap<>();

刚开始在动态代理中会调用send()方法,开始了有关异步调用的内容。通过requestId来确定是哪个请求,利用线程池执行netty客户端的运行,并利用CountDownLatch来先暂停下面代码的运行,如果latch执行了countDown()方法,会再返回这里执行下面的步骤。

  public static void send(LwRequest request, URL url) throws Exception{
String requestId = request.getRequestId();
CountDownLatch latch = new CountDownLatch(1);
requestExecutor.execute(new NettyClient(requestId, url, latch));
latch.await();
ChannelHolder channelHolder = channelHolderMap.get(requestId);
channelHolder.getChannel().writeAndFlush(request);
log.info("客户端发送消息:{}", channelHolder);
}

之后运行Netty客户端中的run()方法,如果与服务端连接成功,将该请求id和对应的channel注册到channelHolderMap变量中,并执行submitRequest方法,将请求id和eventLoop注册到变量requestPool中。最后执行了countDown()方法。

 @Override
public void run() {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(65535, 0, 4));
pipeline.addLast(new LwRpcEncoder(LwRequest.class, new HessianSerializer()));
pipeline.addLast(new LwRpcDecoder(LwResponse.class, new HessianSerializer()));
pipeline.addLast(clientHandler);
}
});
try {
ChannelFuture future = bootstrap.connect(url.getHostname(), url.getPort()).sync();
//连接成功
if (future.isSuccess()) {
ChannelHolder channelHolder = ChannelHolder.builder()
.channel(future.channel())
.eventLoopGroup(group).build();
LwRequestManager.registerChannelHolder(requestId, channelHolder);
latch.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

requestPool.submitRequest(requestId, channelHolder.getChannel().eventLoop()); public void submitRequest(String requestId, EventExecutor executor) {
requestPool.put(requestId, new DefaultPromise<>(executor));
}

当执行了countDown()方法,会跳转到原来最初的地方,执行剩下的代码部分,进行请求发送。等待服务端的响应。

ChannelHolder channelHolder = channelHolderMap.get(requestId);
channelHolder.getChannel().writeAndFlush(request);

当客户端接收到服务端发回的结果信息时,会执行notifyRequest方法。

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, LwResponse response) throws Exception {
lwRequestPool.notifyRequest(response.getRequestId(), response);
}

notifyRequest方法中,会从变量requestPool中获取到返回的LwResponse变量,并封装在Promise中,最后调用setsuccess()方法。

public void notifyRequest(String requestId, LwResponse response) {
Promise<LwResponse> promise = requestPool.get(requestId);
if (promise != null) {
promise.setSuccess(response);
}
}

setsuccess()方法是netty的Promise中的方法。它会通知所有的监听器。在官方解释如下:

Marks this future as a success and notifies all

此时就可以通过fetchResponse根据请求id获取到了服务端发送过来的消息,此时已经执行完毕,需要从requestpool中删除该请求信息。

 LwResponse response = lwRequestPool.fetchResponse(requestId);

 public LwResponse fetchResponse(String requestId) throws Exception {
Promise<LwResponse> promise = requestPool.get(requestId);
if (promise == null)
return null;
LwResponse response = promise.get(10, TimeUnit.SECONDS);
requestPool.remove(requestId); LwRequestManager.destroyChannelHolder(requestId);
return response;
}

高并发下的缓存机制

在原来的版本中,每次请求远程服务时,都需要从注册中心获取服务地址,在高并发情况下,会对注册中心造成一定的影响;或者如果注册中心突然宕机,那么就无法获取待服务地址,整个系统就崩溃了。所以设计一个缓存机制,将请求到的服务地址持久化到本地,当下次请求时,就无须再需要注册中心了,直接从持久化文件中获取,减轻了注册中心的压力。

在进行本地缓存时,会先调用saveServices方法,将URL数组信息保存到Properties中,并获取当前version版本号,然后执行doSaveProperties方法来保存到本地。这个步骤支持同步和异步两种方式。

public void saveServices(String serviceName, List<URL> urlList) {
if (file == null)
return;
try {
StringBuilder buf = new StringBuilder();
for(URL url : urlList) {
if (buf.length() > 0) {
buf.append(";");
}
buf.append(url.getAllInformation());
}
properties.setProperty(serviceName, buf.toString());
long version = lastCacheChanged.incrementAndGet();
if (syncSaveFile) {
doSaveProperties(version);
} else {
registerCacheExecutor.execute(new SaveProperties(version));
} } catch (Throwable t) {
log.warn(t.getMessage(), t);
}
}

doSaveProperties方法中,如果传入的版本号不是最新的版本号,说明其他线程已经修改了,内容发生了变化,直接退出。在写入到文件时会添加锁,进一步保证信息的准确性。如果添加失败,会进行重试操作。

private void doSaveProperties(long version) {
if (version < lastCacheChanged.get())
return;
if (file == null)
return;
try {
File lockfile = new File(file.getAbsolutePath() + ".lock");
if (!lockfile.exists()) {
lockfile.createNewFile();
}
try(RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
FileChannel channel = raf.getChannel();) {
FileLock lock = channel.tryLock();
if (lock == null) {
throw new IOException("不能锁住注册的缓存文件");
}
try {
if (!file.exists()) {
file.createNewFile();
}
try (FileOutputStream outputFile = new FileOutputStream(file)) {
properties.store(outputFile, "RPC Server Cache");
}
} finally {
lock.release();
}
}
}catch (Throwable e) {
savePropertiesRetryTimes.incrementAndGet();
if (savePropertiesRetryTimes.get() > SAVE_MAX_RETRY) {
log.warn("超过最大重试次数,缓存失败!");
savePropertiesRetryTimes.set(0);
return;
}
if (version < lastCacheChanged.get()) {
savePropertiesRetryTimes.set(0);
return;
}
e.printStackTrace();
}
}

具体详细代码可以到我的项目中进行查看:轻量级RPC第三版

轻量级RPC设计与实现第三版的更多相关文章

  1. 轻量级RPC设计与实现第五版(最终版)

    在最近一段时间里,通过搜集有关资料加上自己的理解,设计了一款轻量级RPC,起了一个名字lightWeightRPC.它拥有一个RPC常见的基本功能.主要功能和特点如下: 利用Spring实现依赖注入与 ...

  2. 轻量级RPC设计与实现第四版

    在本版本中引入了SPI机制,关于Java的SPI机制与Dubbo的SPI机制在以前的文章中介绍过. 传送门:Dubbo的SPI机制与JDK机制的不同及原理分析 因为设计的RPC框架是基于Spring的 ...

  3. 轻量级RPC设计与实现第二版

    在上一个版本中利用netty实现了简单的一对一的RPC,需要手动设置服务地址,限制性较大. 在本文中,利用zookeeper作为服务注册中心,在服务端启动时将本地的服务信息注册到zookeeper中, ...

  4. 轻量级RPC设计与实现第一版

    什么是RPC RPC (Remote Procedure Call Protocol), 远程过程调用,通俗的解释就是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应 ...

  5. 微博轻量级RPC框架Motan

    Motan 是微博技术团队研发的基于 Java 的轻量级 RPC 框架,已在微博内部大规模应用多年,每天稳定支撑微博上亿次的内部调用.Motan 基于微博的高并发和高负载场景优化,成为一套简单.易用. ...

  6. 微博轻量级RPC框架Motan正式开源:支撑千亿调用

    支撑微博千亿调用的轻量级 RPC 框架 Motan 正式开源了,项目地址为https://github.com/weibocom/motan. 微博轻量级RPC框架Motan正式开源 Motan 是微 ...

  7. JavaScript高级程序设计第三版.CHM【带实例】

    从驱动全球商业.贸易及管理领域不计其数的复杂应用程序的角度来看,说 JavaScript 已经成为当今世界上最流行的编程语言一点儿都不为过. JavaScript 是一种非常松散的面向对象语言,也是 ...

  8. 笨办法学 Python (第三版)(转载)

    笨办法学 Python (第三版) 原文地址:http://blog.sina.com.cn/s/blog_72b8298001019xg8.html   摘自https://learn-python ...

  9. C# 的轻量级 RPC 框架

    Redola.Rpc 的一个小目标 Redola.Rpc 的一个小目标 Redola.Rpc 的一个小目标:20000 tps. Concurrency level: 8 threads Comple ...

随机推荐

  1. 搭建django项目连接mysql数据库环境

    开通博客园这么久,即将写下第一篇博客,十分兴奋.首先了,庆祝自己写下了码农生涯博客园第一篇博客,其次了,庆祝自己经过了10个小时奋战,终于成功搭建django项目连接mysql数据库的环境.在此过程中 ...

  2. Tomcat配置多线程和配置数据库连接池

    Tomcat配置多线程和配置数据库连接池 1.  tomcat配置线程池: [root@RD2_AS yanghuihui]# cd /usr/tomcat/conf/ [root@RD2_AS co ...

  3. Codeforces 1092 D2 Great Vova Wall (Version 2) (栈)

    题意: 给一排砖,每列的高度$a_i$,问是否可以放1*2的砖,使得n列高度一样,砖只能横着放 思路: 每两个相邻的高度相同的砖可以变成大于等于它的高度的任意高度 所以像这样的 123321 是不满足 ...

  4. NR / 5G - MAC Overview

  5. Software Testing Concepts

    Software Testing Concepts

  6. 【人类观察所】"当代人"正经历的生活

    一."即时满足"的互联网 "轻微烦躁,偶尔自燃,当代生活多数时刻的心情基调." 如果你出生于上个世纪,应该能明白木心的<从前慢>里的 「从前的日色变 ...

  7. k8s系列---dns部署

    1:首先创建kube-dns和dnsmasq这两个yaml,然后生成相应的pod.svc等. 2:然后在去创建其他的验证pod和svc 3:验证nslookup解析的是其他pod的svc的name,而 ...

  8. Red Team 指南--第2章开源情报(OSINT)侦察

    第2章开源情报(OSINT)侦察 贡献者:伊恩·巴维斯翻译:BugMan 哇,慢点牛仔吧!在我们深入探讨“做性感时光”(笑话)红队闻名的黑客冒险,还有一些作业要做.五分之一的专业人士从未学习或做过任何 ...

  9. session和cookie的最深刻理解

    先说session 对SESSION的争论好象一直没有停止过,不过幺麽能理解SESSION的人应该占90以上.但还是讲讲,别嫌老~ 有一些人赞成用SESSION,有一些人不赞成.但这个问题到底要怎么说 ...

  10. 【Go语言系列】2.3、Go语言基本程序结构:变量及常量

    1.什么变量 变量来源于数学,从根本上说,变量相当于是对一块数据存储空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后可以通过引用变量名来使用这块存储空间. 1.1变量声明 Go 语言变 ...