今日内容
今天我们接着上一篇文章的内容。继续来讲如何提升【写性能】。
下面这张图是我们上次给出关于提升写性能的概览图:
打钩的部分是已经在上一篇文章说讲完了的。今天,我们把剩下来的四种方式给讲完。他们分别是【异步法】【批量插入】【文件法】和【缓存法】。
01使用优化 - 异步法
异步法,顾名思义就是:将同步的过程转换为异步的过程。
这种转换的作用在于:将一个很长的同步过程拆成一个或多个步骤,通过类似消息管道进行串联。同时也利用消息管道的重发能力进行补偿。
一个标准的异步化过程如下图这样:
大家跟着这个图的序号看一遍,这个图中显示了两个过程:
第一个过程:将收到的请求写入queue中,然后返回成功。
第二个过程:从queue中消费消息,继而进行后面的写动作。
这里的核心是:使用queue来缓冲所有的写请求。
但是这里有个问题。因为第一步已经返回了用户成功,但是queue中的消息可能存在丢失、溢出、消费过慢的问题。这就导致了用户看到的结果和实际情况不一致。
那我们要怎么解决这个问题呢?我们可以使用下图这种方案来进行优化。
一样,大家跟着这个图中的序号看一遍。这个图和上一个图的区别在于两点。
第一,引入了一个cache。当我们把消息写到消息管道中后,我们就用cache记录一下他的状态。然后在这个消息真正被消费时,从缓存中删除这个状态。
第二,用户的第一阶段动作做完后,返回用户受理状态,而非成功。
用户可以发起请求状态的查询。我们可以根据cache中的情况,给用户返回状态。cache中如果还有,就说明请求处理中,cache中如果没有,就说明请求处理完了。
一般来说,写入动作都会伴有单据的生成。如果你的单据查询量不大的话,也可以不用引入cache。收到用户的查询请求后,直接查询数据库中单据状态返回。
02使用优化 - 批量插入
顾名思义,批量插入的含义就是将原来多个动作一起执行。就像下图这样:
那批量为什么会快呢?很多人没有思考过这个问题。
你想,虽然是把多个请求放在一起请求。但传输的数据量并没有变少,数据库处理的数据也没有变少。为什么批量就变快了呢?
批量的变快的原因是:
第一,可以减少加锁的次数。本来1个请求加1次锁,现在100个请求加1次锁。
第二,批量更新往往可以用上数据库对批量数据操作的优化
那如何实现单笔变批量?
01内存聚合
第一种方式,我把它称之为内存聚合。我们来看下面这张图。
上图中,多个请求过来,写到内存的queue中。然后每个线程循环检查自己的状态。系统中有另一个模块,定时从queue中捞取批量请求一起执行。
这种方式看着很有设计感,但其实并不常用。
因为这种设计有一定的复杂度。并且“请求不释放”、“queue中消息不消费”、“反复轮询请求状态”都容易给系统带来问题。
我把这个方案放在这里,主要是和大家探讨一种技术设计思路,但并不推荐使用。不过话说回来,在操作系统中常能见到这样的设计,所以操作系统真的很厉害。
02流水聚合
流水聚合法简单说就是:将需要处理的请求先入库,然后再批量捞取执行。
我们看下面这个图:
这个图中,我们先把请求的流水落库,然后通过定时任务捞取所有的流水,并在内存中统一计算结果,然后更新数据库,标记原流水状态,结束。
敏感的同学一定已经发现,这个方案中也使用了异步的思想。只是在异步的思想上又叠加了批量处理的思想。
这种方案最大的好处就是:写流水是不锁库。但劣势就是:数据更新较慢,会有时延。
但这里有个问题,如果写入请求的并发量实在太大了。DB就是写请求也扛不住,怎么办呢?
03使用优化 - 文件法
如果数据库扛不住写入请求,那就换个思路,不用DB了,咱们直接写文件。
文件法和批量流水本质一样,都是使用异步的思路。也分为上下两个阶段。
我们先看第一个阶段:写文件。
一阶段做的事情非常简单,就是将请求进来的内容记录到文件中。
接下来我们看二阶段。二阶段其实也非常简单,就像下图这样。
二阶段就是:定时任务起来,服务去读取日志信息,然后执行写入动作。
但这里有个问题。如果文件实在太大的话,这样的执行方式非常的低效。你想,用文件的方式就说明数据量一定小不了。
所以,我们更典型的方式会使用如下这种:
大家务必跟着我的序号来看
第一阶段,会有拆分服务去读取大文件,并把它拆分成小文件存储到分布式文件系统上。然后把分片文件的写入地址等塞到queue里去。
第二阶段,写入服务从queue里读取文件信息,然后处理一个一个小文件。解析其中的内容,并写到数据库里去。
这里的重点是:第一个阶段只能由单个线程去拆文件,以防止文件拆重。第二个阶段则可以用很多的服务器去一起捞取分片文件处理,以此来提高处理速度。
这是大厂处理文件的典型方式。熟悉大数据解决方的同学一定也会觉得很熟悉。没错,hadoop也是这么处理大文件解析的。
这背后,其实是使用了经典的【分而治之】的思想。
04使用优化 - 缓存法
在上一篇文章中我有提到,写性能比读性能难的原因之一是:不能使用缓存。这是我故意留下的一个“错误”。
今天我们提到,缓存中间件也配有持久化方案。只是他的持久化方案不能保证数据完全安全。
但是,我们所有的场景都需要保证数据一点都不能丢吗?
如果有场景可以容忍一定程度的数据丢失,那缓存来当做数据库使用其实是可以的。比如点赞数、弹幕数、播放数、在线人数等这样的计数类需求。
这类需求往往请求量可能很大,但可以容忍在极端情况下有一小部分计数值的丢失。
那既然是把缓存当做DB来用。我们就要看看怎么来做持久化。
持久化的作用主要有两种:一个是故障的恢复,另一个是数据保存。
对于“故障恢复”来说,如果没有特别的要求,可以就依赖缓存中间件自己提供的能力。
对于“数据保存”来说,如果你有这个容量,持续放在缓存中也是可以的,但如果缓存容量不够,一些较冷的数据想放在硬盘上。你就需要定时的去把缓存数据刷到数据库中,并且在需要的时候再实时刷进缓存。
下面是一张整体的示意图:
05性能小结
到这里,我们把读写性能都讲完了。
我们讲了很多的内容,但比这些内容更重要的是背后的设计思想。在这里我就和大家一起再来梳理一下。
我们直接来看下面我画的这张图。
【避免重复计算】重复的活儿不重复干,用空间换时间。
【减少无谓步骤】只为重要的事情付出时间。
【避免过程阻塞】能不等就不等,异步是个宝。
【引入更多资源】找人来干活,死磕不是办法。
【数据职责分离】只供读还是只供写,供全局还是供局部。
【合理技术选型】审时度势,先做选择。
【配合产品方案】产研是一家,问题一起扛。
今日小结
今天我们继续讲了【写性能】的提升方式。其中包括:“使用异步的方式”、“使用批量的方式”、“使用文件的方式”以及“使用缓存的方式”。