百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

高并发应用程序的设计原理和模式_高并发编程在业务中的运用

haoteby 2025-09-11 01:08 50 浏览

概述

在这篇文章篇幅中,我们将讨论一些关于高并发应用程序的设计原理和设计模式。

说起高并发的话题,这向来是一个被广泛讨论的议题,同时又是一个很严肃的话题,仁者见仁智者见智,这里并没有一个具体完整的答案,我们这里只是说一些比较流行的技巧。

高并发的基础

在开始我们的讨论之前,我们需要花一些时间理解一些并发的基础,我们必须清楚地知道什么样的程序是一个高并发的程序,一般情况下,我们把需要在同一时间进行多种计算,并且同时发生,甚至有资源竞争的称为高并发。

2.1 如何创建并发模型

理解如何创建并发模型是非常重要的事情,方法有很多种,但是我们一般主要集中在这两种方式:

  • 进程 - 机器上独立运行的程序实例,通常和其他进程是相互隔离的,它拥有自己独立的计算单元和计算空间。多个进程之间一般无法共享内存,它们之间的通信必须通过消息;
  • 线程 - 在进程的内部是线程,多个线程之间可以共享内存来进行通信,但是每个线程拥有自己的栈空间和线程优先级。线程可以是操作系统层面发起的线程,也可以是程序运行时发起的线程。

2.2 并发模型之间如何进行交互

并发模型工作时不进行通信是个很理想的情况,但是这种情况往往不符合实际,我们这里给出了两种方式来完成交互。

  • 共享内存 - 在这种模式中,并发模型通过读写在内存中共享的对象完成交互,但是不正确的交互,往往会导致错误。
  • 消息通信 - 并发模型,通过消息通道进行消息的传递,每个模块顺序的消费消息,根据消息进行自己的业务逻辑,因为没有更多的共享状态,编程比较容易,但是仍然有一些竞争条件增加交互难度。

2.3 并发模型如何执行

现在的计算机世界已经和以前大不相同,摩尔定律在CPU的效率提升方面,已经受到考验很久了。取而代之的可行方案是,业界将更多的处理器打包到同一个芯片上,目前32核心的处理器也不少见。

众所周知,单个计算机核心一次只能执行一个线程或者是一组命令,但是计算机世界中进程和线程是数以万计的,这么多的并行如何执行,这个就是计算机操作系统的智慧了。计算机操作系统通过时间片来分配CPU资源,达到并发的目标,同时,频繁的切换也会带来大量的上下文切换的损耗。

并行编程中的问题

在讨论并行编程的设计原理之前,我们需要首先明确并行编程中的问题。

  • 互斥 - 为了确保程序的正确性,并行程序需要对于要访问的资源具有独占性,共享资源的同步是互斥很普遍的实现,有多种的程序同步原语可供使用,如锁,信号量,互斥量等。但是进行互斥的并行编程很容易出错,业界有很多关于死锁的讨论。
  • 上下文切换 - 操作系统通过时间片来分配计算资源模拟并发,进行线程调度是操作系统的常规操作,核心在多个线程之间必不可少的要进行切换,切换之后恢复线程现场,这些都是很费时的操作,是影响程序性能的重点问题。

高并发设计模式

了解并发编程的问题之后,我们来看看如何解决这些问题,我们必须重申,并发编程是一项艰巨的任务,需要大量的经验。 因此,遵循一些已建立的模式可以使它更容易。

4.1 以Actor模型为基础的并发

我们首先要讨论的并发编程模型称为Actor模型,它是一个基本的概念模型,基本理念是万物皆可Actor,每个Actor接收消息,并用本地策略来响应消息。这种概念首次是被Carl Hewitt 提出来的,后来在很多的编程语言中得到广泛应用。

Scala语言在并发编程方面的主要就是应用的Actor, Actor是Scala中的普通对象,我们可以通过实例化Actor类来创建。 此外,Scala Actors库还提供了许多有用的actor操作。

class myActor extends Actor {
    def act() {
        while(true) {
            receive {
                // Perform some action
            }
        }
    }
}

在上面的示例中,程序在无限循环保持对消息的监听,当消息到达之后,该消息将从Actor的邮箱中删除,并进行对应的响应。

Actor模型消除了并发编程的基本问题-共享内存, Actor通过消息进行通信,每个Actor依次处理其专用邮箱中的消息。 我们在线程池上执行actor, 而且我们已经看到,本机线程可能是重量级的,因此数量有限。

4.2 基于事件的并发

基于事件的设计显式解决了本机线程生成和操作成本高昂的问题。 基于事件的高并发主要依赖于事件循环(event loop)。 在event loop中一般有一个事件提供程序和一系列的事件处理程序一起使用。 在这样的设计中,event loop 阻塞在事件等待上,当事件到达后,将事件分发给一系列的事件处理程序。

