浅谈技术组件客户端的并发问题

最近在实现一个基于RabbitMQ的消息总线。因为它提供了Client(客户端),这里就牵扯到凡是技术组件的client都无法回避的并发问题。本文借实现消息总线的client谈谈在实现过程中的想法以及最终的处理方式,当然这些都不仅仅适用于消息总线的client,其他通用组件的client也同样适用。

并发问题的分类

其实上面所提到的并发问题,从大的层面上可以划分为两类问题:

  • 自身固有的并发问题:这个存在的前提条件是client自身内部使用了多线程技术,并且本身就存在线程安全的缺陷。
  • 被动调用的并发问题:指的是该client处于多线程调用环境下产生的线程安全问题。

如果你想单纯得看待怎样的并发问题算是自身固有的并发问题,那么你可以假设一个前提:如果你client处于单线程的被调用环境中,那么你client内部使用了多线程,并且存在线程安全问题,就可以看成是纯粹意义上的自身固有的并发问题。说得再直白一点,如果你内部没有采用多线程,那么这个client你可以认为它不存在自身固有的并发问题。但有时候外部调用的多线程环境也能触发你client内部产生并发问题,这种情况下的并发问题我们将其归类为被动调用的并发问题。

听起来可能有些抽象,我们通过一个实例让它更直观一点。我们看redis的客户端jedis。jedis内部对redis的数据结构命令的调用实现并没有采用多线程技术,因此我们可以认为它没有自身固有的并发问题,但一旦你在多线程的环境下共享其Jedis对象(主对象),那么各种各样莫名其妙的错误就出来了。我们可以认为这是被动调用的并发问题。所以要实现一个绝对线程安全的类是非常不容易的,可以说代价也非常大——因为你必须同时考虑这两种不同的并发问题。换句话说,当你的client内部外部都可能存在多线程环境,那么你必须同时考虑调用以及被调用的线程安全。

为了不让讨论的问题变得大而空,这篇文章我们将关注点放在被动调用的并发问题。

RabbitMQ 通信简介

简单介绍一下RabbitMQ跟通信相关的两个关键对象:Connection、Channel。要通信,肯定要先建立TCP连接,这个过程主要由Connection 负责。RabbitMQ支持从一个Connection创建多个Channel,Channel定义并实现了通信的各种API。因此Connection 主要负责链路的建立,而Channel主要负责通信逻辑。RabbitMQ这么设计是为了避免创建太多Connection(每个Connection都是跟RabbitMQ Server的TCP连接)。而对Channel的设计就是网络中常用的“多路复用”技术。

共享实例

这是我最初的想法:构建完全线程安全的类,无论是怎样的被调用环境,都只使用一个单独的Messagebus 对象。这是对被调环境最友好的方式,但这种情况下你就必须以被调环境是多线程环境为基准来设计client的实现模型。

因此我之前的想法是在client内部建立一个Channel 的对象池。所有的单工(单向)通信,都直接从对象池中获取一个Channel对象,通信结束后再归还Channel到对象池。因为这里的Pool构建在Apache Common Pool的基础上,所以对Channel对象的获取与归还是线程安全的,而Channel对象在RabbitMQ 官方Java client中是被明确标注为线程安全的,因此在整个通信逻辑上没有线程安全问题。但完全共享实例意味着内部关键对象也是共享的,这里涉及到client内部的两个比较关键的对象:

  • Pubsuber:用于从一个pubsuberCenter获取实时配置变更数据
  • ConfigManager:用于解析pubsuberManager push过来的数据并及时调整客户端的控制逻辑

在共享实例的实现方式下,这两个对象提供的API必须是线程安全的。与此同时,所有client主对象的API都必须是同步的(最简单也是性能最差的API同步实现方式是将整个方法直接用synchronized标记,如果整个方法不实现为同步的,就必须在方法内部小心得处理同步问题)。

由共享实例向线程独占过渡

因为多线程问题最主要的根源就是数据共享的问题,因此共享实例的实现方式算是正中下怀,而且这是非常考验实现者并发能力的,本人作为并发新手,如果有办法敬而远之,那么是再好不过的了。因此我的思路逐渐从共享变量向独占过渡。

之前谈到一个Connection可以创建多个Channel。RabbitMQ 官方client在其doc上已经直接说明了:Channel是线程安全的,它直接支持在多线程环境下通信。

