怎样学习才能拥有所谓“高并发”的经验?

作者:编程指北
链接:https://www.zhihu.com/question/21177474/answer/1959689012
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

坐标鹅厂,个人觉得高并发本身并不是什么很深不可测的东西,一切的解决方案都来自于业务场景。与其去天天想着怎么获得高并发经验,不如去钻钻底层知识,比如算法、缓存、多线程、并发、JVM、OS、网络、体系结构。好的架构都是演进出来的,而不是设计出来的,多看、多模仿就行了,这玩意常见的套路就那么多:Scale-out(横向扩展)异步处理(队列、MQ)缓存首先分享一个github 仓库:System-design-primerimarvinle/system-design-primer​github.com/imarvinle/system-design-primer/blob/master/README-zh-Hans.md包含了系统设计的方方面面,从客户端、DNS、CDN、负债均衡,队列、缓存等等:在这分享一篇我公众号发过的系统设计学习文章:原文:女朋友都能看懂的「系统设计指北」来源:公众号「编程指北」,分享硬核技术、校招经验、CS学习等,微信搜索即可关注在这呢,也分享一份我整理的计算机经典电子书,多去啃一些经典的书,对于学习计算机的同学帮助非常大,且十分系统:书单推荐,少即是多(含下载方式)其实现在很多所谓的高并发,我感觉就三点:缓存:使⽤缓存来提⾼系统的性能,就好⽐⽤“拓宽河道”的⽅式抵抗⾼并发⼤流量的冲 击,而且缓存的各种应用模式在CPU Cache中也有非常经典的案例,把体系结构学好了,缓存这些只是新瓶装旧酒 比如大家拆红包,偶尔会发现明明点开的时候还显示还有,但是点击「拆」却展示已经被抢完,这就是缓存数据和数据库里的不一致带来的,但是这里我们的体验是允许这样的不一致的。2.异步:在某些场景下,我们不一定要实时流程中做完所有步骤,在做完一些必备的步骤后就可以返回了,剩下的丢到一个MQ或者发个事务消息,异步去完成,比如支付宝每年瓜五福就是,转账是异步去弄的3. Scale-out(横向扩展),这是最简单粗暴,也是最有效的方式,请求量大了,横向堆机器就好了,当然这需要你写的程序能够支持分布式、横向扩展之前我写过一篇所谓的高并发、高性能这方面的一些常见措施总结:来源:公众号「编程指北」,分享硬核技术、CS学习方法、校招打法等 原文:https://mp.weixin.qq.com/s/sOmU5xQBYX0lY8qTeROKnQ这篇回答将总结一下后台服务器开发中有哪些常用的解决“三高”问题的方法和思想。希望这些知识,能够给你一丝启发和帮助,助力你收割 各大公司 Offer~PS: 码字不易,希望大家觉得有帮助的话,帮忙双击点个赞,笔芯先上本文思维导图:首先在这,送大家一份我自己整理的电子书库,绝不是在网上那种打包下载的,而是自己需要学到某个方向知识的时候,需要看了,去网上挨个找的,最后汇总而成。这部分我是会不断把它完善的,当成自己的小电子书库,不多,但贵在精。我整理的这些书大家可以在这里获取,对于学习计算机的同学帮助非常大,且十分系统:书单:编程指北:计算机必读的书单(含下载方式)一、缓存什么是缓存?看看维基百科怎么说: In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere. 在计算机中,缓存是存储数据的硬件或软件组件,以便可以更快地满足将来对该数据的请求。 存储在缓存中的数据可能是之前计算结果,也可能是存储在其他位置的数据副本。缓存本质来说是使用空间换时间的思想,它在计算机世界中无处不在, 比如 CPU 就自带 L1、L2、L3 Cache,这个一般应用开发可能关注较少。但是在一些实时系统、大规模计算模拟、图像处理等追求极致性能的领域,就特别注重编写缓存友好的代码。什么是缓存友好?简单来说,就是代码在访问数据的时候,尽量使用缓存命中率高的方式。这个后面可以单独写一篇 CPU 缓存系统以及如何编写缓存友好代码的文章。1.1 缓存为什么有效?缓存之所以能够大幅提高系统的性能,关键在于数据的访问具有局部性,也就是二八定律:「百分之八十的数据访问是集中在 20% 的数据上」。这部分数据也被叫做热点数据。缓存一般使用内存作为存储,内存读写速度快于磁盘,但容量有限,十分宝贵,不可能将所有数据都缓存起来。如果应用访问数据没有热点,不遵循二八定律,即大部分数据访问并没有集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。每次访问都会回源到数据库查询,那么反而会降低数据访问效率。1.2 缓存分类1. 本地缓存: 使用进程内成员变量或者静态变量,适合简单的场景,不需要考虑缓存一致性、过期时间、清空策略等问题。 可以直接使用语言标准库内的容器来做存储。例如: 2. 分布式缓存: 当缓存的数据量增大以后,单机不足以承载缓存服务时,就要考虑对缓存服务做水平扩展,引入缓存集群。 将数据分片后分散存储在不同机器中,如何决定每个数据分片存放在哪台机器呢?一般是采用一致性 Hash 算法,它能够保证在缓存集群动态调整,不断增加或者减少机器后,客户端访问时依然能够根据 key 访问到数据。 一致性 Hash 算法也是值得用一篇文章来讲的,如果暂时还不懂的话可以去搜一下。 常用的组件有 Memcache、 Redis Cluster 等,第二个是在高性能内存存储 Redis 的基础上,提供分布式存储的解决方案。 1.3 缓存使用指南1. 适合缓存的场景:读多写少: 比如电商里的商品详情页面,访问频率很高,但是一般写入只在店家上架商品和修改信息的时候发生。如果把热点商品的信息缓存起来,这将拦截掉很多对数据库的访问,提高系统整体的吞吐量。 因为一般数据库的 QPS 由于有「ACID」约束、并且数据是持久化在硬盘的,所以比 Redis 这类基于内存的 NoSQL 存储低不少。常常是一个系统的瓶颈,如果我们把大部分的查询都在 Redis 缓存中命中了,那么系统整体的 QPS 也就上去了。 计算耗时大,且实时性不高: 比如王者荣耀里的全区排行榜,一般一周更新一次,并且计算的数据量也比较大,所以计算后缓存起来,请求排行榜直接从缓存中取出,就不用实时计算了。 2. 不适合缓存的场景:写多读少,频繁更新。对数据一致性要求严格: 因为缓存会有更新策略,所以很难做到和数据库实时同步。数据访问完全随机: 因为这样会导致缓存的命中率极低。1.4 缓存更新的策略如何更新缓存其实已经有总结得非常好的「最佳实践」,我们按照套路来,大概率不会犯错。主要分为两类 Cache-Aside 和 Cache-As-SoR。 SoR 即「System Of Record,记录系统」,表示数据源,一般就是指数据库。1、Cache-Aside:这应该是最容易想到的模式了,获取数据时先从缓存读,如果 cache hit 则直接返回,没命中就从数据源获取,然后更新缓存。写数据的时候则先更新数据源,然后设置缓存失效,下一次获取数据的时候必然 cache miss,然后触发回源。直接看伪代码:可以看到这种方式对于缓存的使用者是不透明的,需要使用者手动维护缓存。2、Cache-As-SoR:从字面上来看,就是把 Cache 当作 SoR,也就是数据源,所以一切读写操作都是针对 Cache 的,由 Cache 内部自己维护和数据源的一致性。这样对于使用者来说就和直接操作 SoR 没有区别了,完全感知不到 Cache 的存在。CPU 内部的 L1、L2、L3 Cache 就是这种方式,作为数据的使用方应用程序,是完全感知不到在内存和我们之间还存在几层的 Cache,但是我们之前又提到编写 “缓存友好”的代码,不是透明的吗?这是不是冲突呢?其实不然,缓存友好是指我们通过学习了解缓存内部实现、更新策略之后,通过调整数据访问顺序提高缓存的命中率。Cache-As-SoR 又分为以下三种方式:Read Through:这种方式和 Cache-Aside 非常相似,都是在查询时发生 cache miss 去更新缓存,但是区别在于 Cache-Aside 需要调用方手动更新缓存,而 Cache-As-SoR 则是由缓存内部实现自己负责,对应用层透明。Write Through: 直写式,就是在将数据写入缓存的同时,缓存也去更新后面的数据源,并且必须等到数据源被更新成功后才可返回。这样保证了缓存和数据库里的数据一致性。Write Back:回写式,数据写入缓存即可返回,缓存内部会异步的去更新数据源,这样好处是写操作特别快,因为只需要更新缓存。并且缓存内部可以合并对相同数据项的多次更新,但是带来的问题就是数据不一致,可能发生写丢失。二、预处理和延后处理预先延后,这其实是一个事物的两面,不管是预先还是延后核心思想都是将本来该在实时链路上处理的事情剥离,要么提前要么延后处理。降低实时链路的路径长度, 这样能有效提高系统性能。2.1 预处理举个我们团队实际中遇到的问题:前两个月支付宝联合杭州市政府发放消费劵,但是要求只有杭州市常驻居民才能领取,那么需要在抢卷请求进入后台的时候就判断一下用户是否是杭州常驻居民。而判断用户是否是常驻居民这个是另外一个微服务接口,如果直接实时的去调用那个接口,短时的高并发很有可能把这个服务也拖挂,最终导致整个系统不可用,并且 RPC 本身也是比较耗时的,所以就考虑在这里进行优化。那么该怎么做呢?很简单的一个思路,提前将杭州所有常驻居民的 user_id 存到缓存中, 比如可以直接存到 Redis。大概就是千万量级,这样,当请求到来的时候我们直接通过缓存可以快速判断是否来自杭州常驻居民。如果不是则直接在这里返回前端。这里通过预先处理减少了实时链路上的 RPC 调用,既减少了系统的外部依赖,也极大的提高了系统的吞吐量。预处理在 CPU 和操作系统中也广泛使用,比如 CPU 基于历史访存信息,将内存中的指令和数据预取到 Cache 中,这样可以大大提高Cache 命中率。 还比如在 Linux 文件系统中,预读算法会预测即将访问的 page,然后批量加载比当前读请求更多的数据缓存在 page cache 中,这样当下次读请求到来时可以直接从 cache 中返回,大大减少了访问磁盘的时间。2.2 延后处理还是支付宝,上栗子:这是支付宝春节集五福活动开奖当晚,不过,作为非酋的我一般是不屑于参与这种活动的。大家发现没有,这类活动中奖奖金一般会显示 「稍后到账」,为什么呢?那当然是到账这个操作不简单!到账即转账,A 账户给 B 账户转钱,A 减钱, B 就必须要同时加上钱,也就是说不能 A 减了钱但 B 没有加上,这就会导致资金损失。资金安全是支付业务的生命线,这可不行。这两个动作必须一起成功或是一起都不成功,不能只成功一半,这是保证数据一致性。 保证两个操作同时成功或者失败就需要用到事务。如果去实时的做到账,那么大概率数据库的 TPS(每秒处理的事务数) 会是瓶颈。通过产品提示,将到账操作延后处理,解决了数据库 TPS 瓶颈。延后处理还有一个非常著名的例子,COW(Copy On Write,写时复制)。 Linux 创建进程的系统调用 fork,fork 产生的子进程只会创建虚拟地址空间,而不会分配真正的物理内存,子进程共享父进程的物理空间,只有当某个进程需要写入的时候,才会真正分配物理页,拷贝该物理页,通过 COW 减少了很多不必要的数据拷贝。三、池化后台开发过程中你一定离不开各种 「池子」: 内存池、连接池、线程池、对象池…内存、连接、线程这些都是资源,创建线程、分配内存、数据库连接这些操作都有一个特征, 那就是创建和销毁过程都会涉及到很多系统调用或者网络 IO。 每次都在请求中去申请创建这些资源,就会增加请求处理耗时,但是如果我们用一个 容器(池) 把它们保存起来,下次需要的时候,直接拿出来使用,避免重复创建和销毁浪费的时间。3.1 内存池在 C/C++ 中,经常使用 malloc、new 等 API 动态申请内存。由于申请的内存块大小不一,如果频繁的申请、释放会导致大量的内存碎片,并且这些 API 底层依赖系统调用,会有额外的开销。内存池就是在使用内存前,先向系统申请一块空间留做备用,使用者需要内池时向内存池申请,用完后还回来。内存池的思想非常简单,实现却不简单,难点在于以下几点:如何快速分配内存降低内存碎片率维护内存池所需的额外空间尽量少如果不考虑效率,我们完全可以将内存分为不同大小的块,然后用链表连接起来,分配的时候找到大小最合适的返回,释放的时候直接添加进链表。如:当然这只是玩具级别的实现,业界有性能非常好的实现了,我们可以直接拿来学习和使用。比如 Google 的 「tcmalloc」 和 Facebook 的 「jemalloc」。限于篇幅我们不在这里详细讲解它们的实现原理,如果感兴趣可以搜来看看,也推荐去看看被誉为神书的 CSAPP(《深入理解计算机系统》)第 10 章,那里也讲到了动态内存分配算法。3.2 线程池线程是干嘛的?线程就是我们程序执行的实体。在服务器开发领域,我们经常会为每个请求分配一个线程去处理,但是线程的创建销毁、调度都会带来额外的开销,线程太多也会导致系统整体性能下降。在这种场景下,我们通常会提前创建若干个线程,通过线程池来进行管理。当请求到来时,只需从线程池选一个线程去执行处理任务即可。线程池常常和队列一起使用来实现任务调度,主线程收到请求后将创建对应的任务,然后放到队列里,线程池中的工作线程等待队列里的任务。线程池实现上一般有四个核心组成部分:管理器(Manager): 用于创建并管理线程池。工作线程(Worker): 执行任务的线程。任务接口(Task): 每个具体的任务必须实现任务接口,工作线程将调用该接口来完成具体的任务。任务队列(TaskQueue): 存放还未执行的任务。线程池在 C、C++ 中没有具体的实现,需要应用开发者手动实现上诉几个部分。在 Java 中 「ThreadPoolExecutor」 类就是线程池的实现。后续我也会写文章分析 C++ 如何写一个简单的线程池以及 Java 中线程池是如何实现的。3.3 连接池顾名思义,连接池是创建和管理连接的。大家最熟悉的莫过于数据库连接池,这里我们简单分析下如果不用数据库连接池,一次 SQL 查询请求会经过哪些步骤:和 MySQL server 建立 TCP 连接: 三次握手MySQL 权限认证: Server 向 Client 发送 密钥Client 使用密钥加密用户名、密码等信息,将加密后的报文发送给 ServerServer 根据 Client 请求包,验证是否是合法用户,然后给 Client 发送认证结果Client 发送 SQL 语句Server 返回语句执行结果MySQL 关闭TCP 连接断开 四次挥手可以看出不使用连接池的话,为了执行一条 SQL,会花很多时间在安全认证、网络IO上。如果使用连接池,执行一条 SQL 就省去了建立连接和断开连接所需的额外开销。还能想起哪里用到了连接池的思想吗?我认为 HTTP 长链接也算一个变相的链接池,虽然它本质上只有一个连接,但是思想却和连接池不谋而合,都是为了复用同一个连接发送多个 HTTP 请求,避免建立和断开连接的开销。池化实际上是预处理和延后处理的一种应用场景,通过池子将各类资源的创建提前和销毁延后。四、同步变异步对于处理耗时的任务,如果采用同步的方式,那么会增加任务耗时,降低系统并发度。可以通过将同步任务变为异步进行优化。举个例子,比如我们去 KFC 点餐,遇到排队的人很多,当点完餐后,大多情况下我们会隔几分钟就去问好了没,反复去问了好几次才拿到,在这期间我们也没法干活了,这时候我们是这样的:这个就叫同步轮训, 这样效率显然太低了。服务员被问烦了,就在点完餐后给我们一个号码牌,每次准备好了就会在服务台叫号,这样我们就可以在被叫到的时候再去取餐,中途可以继续干自己的事。这就叫异步,在很多编程语言中有异步编程的库,比如 C++ std::future、Python asyncio 等,但是异步编程往往需要回调函数(Callback function),如果回调函数的层级太深,这就是回调地狱(Callback hell)。回调地狱如何优化又是一个庞大的话题。。。。这个例子相当于函数调用的异步化,还有的是情况是处理流程异步化,这个会在接下来消息队列中讲到。五、消息队列这是一个非常简化的消息队列模型,上游生产者将消息通过队列发送给下游消费者。在这之间,消息队列可以发挥很多作用,比如:5.1 服务解耦有些服务被其它很多服务依赖,比如一个论坛网站,当用户成功发布一条帖子有一系列的流程要做,有积分服务计算积分,推送服务向发布者的粉丝推送一条消息… 对于这类需求,常见的实现方式是直接调用:这样如果需要新增一个数据分析的服务,那么又得改动发布服务,这违背了依赖倒置原则,即上层服务不应该依赖下层服务,那么怎么办呢?引入消息队列作为中间层,当帖子发布完成后,发送一个事件到消息队列里,而关心帖子发布成功这件事的下游服务就可以订阅这个事件,这样即使后续继续增加新的下游服务,只需要订阅该事件即可,完全不用改动发布服务,完成系统解耦。5.2 异步处理有些业务涉及到的处理流程非常多,但是很多步骤并不要求实时性。那么我们就可以通过消息队列异步处理。比如淘宝下单,一般包括了风控、锁库存、生成订单、短信/邮件通知等步骤。但是核心的就风控和锁库存, 只要风控和扣减库存成功,那么就可以返回结果通知用户成功下单了。后续的生成订单,短信通知都可以通过消息队列发送给下游服务异步处理。大大提高了系统响应速度。这就是处理流程异步化。5.3 流量削峰一般像秒杀、抽奖、抢卷这种活动都伴随着短时间海量的请求, 一般超过后端的处理能力,那么我们就可以在接入层将请求放到消息队列里,后端根据自己的处理能力不断从队列里取出请求进行业务处理。就像最近长江汛期,上游短时间大量的洪水汇聚直奔下游,但是通过三峡大坝将这些水缓存起来,然后匀速的向下游释放,起到了很好的削峰作用。起到了平均流量的作用。5.4 总结消息队列的核心思想就是把同步的操作变成异步处理,异步处理会带来相应的好处,比如:服务解耦提高系统的并发度,将非核心操作异步处理,不会阻塞住主流程但是软件开发没有银弹,所有的方案选择都是一种 trade-off。 同样,异步处理也不全是好处,也会导致一些问题:降低了数据一致性,从强一致性变为最终一致性有消息丢失的风险,比如宕机,需要有容灾机制六、批量处理在涉及到网络连接、IO等情况时,将操作批量进行处理能够有效提高系统的传输速率和吞吐量。在前后端通信中,通过合并一些频繁请求的小资源可以获得更快的加载速度。比如我们后台 RPC 框架,经常有更新数据的需求,而有的数据更新的接口往往只接受一项,这个时候我们往往会优化下更新接口,使其能够接受批量更新的请求,这样可以将批量的数据一次性发送,大大缩短网络 RPC 调用耗时。七、数据库我们常把后台开发调侃为「CRUD」,数据库在整个应用开发过程中的重要性不言而喻。而且很多时候系统的瓶颈也往往处在数据库这里,慢的原因也有很多,比如可能是没用索引、没用对索引、读写锁冲突等等。那么如何使用数据才能又快又好呢?下面这几点需要重点关注:7.1 索引索引可能是我们平时在使用数据库过程中接触得最多的优化方式。索引好比图书馆里的书籍索引号,想象一下,如果我让你去一个没有书籍索引号的图书馆找《人生》这本书,你是什么样的感受?当然是怀疑人生,同理,你应该可以理解当你查询数据,却不用索引的时候数据库该有多崩溃了吧。数据库表的索引就像图书馆里的书籍索引号一样,可以提高我们检索数据的效率。索引能提高查找效率,可是你有没有想过为什么呢?这是因为索引一般而言是一个排序列表,排序意味着可以基于二分思想进行查找,将查询时间复杂度做到 O(log(N)),快速的支持等值查询和范围查询。二叉搜索树查询效率无疑是最高的,因为平均来说每次比较都能缩小一半的搜索范围,但是一般在数据库索引的实现上却会选择 B 树或 B+ 树而不用二叉搜索树,为什么呢?这就涉及到数据库的存储介质了,数据库的数据和索引都是存放在磁盘,并且是 InnoDB 引擎是以页为基本单位管理磁盘的,一页一般为 16 KB。AVL 或红黑树搜索效率虽然非常高,但是同样数据项,它也会比 B、B+ 树更高,高就意味着平均来说会访问更多的节点,即磁盘IO次数! 根据 Google 工程师 Jeff Dean 的统计,访问内存数据耗时大概在 100 ns,访问磁盘则是 10,000,000 ns。 所以表面上来看我们使用 B、B+ 树没有 二叉查找树效率高,但是实际上由于 B、B+ 树降低了树高,减少了磁盘 IO 次数,反而大大提升了速度。这也告诉我们,没有绝对的快和慢,系统分析要抓主要矛盾,先分析出决定系统瓶颈的到底是什么,然后才是针对瓶颈的优化。其实关于索引想写的也还有很多,但还是受限于篇幅,以后再单独写。先把我认为索引必知必会的知识列出来,大家可以查漏补缺:主键索引和普通索引,以及它们之间的区别最左前缀匹配原则索引下推覆盖索引、联合索引7.2 读写分离一般业务刚上线的时候,直接使用单机数据库就够了,但是随着用户量上来之后,系统就面临着大量的写操作和读操作,单机数据库处理能力有限,容易成为系统瓶颈。由于存在读写锁冲突,并且很多大型互联网业务往往读多写少,读操作会首先成为数据库瓶颈,我们希望消除读写锁冲突从而提升数据库整体的读写能力。那么就需要采用读写分离的数据库集群方式,一主多从,主库会同步数据到从库。写操作都到主库,读操作都去从库。读写分离到之后就避免了读写锁争用,这里解释一下,什么叫读写锁争用:MySQL 中有两种锁:排它锁( X 锁): 事务 T 对数据 A 加上 X 锁时,只允许事务 T 读取和修改数据 A。共享锁( S 锁): 事务 T 对数据 A 加上 S 锁时,其他事务只能再对数据 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。读写分离解决问题的同时也会带来新问题,比如主库和从库数据不一致MySQL 的主从同步依赖于 binlog,binlog(二进制日志)是 MySQL Server 层维护的一种二进制日志,是独立于具体的存储引擎。它主要存储对数据库更新(insert、delete、update)的 SQL 语句,由于记录了完整的 SQL 更新信息,所以 binlog 是可以用来数据恢复和主从同步复制的。从库从主库拉取 binlog 然后依次执行其中的 SQL 即可达到复制主库的目的,由于从库拉取 binlog 存在网络延迟等,所以主从数据存在延迟问题。那么这里就要看业务是否允许短时间内的数据不一致,如果不能容忍,那么可以通过如果读从库没获取到数据就去主库读一次来解决。7.3 分库分表如果用户越来越多,写请求暴涨,对于上面的单 Master 节点肯定扛不住,那么该怎么办呢?多加几个 Master?不行,这样会带来更多的数据不一致的问题,增加系统的复杂度。那该怎么办?就只能对库表进行拆分了。常见的拆分类型有垂直拆分和水平拆分。考虑拼夕夕电商系统,一般有 订单表、用户表、支付表、商品表、商家表等, 最初这些表都在一个数据库里。 后来随着砍一刀带来的海量用户,拼夕夕后台扛不住了! 于是紧急从阿狸粑粑那里挖来了几个 P8、P9 大佬对系统进行重构。P9 大佬第一步先对数据库进行垂直分库, 根据业务关联性强弱,将它们分到不同的数据库, 比如订单库,商家库、支付库、用户库。 第二步是对一些大表进行垂直分表,将一个表按照字段分成多表,每个表存储其中一部分字段。 比如商品详情表可能最初包含了几十个字段,但是往往最多访问的是商品名称、价格、产地、图片、介绍等信息,所以我们将不常访问的字段单独拆成一个表。 由于垂直分库已经按照业务关联切分到了最小粒度,数据量任然非常大,P9 大佬开始水平分库,比如可以把订单库分为订单1库、订单2库、订单3库… 那么如何决定某个订单放在哪个订单库呢?可以考虑对主键通过哈希算法计算放在哪个库。分完库,单表数据量任然很大,查询起来非常慢,P9 大佬决定按日或者按月将订单分表,叫做日表、月表。分库分表同时会带来一些问题,比如平时单库单表使用的主键自增特性将作废,因为某个分区库表生成的主键无法保证全局唯一,这就需要引入全局 UUID 服务了。经过一番大刀阔斧的重构,拼夕夕恢复了往日的活力,大家又可以愉快的在上面互相砍一刀了。(分库分表会引入很多问题,并没有一一介绍,这里只是为了讲解什么是分库分表)八、具体技法8.1 零拷贝高性能的服务器应当避免不必要数据复制,特别是在用户空间和内核空间之间的数据复制。 比如 HTTP 静态服务器发送静态文件的时候,一般我们会这样写:如果了解 Linux IO 的话就知道这个过程包含了内核空间和用户空间之间的多次拷贝:内核空间和用户空间之间数据拷贝需要 CPU 亲自完成,但是对于这类数据不需要在用户空间进行处理的程序来说,这样的两次拷贝显然是浪费。什么叫 「不需要在用户空间进行处理」?比如 FTP 或者 HTTP 静态服务器,它们的作用只是将文件从磁盘发送到网络,不需要在中途对数据进行编解码之类的计算操作。如果能够直接将数据在内核缓存之间移动,那么除了减少拷贝次数以外,还能避免内核态和用户态之间的上下文切换。而这正是零拷贝(Zero copy)干的事,主要就是利用各种零拷贝技术,减少不必要的数据拷贝,将 CPU 从数据拷贝这样简单的任务解脱出来,让 CPU 专注于别的任务。常用的零拷贝技术:mmap mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。 sendfile sendfile 是 Linux2.1 版本提供的,数据不经过用户态,直接从页缓存拷贝到 socket 缓存,同时由于和用户态完全无关,就减少了一次上下文切换。 在 Linux 2.4 版本,对 sendfile 进行了优化,直接通过 DMA 将磁盘文件数据读取到 socket 缓存,真正实现了 ”0” 拷贝。前面 mmap 和 2.1 版本的 sendfile 实际上只是消除了用户空间和内核空间之间拷贝,而页缓存和 socket 缓存之间的拷贝依然存在。 8.2 无锁化在多线程环境下,为了避免 竞态条件(race condition), 我们通常会采用加锁来进行并发控制,锁的代价也是比较高的,锁会导致上线文切换,甚至被挂起直到锁被释放。基于硬件提供的原子操作 CAS(Compare And Swap) 实现一些高性能无锁的数据结构,比如无锁队列,可以在保证并发安全的情况下,提供更高的性能。首先需要理解什么是 CAS,CAS 有三个操作数,内存里当前值M,预期值 E,修改的新值 N,CAS 的语义就是:如果当前值等于预期值,则将内存修改为新值,否则不做任何操作。用 C 语言来表达就是:注意,上面 CAS 函数实际上是一条原子指令,那么是如何用的呢?假设我需要实现这样一个功能:对一个全局变量 global 在两个不同线程分别对它加 100 次,这里多线程访问一个全局变量存在 race condition,所以我们需要采用线程同步操作,下面我分别用锁和CAS的方法来实现这个功能。通过使用原子操作大大降低了锁冲突的可能性,提高了程序的性能。除了 CAS,还有一些硬件原子指令:Fetch-And-Add,对变量原子性 + 1Test-And-Set,这是各种锁算法的核心,在 AT&T/GNU 汇编语法下,叫 xchg 指令,我会单独写一篇如何使用 xchg 实现各种锁。这回答文章主要是粗浅的介绍了一些系统设计、系统优化的套路和最佳实践。不知道你发现没有,从缓存到消息队列、CAS…,很多看起来很牛逼的架构设计其实都来源于操作系统、体系结构。所以我非常热衷学习一些底层的基础知识,这些看似古老的技术是经过时间洗礼留下来的好东西。现在很多的新技术、框架看似非常厉害,实则不少都是新瓶装旧酒,每几年又会被淘汰一批。码字不易,希望大家觉得有帮助的话,帮小北 @编程指北 双击点个赞,笔芯编辑于 07-13 13:53​赞同 364​​8 条评论​分享​收藏​喜欢收起​继续浏览内容知乎发现更大的世界打开Chrome继续知乎用户前端开发等 4 个话题下的优秀答主154 人赞同了该回答如何真正学会:进一家大厂,看看别人是怎么做事情的。多经历几家大厂后,你会明白到「太阳底下没有新鲜事」,其实大家的做法都差不多,所谓的创新都是微创新,真正的难题都在细节上。你看别人怎么解决问题,然后亲自去解决问题,这就算是学会了。当然这里存在一个先有鸡还是先有蛋的问题,你不懂你就可能面不进一家大厂……如何假装学会:系统设计的本质就是拼乐高,你先要知道都有哪些零件存在,常见的问题通过哪些常见的零件组合模式来解决。这时候别人叫你拼一个东西,你说得清楚怎么拼就好了。真正有难度的细节?如果你的级别不高,你面试官的级别也不高,谁也不是特别懂,就看谁能把谁忽悠住了。上网找些高质量的教材学习一下就好。没有经手过真正的大规模系统就是没有,真正的经验是没办法脱离环境自学的。Fake it till you make it. 发布于 08-27 04:17​赞同 154​​9 条评论​分享​收藏​喜欢继续浏览内容知乎发现更大的世界打开Chrome继续findyi​​华中科技大学 计算机应用硕士13 人赞同了该回答这题我会!担任过360技术总监,独角兽公司技术VP,搞定高并发基本是必须的,不然分分钟失业。记忆中最深刻的一次是在360研发了一个新产品,预估半年内最多百万日活,没想到业务爆炸式发展,直接飙到日活千万,服务器瞬间就扛不住了各种故障频出。副总裁非常生气,要求几天内搞定,当时真是连续通宵加班,头都快秃了才涉险过关。另外我并不觉得一定要有高并发实际项目经验才能做高并发,有些完全没有实际高并发经验的人,但做好了其他准备,他上手也能做的很好。想拥有所谓“高并发”的经验恐怕首先是要夯实计算机体系基础知识,包括:算法/数据结构、操作系统、jvm、数据库、缓存、多线程/多进程等领域。先上本文思维导图:以我从业的经验来看:高并发只是一个不确定的结果,并不是过程。为什么我说是不确定呢?在来自全人类的高并发访问面前,一切都有可能发生,所以我们经常能看到顶级网站的抽风颤抖。首先,我觉得基础知识非常重要,这包括算法,操作系统,jvm,数据库,缓存,多线程等等,其实这都是独立而又关联的知识。每一项知识里,又是一个完整的知识树,比如数据库,分库分表、数据备份、读写分离等等,都需要首先有深入的学习和了解。书本里知识都有,但理论要结合实际,一定要联系到具体的技术才会有用。但也别迷信任何技术,比如说使用了netty就能比谁谁谁快几十倍,但这有两个前提,一个是netty适合你的应用场景,另一个是你用netty用的合理。也别只痴迷于框架,框架只会挡住你的眼睛,让你觉得什么都不重要。大并发面前,没一个框架靠得住,靠得住的只有人,是人来根据具体的应用场景,使用各种知识去解决具体的问题。恰好,前段时间整理了一份计算机硬核书单,里面就包括不少如何设计高并发需要的基础体系知识。我把大学和工作中用的经典电子书库(包含高并发所需要的基础知识、Java全系经典大厚书、数据结构、操作系统、C++/C、网络经典、前端编程经典、Java相关、程序员认知、职场发展)、面试找工作的资料汇总都打包放在这了,点击下方可以直接获取:计算机经典书籍​mp.weixin.qq.com​mp.weixin.qq.com/s?__biz=MzA3MzA5MTU4NA==&mid=100008684&idx=1&sn=eb2ebb6c6f4da1b6d82c5ea643cad435&chksm=1f16fd83286174959fe46170ccbb1c65d51afb0d174b10f85eaeddfe4030d898042fe5fbecf2#rd这套资源可不是一般那种网上找的资源,是伴随我从学生一路成长为腾讯高级开发工程师,360技术经理、360技术总监、中小公司CTO的打包全套!非常宝贵!大家可以抓紧安排学起来,另外重点说一下,看书其实是有技巧的,千万别一头扎进去出不来。首先如果看不懂某个知识点,那大概率是这个知识点背后的基础不牢固,不妨回过头针对性的看看这个知识点。比如在你学习Java的Network之际,要是犯迷糊了,那就应该看看计算机网络相关的书籍或者视频,了解下HTTP、DNS、ARP、TCP、IP、ICMP、UDP等基础知识。这样不光你能攻克学习的卡点,顺带还把计算机体系基础知识给补齐全了。我们说回来,其实高并发并没有想象中的那么难,你把算法,操作系统,jvm,数据库,缓存,多线程这些基础知识攻克后,完全可以自己动手做一个简单的高并发demo。那么一个简单的高并发demo包含哪些关键点呢?其实只有 以下六 点:系统拆分缓存MQ分库分表读写分离ElasticSearch1、系统拆分将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,这样一个最简化的抗高并发的雏形就有了。2、缓存缓存是必须掌握的。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。3、MQMQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费后慢慢写,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。4、分库分表分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 sql 跑的性能。5、读写分离读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。6、ElasticSearchElasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。在一定基础上,实现以上6点,你就具备了初步的高并发经验。接下来再说点更深入的重点以及自学获得高并发经验之后,如何应对猎头、Hr和面试官。一个后端程序员最重要的技能是分布式系统设计,这是经验和知识的结合,或者说就是用前面提到的基础来搭积木。编程语言是不重要的,但有那么几个语言在高并发应用方面积累了非常多的宝贵经验,比如说java,scala还有新兴的go。这里最常见的就是如何分片sharding和负载均衡load balancer。为什么要分片和负载均衡呢? 因为一台机器你再怎么玩,能力也是有限的。很多人都知道都知道更大的并发需要更多的服务器,这么多的服务器怎么分工,就是分片和负载均衡。这听起来还是挺容易的,做起来也不难。有时候你的分布式系统很可能加不了那么多服务器,这就是所谓的scalability,这是我们在项目伊始就要多加注意的地方。但这东西有个平衡,高的scalability不是没有代价的。我以前给人家讲课的时候,常讲一个例子,如果你设计一个自家厨房,你肯定只设计一个洗碗的水池对不对?平时绝对是够用的,但是如果你有一天请20个朋友来你家吃饭,吃饭大家围着桌子一起吃非常快,但你发现洗碗就scale不上去了,派再多人洗碗也没用,最多同时只能一个人在洗碗,这就成为你家接待能力的瓶颈。但家里厨房并不是为接待20+个朋友设计的,搞太多水池是个累赘。这就需要权衡,没有一定对的设计。How to make your code scale · Matthias Mullie所以并没有一种高并发设计可以打遍天下无敌手,google, twitter, amazon, facebook, linkedin都有自己的应用场景,有自己的实际需要,有自己的权衡,有自己的技术特点。有一得就必有一失,比如说google和uber使用go来解决起自身的特殊需要和已经产生的问题,但是go对你的团队来说可能仅仅是个略显丑陋的普通语言。你去照搬人家微服务的设计,很可能还不如老老实实用个spring mvc + mysql。“这里并没有什么所谓的顶点,我们还仍然愿意为此挥拳“------我忘了我在哪里看到了所以我们去了解我们自己系统的特点,选择对应的技术,明白当前技术的得失。有兴趣的朋友可以看看Eventual Consistency vs Strong Consistencyhttps://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore/​cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore/高并发和scalability还不能离开高可用,你搞5w台服务器,但只要坏一台就全部服务都受影响肯定是不行的。你想如果1台服务器的可用性是99%,那如果你有100台,它们在一起每台都不坏的可用性就只有99%^100, 这个值只有36.6%。也就是说如果你有个100台机器的机房,每年大半时间都有机器在出问题。单纯的冗余并不能解决问题,如果有些服务坏掉或者重启,你需要能有一些应对和调整的策略,你需要让别的服务器知道哪些下游服务器可用,哪些已经挂了。这有很多的搞法,大家可以看看zookeeper。但像zookeeper这样的技术也不能过度使用,有些朋友把啥都往zookeeper里放,造成zookeeper成了系统的瓶颈,这就反而不美了。这有还很多细节,比如说id怎么生成,你用一个mysql自增长的整数就会影响并发的能力,uuid生成也没那么简单,也要根据实际情况调整。比如说你的数据怎么sharding,以后怎么扩容,可以看看一致性哈希,再结合前面id算法,可以给你带来很多思考。然后跨多个数据中心怎么办,如果是一个可写其他只读,那我怎么知道去哪个数据中心写,其实还是可以做在那个id算法里。这些我觉得也可以自学,网上公开的资料很多。现在的高并发服务不单单是线上的服务,还包括很多线下的服务,比如说大数据,这个也是不能忽视的部分。数据处理又根据你的需要可以是实时的,实时的可以使用storm, twitter有一个新的实现heron,有更合理的架构,而且和storm api兼容。如果不是实时的,选择就更多了。很多人可能会比较在意GC的设置,其实GC并不直接影响你系统的并发能力,它可能会影响到你系统的吞吐量和访问延时之间的关系,而且其实近年来jvm和go的默认GC设置已经很难让其成为系统的瓶颈了。最后才是实操经验,其实这主要让猎头和hr觉得你行,因为你干过。但你具体行不行技术面试一聊就知道了,再好的公司也不是各个都行。国内做得好的可以多看看阿里、头条等,但我不知道他们具体并发多少。如果你前面那些基础好,去阿里和头条不会太难,去一个核心点的组,就接触到了。或者可以看看技术博客也有帮助。https://blog.twitter.com/engineering/en_us.html​blog.twitter.com/engineering/en_us.html以上,希望对大家有帮助,如果觉得不错的话,请给我 @findyi一个点赞鼓励。发布于 06-25 21:40​赞同 13​​1 条评论​分享​收藏​喜欢收起​继续浏览内容知乎发现更大的世界打开Chrome继续腾讯云广告​不感兴趣知乎广告介绍11.11去哪买云服务器更优惠?来腾讯云!爆款云服务器,全年底价!11.11云上盛惠,服务器2核4G首年70元,累计订单享10%满返查看详情点击跳转至第三方匿名用户104 人赞同了该回答这个问题完全可以重定向到如何处理高并发业务场景.以下只是我工作一年多接触到的一些基础,也许有偏差,要具备高并发的经验确实需要有实际项目,因为业务逻辑其实很容易理清,但是要在高并发的情况下如何找到业务繁忙的热点并进行优化,完全只能凭经验.假如没有靠谱的公司,接触不到高并发的业务场景怎么办?从处理技巧上,可以通过大牛学习高并发的架构,比如张宴:张宴的博客 - Web系统架构与底层研发.至少你可以知道处理高并发的业务逻辑是:前端:异步请求+资源静态化+cdn后端:请求队列+轮询分发+负载均衡+共享缓存数据层:redis缓存+数据分表+写队列存储:raid阵列+热备网络:dns轮询+DDOS攻击防护对于高并发并没有什么通用解决方案,必须根据业务场景进行分析,不同的业务场景对于架构的取舍是不一样的.但万变不离其宗,掌握这些处理高并发的分析方法还是很有必要的.如何学习高并发的工具?处理高并发的开源轮子其实很多.很多高并发的架构分享都会提及使用的工具,自己多留心,再看看手册,有条件自己搭起来跑一跑.redis,nginx/Tengine,keeplive,DRBD,heartbeat这些小工具还是可以在虚拟机上面多开几台跑起来的.至于大业务场景,除了进大公司没有别的办法,因为有些工具运行的配置要求太高,必须多台服务器配合才能完成.如何模拟高并发场景?并不是只有实际生产环境才能测试高并发,其实模拟高并发的轮子也很多,最常用的apache benchmark,winrunner,loadrunner,这些教程很多,用来模拟基本的高并发业务绰绰有余,自己安装试用版,学学如何用,模拟些常用的业务.如果有精力,业内很喜欢用perl,python,C来写一些针对热点业务的负载脚本.这需要有http协议等网络封包的理论基础.一些建议处理高并发要学习的东西实在太多.要在没有实际工作经验的情况下逐一了解太难,也很难深入.对于高并发的学习,我建议除了多阅读高并发架构的文档学习基本的方法论以外,自己要去深入学习网络基础,数据结构和算法.这些都是处理高并发热点的理论基础.发布于 2013-06-09 15:21​赞同 104​​2 条评论​分享​收藏​喜欢收起​继续浏览内容知乎发现更大的世界打开Chrome继续阿里巴巴淘系技术​已认证的官方帐号27 人赞同了该回答邀请了多位阿里淘系的架构专家来回答这题,先分享第一波“高并发”的学习和实践方法,本答案将持续更新。(点击头像关注我们账号,别错过更多阿里工程师一线技术干货)———————————————————————————————————————— 现实中,哪怕是大公司,高并发系统也是可遇不可求的。不过,高并发其实是可以通过压测来模拟的。高并发的背后,核心是高可用和低延迟。所以我们其实是想有能力设计一个系统,在高并发访问的时候,系统依然可用,而且响应速度不会变慢。想提升高并发系统的设计和开发能力,有2个方面:一个是系统的学习相关理论;一个是找一个目标系统,不断想办法去提升他的性能。前者是后者的理论基础。如果想从事一个高并发系统开发的岗位,要学习的相关技术其实是很多的,这些技术核心就是解决高并发情况下如何保持系统的高可用和低延迟。 以Java工程师为例,互联网程序员面试中经常会考察的内容包括:(1) 架构设计:高可用与稳定性、事务一致性、多副本一致性、CAP理论。(2) 相关技术:多线程(JUC/AQS/线程池)、RPC调用及框架(如Thrift)、NIO及NIO框架(如Netty)、高并发框架(如Disruptor) 、微服务框架(SpringBoot)、微服务治理(Spring Cloud)、数据库相关技术(如:索引优化、分库分表、读写分离)、分布式缓存(如redis)、消息中间件系统(如RabbitMQ)、容器技术(如docker)。(3) 工具:系统性能查看(top、uptime、vmstat、iostat)、压测工具(如ab、locust、Jmeter、go)、线程分析(如jps、jstack)等。当然,一开始,我们不可能逐一把这些技能全部掌握,我们可以从一个实际项目入手,不断的把这些技术用上去,发现哪些知识不足,再去补充相关的知识。“如何设计一个好的秒杀系统“,一定是互联网大厂面试中最常问的一个问题。所以从设计一个秒杀系统开始实践,是个不错的选择。秒杀系统的特点:(1)瞬时并发量大秒杀时会有大量用户在同一时间进行抢购,瞬时并发访问量突增 10 倍,甚至 100 倍以上都有。(2)库存量少一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。(3)业务简单流程比较简单,一般都是下订单、扣库存、支付订单。设计秒杀系统的关键点:(1)限流由于活动库存量一般都是很少,对应的只有少部分用户才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户流量进入后端服务器。 (2)削峰秒杀开始的那一瞬间,会有大量用户冲击进来,所以在开始时候会有一个瞬间流量峰值。如何把瞬间的流量峰值变得更平缓,是能否成功设计好秒杀系统的关键因素。实现流量削峰填谷,一般的采用缓存和 MQ 中间件来解决。(3)异步秒杀其实可以当做高并发系统来处理,在这个时候,可以考虑从业务上做兼容,将同步的业务,设计成异步处理的任务,提高网站的整体可用性。 (4)缓存秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到 OLTP 的数据库,类似 MySQL、Oracle。由于数据库底层采用 B+ 树的储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移到内存的缓存或者 Redis 中,会极大的提高并发效率。从0到1搭建一个秒杀系统,也并不容易,涉及到很多前端、后端、中间件的技术。这个跟其实是所有公司的工作常态,大部分时间也是在搭架子,真正做技术优化的时间并不多,经常是在业务量突增或者大促活动来临时,集中搞一波性能优化。 所以,如果没有实际的高并发项目可做,自己弄个秒杀系统自娱自乐也是不错的。搭建系统 -> 压测 -> 发现问题 -> 学习知识 -> 优化系统,通过这样的循环,相信你一定既能体验到学习的乐趣,同时实力也大幅提升。

猜你喜欢

转载自blog.csdn.net/weixin_46742102/article/details/121536438