基本上,事件循环不过是事件分配器而已! 事件循环本身可以在单个本机线程上运行。 那么,事件循环中真正发生的事情是什么? 让我们来看一个非常简单的事件循环的伪代码作为示例:

while(true) {
    events = getEvents();
    for(e in events)
        processEvent(e);
}

基本上,我们的event loop需要做的工作就是不断地获取事件,并在发现事件时对其进行处理, 该方法确实很简单,但可以从事件驱动的设计中受益。上面的设计能够消除多线程程序中一些典型问题,比如死锁。

JavaScript通过实现event loop从而提供了一种异步编程的能力, 它维护一个调用堆栈,以跟踪所有要执行的功能,同时,它还维护一个事件队列,用于发送新的需要处理的方法调用消息。 事件循环不断检查调用堆栈,并从事件队列中获取新的事件,并分派到由浏览器提供的Web API。

事件循环本身可以在单个线程上运行,但是Web API运行在另外的单独线程中。

4.3 非阻塞算法

在非阻塞算法中,一个线程的挂起不会导致其他线程的挂起,我们知道在我们的应用程序中只能有数量有限的本机线程,如果程序中使用阻塞算法会严重降低程序的性能。

非阻塞算法充分利用底层硬件提供的数据比较和交换原语,这里则意味着硬件将比较内存位置的内容与给定值,并且只有它们相同时,才会将值更新为新的给定值,这看起来很简单,但实际上为我们提供了一个原子操作,否则将需要同步,就是我们常说的CAS。

充分利用这样的特性,我们必须编写利用符合这种原语操作的新数据结构,这为我们的程序开启了一扇新的窗口,我们可以很容易的实现免等待和无锁化编程。 Java具有几种非阻塞数据结构,例如AtomicBoolean,AtomicInteger,AtomicLong和AtomicReference。

编程中,经常碰到多个线程试图访问同一代码的应用程序:

boolean open = false;
if(!open) {
    // Do Something
    open=false;
}

显然,上面的代码不是线程安全的,并且它在多线程环境中的行为可能是不可预测的。 我们在这里的选择是使这段代码与锁同步,或者使用原子操作:

AtomicBoolean open = new AtomicBoolean(false);
if(open.compareAndSet(false, true) {
    // Do Something
}

如我们所见,使用像AtomicBoolean这样的非阻塞数据结构可以帮助我们编写线程安全的代码,而不用太多的关注锁的细节。

编程语言的支持

我们可以通过多种方式来构造出并发模型,尽管编程语言在实现方面有所不同,但主要都是依赖底层操作系统如何支持这一概念。但是,本机线程有所限制,在基于线程的并发在可伸缩性方面遇到了新的瓶颈之后,我们必须考虑新的实现手段。

事实证明,我们上面讨论的一些设计实践确实非常有效,但是,它为我们带来益处的同时,也让编程更加复杂,我们真正需要的是并发能力,而不一定是多线程。

我们还有一种有效的方法来实现这些,我们称之为绿色线程(Green Threads)。绿色线程是由运行时库调度的线程,而不是由底层操作系统本机调度的线程。尽管这并不能解决基于线程的并发中的所有问题,但在某些情况下,它肯定可以为我们提供更好的性能。

现在,除非编程语言中默认支持这些绿色线程的方式,不然我们很难实现。并非每种编程语言都具有此内置支持,同样,我们可以通过不同的编程语言以非常独特的方式来实现我们所谓的绿色线程,让我们来看一些可用的选项。

5.1 Go语言的实现-Goroutines

Go编程语言中的Goroutine是轻量级线程, 他们具有能力是方法并行执行。 Goroutine的使用成本比较低,只占用几千字节大小的堆栈就可以实现。

最重要的是,goroutines与较少数量的本机线程进行多路复用。 此外,goroutine使用通道相互通信,从而避免了对共享内存的访问。

5.2 Erlang语言的实现-process

在Erlang中,每个执行线程都称为一个process, 但是,它与我们平时所说的线程大不一样! Erlang process非常轻量级,内存占用小,并且创建和处理速度快,调度开销低。

在幕后,Erlang process只是运行时调度的一些 方法, 而且,Erlang进程不共享任何数据,它们通过消息传递相互通信。

5.3 Java中一些提案-Fibers (纤程)

Java并发的讨论一直在不断发展,Java确实从一开始就对绿色线程(至少对Solaris操作系统)提供了支持。 但是,由于存在超出本教程范围的障碍,因此该方法已被中止。

从那时起,Java中的并发全部与本机线程有关,以及如何巧妙地使用它们。但是出于明显的原因,我们可能很快就会在Java中有了一个新的并发抽象,称为 Fiber。 Project Loom建议将继续引入Fiber,这可能会改变我们用Java编写并发应用程序的方式。

语言支持总结

这只是不同编程语言中关于绿色线程的简要介绍,其他编程语言还尝试了更多有趣的方式来处理并发,在这里抛砖引玉。

高并发应用

实际应用程序通常具有多个层次,多个层次互相交互来为我们提供服务,比如可能包含代理服务,网关,Web服务,数据库,目录服务和文件系统。

在这种情况下,我们如何确保高并发性? 让我们探讨其中的一些程序分层以及构建高度并发应用程序的选择。

6.1 web层

Web层通常是用户请求到达的第一层,因此在此处不可避免地需要进行高并发性设置。 让我们看看其中的一些选项:

  • Node(也称为NodeJS或Node.js)是基于Chrome的V8 JavaScript引擎构建的开源,跨平台JavaScript运行时。 在处理异步I / O操作时,Node工作得很好。 Node之所以如此出色,是因为它在单个线程上实现了一个事件循环, 借助回调的事件循环可异步处理所有阻塞操作,例如I/O。
  • nginx是一个开放源代码的Web服务器,除其他用法外,我们通常将其用作反向代理。 nginx提供高并发性的原因是它使用了异步的,事件驱动的方法。 nginx在单个线程中运行master处理, 在主流程(master process)中维护执行实际业务处理的工作流程。 因此,工作进程同时处理每个请求。

6.2 应用层

在设计应用程序时,有多种工具可帮助我们构建高并发性。

  • Akka是用Scala编写的工具包,用于在JVM上构建高度并发和分布式的应用程序。 Akka处理并发的方法基于我们前面讨论的参与者模型。 Akka在参与者和基础系统之间创建了一个层。该框架处理创建和调度线程,接收和调度消息。
  • Project Reactor是一个反应式库,用于在JVM上构建非阻塞应用程序。它基于Reactive Streams规范,侧重于有效的消息传递和需求管理(背压)。Reactor的操作和调度可以维持消息的高吞吐率,几个流行的框架提供了Reactor的实现,包括Spring WebFlux 和 RSocket。
  • Netty是一个异步的,事件驱动的网络应用程序框架。我们可以使用Netty开发高度并发的协议服务器和客户端。 Netty利用了NIO,它是Java API的集合,可通过缓冲区和通道提供异步数据传输。它为我们提供了许多优势,例如更好的吞吐量,更低的延迟,更少的资源消耗以及最小化不必要的内存复制(零拷贝)。

6.4 数据层

最后,没有数据就没有完整的应用程序,数据来自持久性存储。当我们讨论有关数据库的高并发性时,大多数重点仍然放在NoSQL系列上。这主要是因为NoSQL数据库可以提供线性可伸缩性,但是在关系型变量中却很难实现。让我们看一下数据层的两个流行工具:

  • Cassandra是一个免费的开源NoSQL分布式数据库,可在商品硬件上提供高可用性,高可伸缩性和容错能力。但是,Cassandra不提供跨越多个表的ACID事务。因此,如果我们的应用程序不需要强一致性和事务性,那么我们可以从Cassandra的低延迟操作中受益。
  • Kafka是一个分布式流媒体平台。 Kafka将记录流存储在称为主题的类别中。它可以为记录的生产者和使用者提供线性水平可伸缩性,同时提供高可靠性和耐用性。分区,副本和代理是提供大量分布式并发的基本概念。

6.4 缓存层

现在,任何的高并发啊程序都无法承受每一次进行数据库的访问,数据库不能承受其重,这就让我们选择了一个缓存-最好是可以支持我们高度并发的应用程序的内存中缓存:

  • Hazelcast是一个分布式,云友好的内存中对象存储和计算引擎,它支持各种数据结构,例如Map,Set,List,MultiMap,RingBuffer和HyperLogLog。它具有内置的复制功能,并提供高可用性和自动分区。
  • Redis是一种内存数据结构存储,我们主要将其用作缓存。它提供了一个内存中的键值数据库,该数据库具有可选的持久性。支持的数据结构包括字符串,哈希,列表和集合。 Redis具有内置的复制功能,并提供高可用性和自动分区。如果我们不需要持久性,Redis可以为我们提供功能丰富,网络化的内存中缓存,并具有出色的性能。

当然,在我们追求构建高度并发的应用程序时,我们几乎没有涉及任何可以直接使用的内容。重要的是一直在介绍进行并发设计的时候一定要选择适当的设计。这些选项中的某些选项可能是合适的,而其他选项则可能不合适。

而且,别忘了还有更多可用的选项可能更适合我们的要求。^_^

结论

在本文中,我们讨论了并发编程的基础, 我们了解了并发的一些基本方面以及它可能导致的问题。 此外,我们经历了一些设计模式和原则,这些设计模式可以帮助我们避免并发编程中的典型问题。

最后,我们介绍了一些可用于构建高度并行的端到端应用程序的框架,库和软件。

译于 2021/04/26 23:50 程序员-刘高飞

相关推荐

如何随时清理浏览器缓存_清理浏览器缓存怎么弄

想随时清理浏览器缓存吗?Cookieformac版是Macos上一款浏览器缓存清理工具,所有的浏览器Cookie,本地存储数据,HTML5数据库,FlashCookie,Silverlight,...

Luminati代理动态IP教程指南配置代理VMLogin中文版反指纹浏览器

介绍如何使用在VMLogin中文版设置Luminati代理。首先下载VMLogin中文版反指纹浏览器(https://cn.vmlogin.com)对于刚接触Luminati动态ip的朋友,是不是不懂...

mac清除工具分享,解除您在安全方面的后顾之忧

想要永久的安全的处理掉重要数据,删除是之一,使用今天小编分享的mac清除工具,为您的操作再增一层“保护”,小伙伴慎用哟,一旦使用就不可以恢复咯,来吧一起看看吧~mac清除工具分享,解除您在安全方面的后...

取代cookie的网站追踪技术:”帆布指纹识别”

【前言】一般情况下,网站或者广告联盟都会非常想要一种技术方式可以在网络上精确定位到每一个个体,这样可以通过收集这些个体的数据,通过分析后更加精准的去推送广告(精准化营销)或其他有针对性的一些活动。Co...

辅助上网为啥会被抛弃 曲奇(Cookie)虽甜但有毒

近期有个小新闻,大概很多小伙伴都没有注意到,那就是谷歌Chrome浏览器要弃用Cookie了!说到Cookie功能,很多小伙伴大概觉得不怎么熟悉,有可能还不如前一段时间被弃用的Flash“出名”,但它...

浏览器指纹是什么?浏览器指纹包括哪些信息

本文关键词:浏览器指纹、指纹浏览器、浏览器指纹信息、指纹浏览器原理什么是浏览器指纹?浏览器指纹是指浏览器的各种信息,当我们访问其他网站时,即使是在匿名的模式下,这些信息也可以帮助网站识别我们的身份。...

那些通用清除软件不曾注意的秘密_清理不常用的应用软件

系统清理就像卫生检查前的大扫除,即使你使出吃奶的劲儿把一切可能的地方都打扫过,还会留下边边角角的遗漏。随着大家电脑安全意识的提高,越来越多的朋友开始关注自己的电脑安全,也知道安装360系列软件来"武装...

「网络安全宣传周」这些安全上网小知识你要知道!

小布说:互联网改变了人们的衣食住行,但与之伴生的网络安全威胁也不容忽视。近些年来,风靡全球的勒索病毒、时有发生的电信诈骗、防不胜防的个人信息泄露时时刻刻都威胁着我们的生活。9月18日-24日是第四届...

TypeScript 终极初学者指南_typescript 进阶

在过去的几年里TypeScript变得越来越流行,现在许多工作都要求开发人员了解TypeScript...

jQuery知识一览_jquery的认识和使用

一、概览jQuery官网:https://jquery.com/jQuery是一个高效、轻量并且功能丰富的js库。核心在于查询query。...

我的第一个Electron应用_electronmy

hello,好久不见,最近笔者花了几天时间入门Electron,然后做了一个非常简单的应用,本文就来给各位分享一下过程,Electron大佬请随意~笔者开源了一个Web思维导图,虽然借助showSav...

HTML5 之拖放(Drag 和 Drop)_html拖放api

简介拖放是一种常见的特性,即抓取对象以后拖到另一个位置。在HTML5中,拖放是标准的一部分,任何元素都能够拖放。先点击一个小例子:在用户开始拖动<p>元素时执行JavaScrip...

如何用JavaScript判断输入值是数字还是字母?

在日常开发中,我们有时候需要判断用户输入的是数字还是字母。本文将介绍如何用JavaScript实现这一功能。检查输入值是否是数字或字母...

图形编辑器开发:快捷键的管理_图形编辑工具

大家好,我是前端西瓜哥。...

浏览器原生剪贴板:原来它能这样读取用户截图!

当我们使用GitHub时,会发现Ctrl+V就能直接读取用户剪贴板图片进行粘贴,那么它是如何工作的?安全性如何?...