因此,我的思路很快就变成了像下面这样:

这种模式下,Connection被实现为单例模式,在一个JVM进程中(不管被调环境是否处于多线程状态)只会存在一个Connection对象。一个Client主对象(就是上图中的Messagebus对象)只关联一个独立的Channel并且约定一个Client主对象只能适用于一个独立的线程,不得跨线程使用。这样,整个client的通信逻辑中就完全不必关注并发问题。但这种模式下,Pubsuber以及ConfigManager仍然是单例的(也就是说是共享实例的模式),因此他们的API还必须实现为线程安全的。

这看起来是一种不错的方法,但当我已经开始动手实现之后,才发现一个问题:如果Messagebus对象在各个线程中是独占的,谁行驶Connection的关闭动作?如果它被某个线程上的Messagebus对象关闭了,对其他线程上正在工作的Messagebus将是一个灾难——它们将以一种极其不优雅的方式抛出异常。共享实例模式下,client主对象倒不存在这个问题,因为其client主对象的控制权完全归主线程所有。

完全线程独占的实现

client主对象以及所有的关联对象由线程独占(Connection对象在不同的线程中也是不同的实例)。这也是jedis的实现方式,其实在思考上面两种方式之前,我就了解了这种实现方式。但我一直在尝试是否还有其他的方式,因为这么做毕竟比较浪费资源——客户端创建多少个通信线程,就至少有多少套对象簇(包括这么多Connection对象)。但这也是最简单、通信性能最好的方式——因为它完全不需要处理被调线程安全问题,并且也不像上面一种思路中需要为谁掌管Connection的控制权而纠结。单个线程的内存模型如下:

也就是说,当你创建一个Messagebus client对象,就会有上面虚线框内的关联对象簇被创建(它们跟client主对象有相同的生命周期)。

这种实现方式是如何解决被调多线程并发问题的?两步:

  • 基于约定:规定不得在多线程之间共享client主对象,否则后果自负
  • Client主对象以及依赖对象完全独占,并创建Client主对象的对象池

现在,在多线程环境下就不存在任何对象共享了:

你可以看一下它的使用示例

这种模式,client的所有实现代码都无需再为线程安全问题而做任何伤害性能的加锁操作而且对象的归属上也非常清晰。但毫无疑问,它也存在一些缺点:

  1. 它浪费了不少资源:不只是客户端的JVM的内存空间,还有RabbitMQ Server的连接资源
  2. 安全隐患:这种模式,如果服务端不做任何处理,客户端甚至可以通过不断创建Messagebus 对象,而发起DDos攻击

其实,归根到底一切都是权衡——看你关注什么又愿意舍弃什么。

总结

其实很多API都无法完全做到十全十美,本文的并发问题只是一个普遍现象,其他的问题还有乱用、错用的问题。比如上面说到的第二种模型,如果我们不谈消息总线,只采用RabbitMQ原生的java client的话,多线程通信时你可以这样:在主线程上创建Connection对象,然后为每个线程分配独立的Channel对象,最终在主线程上关闭Connection对象。但Channel对象开放了获取Connection对象的API,因此也就给了每个线程对Connection的控制权。你只能说技术组件只能顾好自身,它不揣测任何使用场景以及使用意图。

时间: 2024-11-03 21:44:59

浅谈技术组件客户端的并发问题的相关文章

浅谈技术翻译(转自李松峰)

有的译者认为“技术书籍以技术引导为己任,最重要的是让读者入门然后去读英文原文,而不是要传承中华文化”,这种看法你认同吗? 首先,咱们先明确一下“技术书籍”这个概念.因为“技术”是一个很宽泛的词,必须先界定一下.你想,修车是技术,开飞机是技术,甚至养猪.理发.炒菜…等等里面都可以有技术,都有相应的书籍.但我们今天说的“技术书籍”,仅仅指“信息技术类书籍”,具体一点说,就是“计算机和网络技术书籍”.而我们今天谈论的翻译,也仅仅局限于“计算机相关技术书籍”的翻译,而下面的讨论也以英译中为例. 好了,开

浅谈PHP组件、框架以及Composer

