乐趣区

关于java:一文搞懂后台高性能服务器设计的常见套路-BAT-高频面试系列

微信搜寻????「编程指北」,关注这个写干货的程序员,回复「资源」,即可获取后盾开发学习路线和书籍

前言

金九银十,又是一年校招季。

经验过,才深知不易。最近,和作为校招面试官的共事聊了聊,问他们是如何去考查一个学生的,我简略归为以下几点:

  1. 聪慧、反馈快,这点自不必说,聪慧意味着学习能力、适应力强,可能疾速胜任工作。
  2. 算法不错,代码基本功好,这点其实考查的是算法能力和代码是否写得优雅。
  3. 根底过硬,技术岗面试最外围的还是考查「技术储备」,包含了语言基本功,操作系统、网络、体系结构、零碎设计。
  4. 语言组织和表达能力,这点很重要,很多同学懂得某个知识点,却很难用简洁精确的语言表述进去。

想必有很多同学在刷题、刷面经,不过我想说“面经虽好,不要贪杯哦~”,面经能够刷,看看面试官都是怎么发问的,但不要寄希望于原题。
因为面试过程中的问题往往是一环扣一环的,这意味着你须要有足够的 技术深度 ,将常识由 点连接成面,而不是停留在互相孤立的知识点上。

所以还是倡议 系统性 的看书,如果感觉工夫不够,能够关注书里的重点章节。至于看哪些书?前面也会列一个我的书单和浏览倡议。在【编程指北】后盾回复【书单】即可获取

那么回到技术面试上,除了算法和网络、操作系统这种根底之外,还有一类 零碎设计和优化 的问题。这类问题须要你有一个全局的技术视线,以及相熟一些罕用的系统优化方法论,也就是工程上的一些 Best Practice,而不至于本人长期拍脑袋瞎设计。

在互联网公司,常常面临一个“三高”问题:

  • 高并发
  • 高性能
  • 高可用

这篇文章将总结一下后盾服务器开发中有哪些罕用的解决“三高”问题的办法和思维。

心愿这些常识,可能给你一丝启发和帮忙,助力你收割 各大公司 Offer~

先上本文思维导图:

注释

一、缓存

什么是缓存?看看维基百科怎么说:

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 算法也是值得用一篇文章来讲的,如果临时还不懂的话能够去搜一下。

    罕用的组件有 MemcacheRedis Cluster 等,第二个是在高性能内存存储 Redis 的根底上,提供分布式存储的解决方案。

1.3 缓存使用指南

1. 适宜缓存的场景:

  • 读多写少:

    比方电商里的商品详情页面,拜访频率很高,然而个别写入只在店家上架商品和批改信息的时候产生。如果把热点商品的信息缓存起来,这将拦挡掉很多对数据库的拜访,进步零碎整体的吞吐量。

    因为个别数据库的 QPS 因为有「ACID」束缚、并且数据是长久化在硬盘的,所以比 Redis 这类基于内存的 NoSQL 存储低不少。经常是一个零碎的瓶颈,如果咱们把大部分的查问都在 Redis 缓存中命中了,那么零碎整体的 QPS 也就下来了。

  • 计算耗时大,且实时性不高:
    比方王者光荣里的全区排行榜,个别一周更新一次,并且计算的数据量也比拟大,所以计算后缓存起来,申请排行榜间接从缓存中取出,就不必实时计算了。

2. 不适宜缓存的场景

  • 写多读少,频繁更新。
  • 对数据一致性要求严格: 因为缓存会有更新策略,所以很难做到和数据库实时同步。
  • 数据拜访齐全随机: 因为这样会导致缓存的命中率极低。

1.4 缓存更新的策略

如何更新缓存其实曾经有总结得十分好的「最佳实际」,咱们依照套路来,大概率不会犯错。

次要分为两类 Cache-AsideCache-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 查问申请会通过哪些步骤:

  1. 和 MySQL server 建设 TCP 连贯:

    • 三次握手
  2. MySQL 权限认证:

    • Server 向 Client 发送 密钥
    • Client 应用密钥加密用户名、明码等信息,将加密后的报文发送给 Server
    • Server 依据 Client 申请包,验证是否是非法用户,而后给 Client 发送认证后果
  3. Client 发送 SQL 语句
  4. Server 返回语句执行后果
  5. MySQL 敞开
  6. 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 大佬对系统进行重构。

  1. P9 大佬第一步先对数据库进行垂直分库,

依据业务关联性强弱,将它们分到不同的数据库, 比方订单库,商家库、领取库、用户库。

  1. 第二步是对一些大表进行垂直分表,将一个表依照字段分成多表,每个表存储其中一部分字段。 比方商品详情表可能最后蕴含了几十个字段,然而往往最多拜访的是商品名称、价格、产地、图片、介绍等信息,所以咱们将不常拜访的字段独自拆成一个表。
  • 因为垂直分库曾经依照业务关联切分到了最小粒度,数据量任然十分大,P9 大佬开始程度分库,比方能够把订单库分为订单 1 库、订单 2 库、订单 3 库 …… 那么如何决定某个订单放在哪个订单库呢?能够思考对主键通过哈希算法计算放在哪个库。
  • 分完库,单表数据量任然很大,查问起来十分慢,P9 大佬决定按日或者按月将订单分表,叫做日表、月表。

分库分表同时会带来一些问题,比方平时单库单表应用的主键自增个性将作废,因为某个分区库表生成的主键无奈保障全局惟一,这就须要引入全局 UUID 服务了。

通过一番大刀阔斧的重构,拼夕夕复原了来日的生机,大家又能够欢快的在下面相互砍一刀了。

(分库分表会引入很多问题,并没有一一介绍,这里只是为了解说什么是分库分表)