本篇文章主要介绍了PHP组件.框架以及Composer,具有一定的学习价值,感兴趣的朋友可以了解一下. 什么是组件 组件是一组打包的代码,是一系列相关的类.接口和Trait,用于帮助我们解决PHP应用中某个具体问题.例如,你的PHP应用需要收发HTTP请求,可以使用现成的组件如guzzle/guzzle实现.我们使用组件不是为了重新实现已经实现的功能,而是把更多时间花在实现项目的长远目标上. 优秀的PHP组件具备以下特性: 作用单一:专注于解决一个问题,而且使用简单的接口封装功能 小型:小巧玲珑

浅谈java中如何处理高并发的问题

1.从最基础的地方做起,优化我们写的代码,减少必要的资源浪费     a.避免频繁的使用new对象,对于整个应用只需要存在一个实例的类,我们可以使用单例模式.对于String连接操作,使用StringBuffer或StringBuilder,对于工具类可以通过静态方法来访问.     b.避免使用错误的方式,尽量不用instanceof做条件判断.使用java中效率高的类,比如ArrayList比Vector性能好. 2.html静态化     我们通过一个链接地址访问,通过这个链接地址,服务器

浅谈技术工程师的进步

本来发微博的,越说越多,算了,发篇博客吧,说点工程师如何取得进步的问题. 1.描述和记录问题要精确,数字化: "负载很高,连接很多,速度很卡"这种描述都是不对的,负载uptime值多少,连接数具体有多少,平时正常多少,高峰多少,访问延迟有多大,全部要数字化,而且要有问题状况下和平时的对比,养成这样的习惯,技术分析能力才会有进步. 2.分析过程要有条理: 出问题找不到原因,不奇怪,我也经常找不到:但是你为找原因做了怎样的努力?有没有努力去记录更详尽的问题日志,有没有通过对比测试排除各种潜

安卓开发_浅谈自定义组件

在Android中,所有的UI界面都是由View类和ViewGroup类及其子类组合而成.其中,View类是所有UI组件的基类,而ViewGroup类是容纳这些UI组件的容器. 其本身也是View类的子类. 在实际开发中,View类还不足以满足程序所有的需求.这时,便可以通过继承View类来开发自己的组件. 开发自定义组件的步骤: 1.创建一个继承android.view.View类的View类,并且重写构造方法. 2.根据需要重写相应的方法. 3.创建并实例化自定义View类,并将其添加到布局

<转>浅谈缓存击穿、缓存并发和缓存失效

原文地址:缓存穿透.缓存并发.缓存失效之思路变迁 我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题: 缓存穿透 缓存并发 缓存失效 一.缓存穿透 注:上面三个图会有什么问题呢? 我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回. 这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了. 那这种问题有什么好

浅谈 XSS & CSRF(转)

浅谈 XSS & CSRF 客户端(浏览器)安全 同源策略(Same Origin Policy) 同源策略阻止从一个源加载的文档或脚本获取或设置另一个源加载的文档的属性. 如: 不能通过Ajax获取另一个源的数据: JavaScript不能访问页面中iframe加载的跨域资源. 对 http://store.company.com/dir/page.html 同源检测 跨域限制 浏览器中,script.img.iframe.link等标签,可以跨域引用或加载资源. 不同于 XMLHttpReq

【转】浅谈分布式服务协调技术 Zookeeper

非常好介绍Zookeeper的文章, Google的三篇论文影响了很多很多人,也影响了很多很多系统.这三篇论文一直是分布式领域传阅的经典.根据MapReduce,于是我们有了Hadoop:根据GFS,于是我们有了HDFS:根据BigTable,于是我们有了HBase.而在这三篇论文里都提及Google的一个Lock Service —— Chubby,哦,于是我们有了Zookeeper. 随着大数据的火热,Hxx们已经变得耳熟能详,现在作为一个开发人员如果都不知道这几个名词出门都好像不好意思跟人

浅谈PHP缓存技术之一

近来做了一阵子程序性能的优化工作,有个比较有意思的想法,想提出来和大家交流一下. Cache是"以空间换时间"策略的典型应用模式,是提高系统性能的一种重要方法.缓存的使用在大访问量的情况下能够极大的减少对数据库操作的次 数,明显降低系统负荷提高系统性能.相比页面的缓存,结果集是一种"原始数据"不包含格式信息,数据量相对较小,而且可以再进行格式化,所以显得相当灵 活.由于php是"一边编译一边执行"的脚本语言,某种程度上也提供了一种相当方便的结果集