八、具体技法

8.1 零拷贝

高性能的服务器该当防止不必要数据复制,特地是在 用户空间和内核空间之间的数据复制。 比方 HTTP 动态服务器发送动态文件的时候,个别咱们会这样写:

如果理解 Linux IO 的话就晓得这个过程蕴含了内核空间和用户空间之间的屡次拷贝:

内核空间和用户空间之间数据拷贝须要 CPU 亲自实现,然而对于这类 数据不须要在用户空间进行解决 的程序来说,这样的两次拷贝显然是节约。什么叫 「不须要在用户空间进行解决」?

比方 FTP 或者 HTTP 动态服务器,它们的作用只是将文件从磁盘发送到网络,不须要在中途对数据进行编解码之类的计算操作。

如果可能间接将数据在内核缓存之间挪动,那么除了缩小拷贝次数以外,还能防止内核态和用户态之间的上下文切换。

而这正是零拷贝(Zero copy)干的事,次要就是利用各种零拷贝技术,缩小不必要的数据拷贝,将 CPU 从数据拷贝这样简略的工作解脱进去,让 CPU 专一于别的工作。

罕用的零拷贝技术:

  1. mmap

    mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间能够共享内核空间的数据。这样,在进行网络传输时,就能够缩小内核空间到用户空间的拷贝次数。

  1. 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,对变量原子性 + 1
  • Test-And-Set,这是各种锁算法的外围,在 AT&T/GNU 汇编语法下,叫 xchg 指令,我会独自写一篇如何应用 xchg 实现各种锁。

8.3 序列化与反序列化

先看看维基百科怎么定义的序列化:

In computing, serialization (US spelling) or serialisation (UK spelling) is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, across a computer network) and reconstructed later (possibly in a different computer environment). When the resulting series of bits is reread according to the serialization format, it can be used to create a semantically identical clone of the original object. For many complex objects, such as those that make extensive use of references, this process is not straightforward. Serialization of object-oriented objects does not include any of their associated methods with which they were previously linked.

我置信你大概率没有看完下面的英文形容,其实我也不爱看英文材料,总感觉很慢,然而计算机领域一手的学习材料都是美帝那边的,所以没方法,必须逼本人去试着读一些英文的材料。

实际上也没有那么难,相熟罕用的几百个专业名词,句子都是非常简单的一些从句。没看的话,再倒回去看看?

这里我就不做翻译了,次要是程度太低,预计做到「信达雅」的信都很难。

扯远了,还是回到序列化来。

所有的编程肯定是围绕数据开展的,而数据出现模式往往是结构化的,比方 构造体(Struct)、类(Class)。 然而当咱们 通过网络、磁盘等传输、存储数据的时候却要求是二进制流。 比方 TCP 连贯,它提供给下层利用的是面向连贯的牢靠字节流服务。那么如何将这些构造体和类转化为可存储和可传输的字节流呢?这就是序列化要干的事件,反之,从字节流如何复原为结构化的数据就是反序列化。

序列化解决了对象长久化和跨网络数据交换的问题。

序列化个别依照序列化后的后果是否可读,可分为以下两类:

  • 文本类型:

    如 JSON、XML,这些类型可读性十分好,是自解释的。也经常用在前后端数据交互上,因为接口调试,可读性高十分不便。然而毛病就是信息密度低,序列化后占用空间大。

  • 二进制类型

    如 Protocol Buffer、Thrift 等,这些类型采纳二进制编码,数据组织得更加紧凑,信息密度高,占用空间小,然而带来的问题就是根本不可读。

还有 Java、Go 这类语言内置了序列化形式,比方在 Java 里实现了 Serializable 接口即示意该对象可序列化。

说到这让我想起了大一写的的两个程序,一个是用刚 C 语言写的公交管理系统,过后须要将公交线路、站点信息长久化保留,过后的计划就是每个公交线路写在一行,用 “|” 宰割信息,比方:

5|6:00-22:00| 大学城|南山站|北京站
123|6:30-23:00|南湖小道|茶山刘|世界

第一列就是线路编号、第二项是发车工夫、前面就是路径的站点。是不是十分原始?实际上这也是一种序列化形式,只是效率很低,也不通用。而且存在一个问题就是如果信息中蕴含“|”怎么办?当然是用本义。

第二个程序是用 Java 写的网络五子棋,过后须要通过网络传输示意棋子地位的对象,查了一圈最初发现只须要实现 Serializable 接口,本人什么都不必干,就能本人实现对象的序列化,而后通过网络传输后反序列化。过后哪懂得这就叫序列化,只感觉牛逼、神奇!

最初实现了一个能够网络五子棋,拉着隔壁室友一起玩。。。真的是成就感满满哈哈哈。

说来在编程方面,曾经很久没有这样的成就感了。

总结

这篇文章次要是浅显的介绍了一些零碎设计、系统优化的套路和最佳实际。

不晓得你发现没有,从缓存到音讯队列、CAS……,很多看起来很牛逼的架构设计其实都来源于操作系统、体系结构。

所以我十分热衷学习一些底层的基础知识,这些看似古老的技术是通过工夫洗礼留下来的好货色。当初很多的新技术、框架看似十分厉害,实则不少都是新瓶装旧酒,每几年又会被淘汰一批。

最初说一句(求关注)

这篇文章写了挺久的,从写文章、画图,调格局每一步都很花工夫。如果感觉对你有帮忙的话,能够点个 关注 或者 在看 激励下~

文章继续更新,微信搜寻「编程指北」第一工夫获取,回复【材料】有我筹备的一线 BAT 大厂面试材料和简历模板。

期待你的关注~
有任何问题,欢送留言~

退出移动版