40 信息流设计(二):通用信息流系统的拉模式要如何做?

你好,我是唐扬。

在前一节课中,我带你了解了如何用推模式来实现信息流系统,从中你应该了解到了推模式存在的问题,比如它在面对需要支撑很大粉丝数量的场景时,会出现消息推送延迟、存储成本高、方案可扩展性差等问题。虽然我们也会有一些应对的措施,比如说选择插入性能更高的数据库存储引擎来提升数据写入速度,降低数据推送延迟;定期删除冷数据以减小存储成本等等,但是由于微博大 V 用户粉丝量巨大,如果我们使用推模式实现信息流系统,那么只能缓解这些用户的微博推送延迟问题,没有办法彻底解决。

这个时候你可能会问了:那么有没有一种方案可以一劳永逸地解决这个问题呢?当然有了,你不妨试试用拉模式来实现微博信息流系统。那么具体要怎么做呢?

如何使用拉模式设计信息流系统

所谓拉模式,就是指用户主动拉取他关注的所有人的微博,将这些微博按照发布时间的倒序进行排序和聚合之后,生成信息流数据的方法。

按照这个思路实现微博信息流系统的时候你会发现:用户的收件箱不再有用,因为信息流数据不再出自收件箱,而是出自发件箱。发件箱里是用户关注的所有人数据的聚合。因此用户在发微博的时候就只需要写入自己的发件箱,而不再需要推送给粉丝的收件箱了,这样在获取信息流的时候,就要查询发件箱的数据了。

这个逻辑我还用 SQL 的形式直观地表达出来,方便你理解。假设用户 A 关注了用户 B、C、D,那么当用户 B 发送一条微博的时候,他会执行这样的操作:

1
insert into outbox(userId, feedId, create_time) values("B", $feedId, $current_time); // 写入 B 的发件箱

当用户 A 想要获取他的信息流的时候,就要聚合 B、C、D 三个用户收件箱的内容了:

1
select feedId from outbox where userId in (select userId from follower where fanId = "A") order by create_time desc

你看,拉模式的实现思想并不复杂,并且相比推模式来说,它有几点明显的优势。

首先,拉模式彻底解决了推送延迟的问题,大 V 发微博的时候不再需要推送到粉丝的收件箱,自然就不存在延迟的问题了。

其次,存储成本大大降低了。在推模式下,谢娜的粉丝有 1.2 亿,那么谢娜发送一条微博就要被复制 1.2 亿条,写入到存储系统中。在拉模式下只保留了发件箱,微博数据不再需要复制,成本也就随之降低了。

最后,功能扩展性更好了。比如,微博增加了分组的功能,而你想把关注的 A 和 B 分成一个单独的组,那么 A 和 B 发布的微博就形成了一个新的信息流,这个信息流要如何实现呢?很简单,你只需要查询这个分组下所有用户(也就是 A 和 B),然后查询这些用户的发件箱,再把发件箱中的数据,按照时间倒序重新排序聚合就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<Long> uids = getFromGroup(groupId); // 获取分组下的所有用户



Long<List<Long>> ids = new ArrayList<List<Long>>();



for(Long id : uids) {



ids.add(getOutboxByUid(id)); // 获取发件箱的内容 id 列表



}



return merge(ids); // 合并排序所有的 id

拉模式之所以可以解决推模式下的所有问题,是因为在业务上关注数始终是有上限的,那么它是不是一个无懈可击的方案呢?当然不是,拉模式也会有一些问题,在我看来主要有这样两点。

第一点,不同于推模式下获取信息流的时候,只是简单地查询收件箱中的数据,在拉模式下,我们需要对多个发件箱的数据做聚合,这个查询和聚合的成本比较高。微博的关注上限是 2000,假如你关注了 2000 人,就要查询这 2000 人发布的微博信息,然后再对查询出来的信息做聚合。

那么,如何保证在毫秒级别完成这些信息的查询和聚合呢?答案还是缓存。我们可以把用户发布的微博 ID 放在缓存中,不过如果把全部用户的所有微博都缓存起来,消耗的硬件成本也是很高的。所以我们需要关注用户浏览信息流的特点,看看是否可能对缓存的存储成本做一些优化。

在实际执行中,我们对用户的浏览行为做了抽量分析,发现 97% 的用户都是在浏览最近 5 天之内的微博,也就是说,用户很少翻看五天之前的微博内容,所以我们只缓存了每个用户最近 5 天发布的微博 ID。假设我们部署 6 个缓存节点来存储这些微博 ID,在每次聚合时并行从这几个缓存节点中批量查询多个用户的微博 ID,获取到之后再在应用服务内存中排序后就好了,这就是对缓存的 6 次请求,可以保证在 5 毫秒之内返回结果。

第二,缓存节点的带宽成本比较高。你想一下,假设微博信息流的访问量是每秒 10 万次请求,也就是说,每个缓存节点每秒要被查询 10 万次。假设一共部署 6 个缓存节点,用户人均关注是 90,平均来说每个缓存节点要存储 15 个用户的数据。如果每个人平均每天发布 2 条微博,5 天就是发布 10 条微博,15 个用户就要存储 150 个微博 ID。每个微博 ID 要是 8 个字节,150 个微博 ID 大概就是 1kB 的数据,单个缓存节点的带宽就是 1kB * 10 万 = 100MB,基本上跑满了机器网卡带宽了。那么我们要如何对缓存的带宽做优化呢?

在14 讲中我提到,部署多个缓存副本提升缓存可用性,其实,缓存副本也可以分摊带宽的压力。我们知道在部署缓存副本之后,请求会先查询副本中的数据,只有不命中的请求才会查询主缓存的数据。假如原本缓存带宽是 100M,我们部署 4 组缓存副本,缓存副本的命中率是 60%,那么主缓存带宽就降到 100M * 40% = 40M,而每组缓存副本的带宽为 100M / 4 = 25M,这样每一组缓存的带宽都降为可接受的范围之内了。

img

在经过了上面的优化之后,基本上完成了基于拉模式信息流系统方案的设计,你在设计自己的信息流系统时可以参考借鉴这个方案。另外,使用缓存副本来抗流量也是一种常见的缓存设计思路,你在项目中必要的时候也可以使用。

推拉结合的方案是怎样的

但是,有的同学可能会说:我在系统搭建初期已经基于推模式实现了一套信息流系统,如果把它推倒重新使用拉模式实现的话,系统的改造成本未免太高了。有没有一种基于推模式的折中的方案呢?

其实我在网易微博的时候,网易微博的信息流就是基于推模式来实现的,当用户的粉丝量大量上涨之后,我们通过对原有系统的改造实现了一套推拉结合的方案,也能够基本解决推模式存在的问题,具体怎么做呢?

方案的核心在于大 V 用户在发布微博的时候,不再推送到全量用户,而是只推送给活跃的用户。这个方案在实现的时候有几个关键的点。

首先,我们要如何判断哪些是大 V 用户呢?或者说,哪些用户在发送微博时需要推送全量用户,哪些用户需要推送活跃用户呢?在我看来,还是应该以粉丝数作为判断标准,比如,粉丝数超过 50 万就算作大 V,需要只推送活跃用户。

其次,我们要如何标记活跃用户呢?活跃用户可以定义为最近几天内在微博中有过操作的用户,比如说刷新过信息流、发布过微博、转发评论点赞过微博,关注过其他用户等等,一旦有用户有过这些操作,我们就把他标记为活跃的用户。

而对大 V 来说,我们可以存储一个活跃粉丝的列表,这个列表里面就是我们标记的活跃用户。当某一个用户从不活跃用户变为活跃用户时,我们会查询这个用户的关注者中哪些是大 V,然后把这个用户写入到这些大 V 的活跃粉丝列表里面,这个活跃粉丝列表是定长的,如果活跃粉丝数量超过了长度,就把最先加入的粉丝从列表里剔除,这样可以保证推送的效率。

最后,一个用户被从活跃粉丝列表中剔除,或者是他从不活跃变成了活跃后,由于他不在大 V 用户的活跃粉丝列表中,所以也就不会收到微博的实时推送,因此,我们需要异步地把大 V 用户最近发布的微博插入到他的收件箱中,保证他的信息流数据的完整性。

img

采用推拉结合的方式可以一定程度上弥补推模式的缺陷,不过也带来了一些维护的成本,比如说系统需要维护用户的在线状态,还需要多维护一套活跃的粉丝列表数据,在存储上的成本就更高了。

因此,这种方式一般适合中等体量的项目,当粉丝量级在百万左右,活跃粉丝数量在 10 万级别时,一般可以实现比较低的信息传播延迟以及信息流获取延迟,但是当你的粉丝数量继续上涨,流量不断提升之后,无论是活跃粉丝的存储还是推送的延迟都会成为瓶颈,所以改成拉模式会更好的支撑业务。

课程小结

以上就是本节课的全部内容了。本节课我带你了解了基于拉模式和推拉结合模式实现信息流系统的方案,这里你需要了解的几个重点是:

在拉模式下,我们只需要保存用户的发件箱,用户的信息流是通过聚合关注者发件箱数据来实现的;

拉模式会有比较大的聚合成本,缓存节点也会存在带宽的瓶颈,所以我们可以通过一些权衡策略尽量减少获取数据的大小,以及部署缓存副本的方式来抗并发;

推拉结合的模式核心是只推送活跃的粉丝用户,需要维护用户的在线状态以及活跃粉丝的列表,所以需要增加多余的空间成本来存储,这个你需要来权衡。

拉模式和推拉结合模式比较适合微博这种粉丝量很大的业务场景,因为它们都会有比较可控的消息推送延迟。你可以看到,在这两节课程中我们灵活使用数据库分库分表、缓存消息队列、发号器等技术,实现了基于推模式、拉模式以及推拉结合模式的信息流系统,你在做自己系统的方案设计时,应该充分发挥每种技术的优势,权衡业务自身的特性,最终实现技术和业务上的平衡,也就是既能在业务上满足用户需求,又能在技术上保证系统的高性能和高可用。

加餐 数据的迁移应该如何做?

你好,我是唐扬。

在“数据库优化方案(二):写入数据量增加时,如何实现分库分表?”中我曾经提到,由于 MySQL 不像 MongoDB 那样支持数据的 Auto Sharding(自动分片),所以无论是将 MySQL 单库拆分成多个数据库,还是由于数据存储的瓶颈,不得不将多个数据库拆分成更多的数据库时,你都要考虑如何做数据的迁移。

其实,在实际工作中,不只是对数据库拆分时会做数据迁移,**很多场景都需要你给出数据迁移的方案,**比如说某一天,你的老板想要将应用从自建机房迁移到云上,那么你就要考虑将所有自建机房中的数据,包括 MySQL,Redis,消息队列等组件中的数据,全部迁移到云上,这无论对哪种规模的公司来说都是一项浩瀚的工程,所以你需要在迁移之前,准备完善的迁移方案。

“数据的迁移”的问题比较重要,也比较繁琐,也是开发和运维同学关注的重点。在课程更新的过程中,我看到有很多同学,比如 @每天晒白牙,@枫叶 11,@撒旦的堕落等等,在留言区询问如何做数据迁移,所以我策划了一期加餐,准备从数据库迁移和缓存迁移两个方面,带你掌握数据迁移的方法,也带你了解数据迁移过程中,需要注意的关键点,尽量让你避免踩坑。

如何平滑地迁移数据库中的数据

你可能会认为:数据迁移无非是将数据从一个数据库拷贝到另一个数据库,可以通过 MySQL 主从同步的方式做到准实时的数据拷贝;也可以通过 mysqldump 工具将源库的数据导出,再导入到新库,这有什么复杂的呢?

其实,这两种方式只能支持单库到单库的迁移,无法支持单库到多库多表的场景。而且即便是单库到单库的迁移,迁移过程也需要满足以下几个目标:

迁移应该是在线的迁移,也就是在迁移的同时还会有数据的写入;

数据应该保证完整性,也就是说在迁移之后需要保证新的库和旧的库的数据是一致的;

迁移的过程需要做到可以回滚,这样一旦迁移的过程中出现问题,可以立刻回滚到源库,不会对系统的可用性造成影响。

如果你使用 Binlog 同步的方式,在同步完成后再修改代码,将主库修改为新的数据库,这样就不满足可回滚的要求,一旦迁移后发现问题,由于已经有增量的数据写入了新库而没有写入旧库,不可能再将数据库改成旧库。

一般来说,我们有两种方案可以做数据库的迁移。

“双写”方案

第一种方案我称之为双写,其实说起来也很简单,它可以分为以下几个步骤:

\1. 将新的库配置为源库的从库,用来同步数据;如果需要将数据同步到多库多表,那么可以使用一些第三方工具获取 Binlog 的增量日志(比如开源工具 Canal),在获取增量日志之后就可以按照分库分表的逻辑写入到新的库表中了。

\2. 同时,我们需要改造业务代码,在数据写入的时候,不仅要写入旧库,也要写入新库。当然,基于性能的考虑,我们可以异步地写入新库,只要保证旧库写入成功即可。**但是,我们需要注意的是,**需要将写入新库失败的数据记录在单独的日志中,这样方便后续对这些数据补写,保证新库和旧库的数据一致性。

\3. 然后,我们就可以开始校验数据了。由于数据库中数据量很大,做全量的数据校验不太现实。你可以抽取部分数据,具体数据量依据总体数据量而定,只要保证这些数据是一致的就可以。

\4. 如果一切顺利,我们就可以将读流量切换到新库了。由于担心一次切换全量读流量可能会对系统产生未知的影响,所以这里**最好采用灰度的方式来切换,**比如开始切换 10% 的流量,如果没有问题再切换到 50% 的流量,最后再切换到 100%。

\5. 由于有双写的存在,所以在切换的过程中出现任何的问题,都可以将读写流量随时切换到旧库去,保障系统的性能。

\6. 在观察了几天发现数据的迁移没有问题之后,就可以将数据库的双写改造成只写新库,数据的迁移也就完成了。

**其中,最容易出问题的步骤就是数据校验的工作,**所以,我建议你在未开始迁移数据之前先写好数据校验的工具或者脚本,在测试环境上测试充分之后,再开始正式的数据迁移。

如果是将数据从自建机房迁移到云上,你也可以使用这个方案,**只是你需要考虑的一个重要的因素是:**自建机房到云上的专线的带宽和延迟,你需要尽量减少跨专线的读操作,所以在切换读流量的时候,你需要保证自建机房的应用服务器读取本机房的数据库,云上的应用服务器读取云上的数据库。这样在完成迁移之前,只要将自建机房的应用服务器停掉,并且将写入流量都切到新库就可以了。

img

这种方案是一种比较通用的方案,无论是迁移 MySQL 中的数据,还是迁移 Redis 中的数据,甚至迁移消息队列都可以使用这种方式,你在实际的工作中可以直接拿来使用。

这种方式的**好处是:**迁移的过程可以随时回滚,将迁移的风险降到了最低。**劣势是:**时间周期比较长,应用有改造的成本。

级联同步方案

这种方案也比较简单,比较适合数据从自建机房向云上迁移的场景。因为迁移上云,最担心云上的环境和自建机房的环境不一致,会导致数据库在云上运行时,因为参数配置或者硬件环境不同出现问题。

所以,我们会在自建机房准备一个备库,在云上环境上准备一个新库,通过级联同步的方式在自建机房留下一个可回滚的数据库,具体的步骤如下:

\1. 先将新库配置为旧库的从库,用作数据同步;

\2. 再将一个备库配置为新库的从库,用作数据的备份;

\3. 等到三个库的写入一致后,将数据库的读流量切换到新库;

\4. 然后暂停应用的写入,将业务的写入流量切换到新库(由于这里需要暂停应用的写入,所以需要安排在业务的低峰期)。

img

**这种方案的回滚方案也比较简单,**可以先将读流量切换到备库,再暂停应用的写入,将写流量切换到备库,这样所有的流量都切换到了备库,也就是又回到了自建机房的环境,就可以认为已经回滚了。

上面的级联迁移方案可以应用在,将 MySQL 从自建机房迁移到云上的场景,也可以应用在将 Redis 从自建机房迁移到云上的场景,如果你有类似的需求可以直接拿来应用。

这种方案优势是简单易实施,在业务上基本没有改造的成本;缺点是在切写的时候需要短暂的停止写入,对于业务来说是有损的,不过如果在业务低峰期来执行切写,可以将对业务的影响降至最低。

数据迁移时如何预热缓存

另外,在从自建机房向云上迁移数据时,我们也需要考虑缓存的迁移方案是怎样的。那么你可能会说:缓存本来就是作为一个中间的存储而存在的,我只需要在云上部署一个空的缓存节点,云上的请求也会穿透到云上的数据库,然后回种缓存,对于业务是没有影响的。

你说的没错,但是你还需要考虑的是缓存的命中率。

如果你部署一个空的缓存,那么所有的请求就都穿透到数据库,数据库可能因为承受不了这么大的压力而宕机,这样你的服务就会不可用了。所以,缓存迁移的重点是保持缓存的热度。

刚刚我提到,Redis 的数据迁移可以使用双写的方案或者级联同步的方案,所以在这里我就不考虑 Redis 缓存的同步了,而是以 Memcached 为例来说明。

使用副本组预热缓存

在“缓存的使用姿势(二):缓存如何做到高可用?”中,我曾经提到,为了保证缓存的可用性,我们可以部署多个副本组来尽量将请求阻挡在数据库层之上。

数据的写入流程是写入 Master、Slave 和所有的副本组,而在读取数据的时候,会先读副本组的数据,如果读取不到再到 Master 和 Slave 里面加载数据,再写入到副本组中。**那么,我们就可以在云上部署一个副本组,**这样,云上的应用服务器读取云上的副本组,如果副本组没有查询到数据,就可以从自建机房部署的主从缓存上加载数据,回种到云上的副本组上。

img

当云上部署的副本组足够热之后,也就是缓存的命中率达到至少 90%,就可以将云机房上的缓存服务器的主从都指向这个副本组,这时迁移也就完成了。

**这种方式足够简单,不过有一个致命的问题是:**如果云上的请求穿透云上的副本组,到达自建机房的主从缓存时,这个过程是需要跨越专线的。

这不仅会占用较多专线的带宽,同时专线的延迟相比于缓存的读取时间是比较大的,一般,即使是本地的不同机房之间的延迟也会达到 2ms~3ms,那么,一次前端请求可能会访问十几次甚至几十次的缓存,一次请求就会平白增加几十毫秒甚至过百毫秒的延迟,会极大地影响接口的响应时间,因此在实际项目中我们很少使用这种方案。

**但是,这种方案给了我们思路,**让我们可以通过方案的设计在系统运行中自动完成缓存的预热,所以,我们对副本组的方案做了一些改造,以尽量减少对专线带宽的占用。

改造副本组方案预热缓存

改造后的方案对读写缓存的方式进行改造,步骤是这样的:

\1. 在云上部署多组 mc 的副本组,自建机房在接收到写入请求时,会优先写入自建机房的缓存节点,异步写入云上部署的 mc 节点;

\2. 在处理自建机房的读请求时,会指定一定的流量,比如 10%,优先走云上的缓存节点,这样虽然也会走专线穿透回自建机房的缓存节点,但是流量是可控的;

\3. 当云上缓存节点的命中率达到 90% 以上时,就可以在云上部署应用服务器,让云上的应用服务器完全走云上的缓存节点就可以了。

img

使用了这种方式,我们可以实现缓存数据的迁移,又可以尽量控制专线的带宽和请求的延迟情况,你也可以直接在项目中使用。

课程小结

以上我提到的数据迁移的方案,都是我在实际项目中,经常用到的、经受过实战考验的方案,希望你能通过这节课的学习,将这些方案运用到你的项目中,解决实际的问题。与此同时,我想再次跟你强调一下本节课的重点内容:

双写的方案是数据库、Redis 迁移的通用方案,**你可以在实际工作中直接加以使用。**双写方案中最重要的,是通过数据校验来保证数据的一致性,这样就可以在迁移过程中随时回滚;

如果你需要将自建机房的数据迁移到云上,那么也可以考虑**使用级联复制的方案,**这种方案会造成数据的短暂停写,需要在业务低峰期执行;

缓存的迁移重点,是保证云上缓存的命中率,你可以**使用改进版的副本组方式来迁移,**在缓存写入的时候,异步写入云上的副本组,在读取时放少量流量到云上副本组,从而又可以迁移部分数据到云上副本组,又能尽量减少穿透给自建机房造成专线延迟的问题。

**如果你作为项目的负责人,**那么在迁移的过程中,你一定要制定周密的计划:如果是数据库的迁移,那么数据的校验应该是你最需要花费时间来解决的问题。

如果是自建机房迁移到云上,那么专线的带宽一定是你迁移过程中的一个瓶颈点,你需要在迁移之前梳理清楚,有哪些调用需要经过专线,占用带宽的情况是怎样的,带宽的延时是否能够满足要求。你的方案中也需要尽量做到在迁移过程中,同机房的服务,调用同机房的缓存和数据库,尽量减少对于专线带宽资源的占用。

期中测试 10道高并发系统设计题目自测

img

img

img

img

img

img

用户故事 从“心”出发,我还有无数个可能

你好,我是 Longslee,很高兴与大家一起学习《高并发系统设计 40 问》。

我从事软件相关的职业已经有九年时间了,之前在一家税务行业类公司工作,目前在一家电信行业相关的公司,从事开发和运维工作。

我并不算“极客时间”的老用户,因为接触“极客时间”只有短短几个月,一开始只抱着试试看的心态,尝试着订阅了几门课程,后来便自然而然地将它当作工作之余,获取信息的必需品。

要说跟这门课结缘,还是在今年 10 月份,那时,我偶然打开“极客时间”,看到了《高并发系统设计 40 问》的课程,被开篇词的题目**“你为什么要学习高并发系统设计”**吸引了。开篇词中提到:

公司业务流量平稳,并不表示不会遇到一些高并发的需求场景;为了避免遇到问题时手忙脚乱,你有必要提前储备足够多的高并发知识,从而具备随时应对可能出现的高并发需求场景的能力……

这些信息着实戳中了我。

回想起来,自己所处的行业是非常传统的 IT 行业,几乎与“互联网”不着边,所以我平时特别难接触一线的技术栈。然而,虽然行业传统,但并不妨碍日常工作中高并发的出现,比如,偶尔出现的线上促销活动。

单纯从我自己的角度出发,除了因为开篇词戳中之外,选择这个课程,还在于自己想拓宽视野、激发潜能,另一方面,当真的遇到“高并发”时,不至于望洋兴叹,脑海一片空白。

**在课程设计上,**每一节课的标题都是以问号结束,这种看似寻常的设计,很容易让我在学习时,联想到自己的实际工作,从而先问问自己:我们为什么要架构分层?如何避免消息重复?等等,自己有了一些答案后,再进入正式的学习,对概念性的知识查漏补缺。

我个人认为,这也算是这门课程的一个小的特色。唐扬老师抛出问题,并用自己的经验进行回答,让这篇文章有了一个很好的闭环。

目前来说,我所在的行业和项目,为了应对日益复杂的业务场景,和日渐频繁的促销活动,也在慢慢地转变,更多地引入互联网行业知识,产品也更加与时俱进。

作为这个行业的一员,在日常工作中,我自然也遇到了一些难题,碰到了一些瓶颈,但是在寻找解决方式的时候,往往局限在自己擅长的技术体系和历史的过往经验上。而在学习了这门课之后,我拓宽了眼界,会不自主地思考“是不是可以用今天学到的方式解决某些问题?”“当初选用的中间件和使用方式合不合理?”等等。

而且,就像我提到的,自己所处的行业在不断改变,其实,就目前的趋势来看,很早就存在的信息化产品和目前主流的互联网产品渐渐难以界定了。就比如高校的教务系统,听起来好像跟我们接触的各类网站大不一样,但是在开学的时候,又有多少选课系统能扛住同学们瞬间的巨大流量呢?

《17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?》讲的就是各厂处理可预见且短时间内大流量的“套路”,而我认为,这个“套路”也可以应用到大学的选课系统。因为教务系统在通常情况下都是很闲的,如果整体升级来提高 QPS 性价比太低,所以只要保证在选课时,服务的稳定性就好了。这里可以引入消息队列,来缓解数据库的压力,再通过异步拆分,提高核心业务的处理速度。

**其实,还有好多节课都给我留下了深刻的印象,**比如,第 2 讲、第 10 讲、第 13 讲等等。

单看《02 | 架构分层:我们为什么一定要这么做?》这个题目,我一开始会觉得“老生常谈”,软件分层在实际项目中运用的太多太多了,老师为什么单独拿出来一讲介绍呢?然而当我看到“如果业务逻辑很简单的话,可不可以从表示层直接到数据访问层,甚至直接读数据库呢?”这句话时,联系到了自己的实际业务:

我所参与的一个工程,确实因为业务逻辑基本等同数据库逻辑,所以从表示层直接与数据访问层交互了。但是如果数据库或者数据访问层发生改动,那将要修改表示层的多个地方,万一漏掉了需要调整的地方,连问题都不好查了,并且如果以后再无意地引入逻辑层,修改的层次也将变多。

对我而言,这篇文章能够有触动我的地方,引发我的思考,所以在接下来的项目中,我坚持选用分层架构。

而《10 | 发号器:如何保证分库分表后 ID 的全局唯一性》**给我的项目提供了思路:**我的需求不是保证分库分表后,主键的唯一性,但由于需要给各个客户端分配唯一 ID,用客户端策略难免重复,所以在读到:

一种是嵌入到业务代码里,也就是分布在业务服务器中。这种方案的好处是业务代码在使用的时候不需要跨网络调用,性能上会好一些,但是就需要更多的机器 ID 位数来支持更多的业务服务器。另外,由于业务服务器的数量很多,我们很难保证机器 ID 的唯一性,所以就需要引入 ZooKeeper 等分布式一致性组件,来保证每次机器重启时都能获得唯一的机器 ID……

我采取了类似发号器的概念,并且摒弃了之前 UUID 似的算法。采用发号器分发的 ID 后,在数据库排序性能有所提升,业务含义也更强了。

除此之外,在学习《13 | 缓存的使用姿势(一):如何选择缓存的读写策略?》之前,我的项目中没有过多地考虑,数据库与缓存的一致性。比如,我在写入数据时,选择了先写数据库,再写缓存,考虑到写数据库失败后事务回滚,缓存也不会被写入;如果缓存写入失败,再设计重试机制。

看起来好像蛮 OK 的样子,但是因为没有考虑到在多线程更新的情况下,确实会造成双方的不一致,**所造成的后果是:有时候从前端查询到的结果与真实数据不符。**后来,根据唐扬老师提到的 Cache Aside(旁路缓存)策略,我顿然醒悟,然后将这一策略用于该工程中,效果不错。这节课,我从唐扬老师的亲身经历中,学到了不少的经验,直接用到了自己的项目中。

真的很感谢唐扬老师,也很开心能够遇到这门课程,在这里,想由衷地表达自己的感谢之情。

**那么我是怎么学习这门课程的呢?**在这里,我想分享几点:

知行合一

学完课程后,除了积极思考“能否用”“怎么用”“何时用”这些问题外,一定要趁热打铁,要么继续深入话题,翻阅其他资料,巩固下来;要么敲敲代码实现一遍,化为自己的技能;如果时间充裕,甚至可以立马着手改进项目。

留言区 = 挖宝区

每节课结束,我都会在留言板留下疑问,或者分享体验,我喜欢问问题其实是跟自己在大学时,参加的一场宣讲会有关。当时,来招聘的负责人是一位美国留学回来的台湾工程师,他介绍完后问大家有没有疑问,并没有人回答。

后来,他讲了一个经历,使我感慨良多。他说当他刚去美国大学的时候,教授讲完课就要答疑,一个白人学生提了一个,在中国学生看来十分简单且幼稚的问题,以后的每节课,这位白人同学都要提问,渐渐地,提的问题他都听不懂了!再后来,教授也不懂了。

所以,我会不断地发问,不懂就问,把留言区当作挖宝区,看大家的留言,进行思考。比如 @李冲同学的几个跟帖,就解答了我对布隆过滤器的误解,并且还知道了另一种布谷鸟过滤器。

勤做笔记

有的时候,我当时理解的比较透彻,可过了两三天之后,就有些模糊了,所以后来,我根据自己的理解写成思维导图形式,随时随地都可以翻阅。另外,在实现这些方案的代码后面,也可以写下相应的注释,Review 的时候还可以温故知新。

**在最后,我也想分享一下自己为什么用专栏这种形式来学习。**善用搜索引擎的同学们都有体会,搜索出来的知识分布在各处,雷同的不少,有经验的介绍甚少,我没办法在有限的时间内,将搜索到的知识形成体系。

当然了,要想系统地学习可以借助书籍。**但是对我来说,**书籍类学习周期长,章节之间的关联性也不大,容易学了这里忘了那里。书籍多是讲一个专业点,对于跨专业的知识经常一笔带过,而专栏,是有作者自己的理解在里边,前后之间有贯通,学习起来轻松愉悦。

**就拿一致性 Hash 这个知识点来说,**我从网上看了不少关于一致性 Hash 的文章,但没有看到应用,更别谈应用中的缺陷,有的描述甚至让我误认为节点变化后,数据也会跟着迁移。唐扬老师的《14 | 缓存的使用姿势(二):缓存如何做到高可用?》,倒是给了我网络上看不到的盲区,通过在留言区与老师交流后,颇有一种豁然开朗的收获感。

当然了,这些只是我个人的感受,见仁见智,**你或许有自己的学习方法,也或许大家的起点不同,**在这里,我只想把自己的真实感受分享出来,也十分感谢大家倾听我的故事。

总的来说,想要提升自己,并没有捷径,只有一步一步地踏实前行,从踩过的坑中,努力地爬出来。

对我来说,唐扬老师的《高并发系统设计 40 问》犹如及时雨一般的存在,弥补了我高并发相关知识上的缺陷,我相信,认真学完课程之后,自己的视野一定有所开拓,职业生涯也会进入新的篇章。

结束语 学不可以已

你好,我是唐扬。

时间一晃而过,四个月的学习已经接近尾声了,在 103 个日夜里,我们共同学习了 45 篇高并发系统设计的相关文章,从基础篇,逐渐扩展到演进篇,最终进行了实战分析和讲解。

这段日子里,我们一起沟通交流,很多同学甚至在凌晨还在学习、留言,留言区里经常会看到熟悉的身影,比如 @小喵喵,@吃饭饭,@Keith。还有一些同学分享了一些新的知识,比如 @蓝魔,是你们的积极和努力鼓励我不断前进,让我明白知识无止境。在写稿之余,我也订阅了几节极客时间的课程,也买了几本相关的书籍,努力为你们交付高质量的内容。这 103 个日夜虽然辛苦,但也是充满感恩的,在这里,我由衷感谢你的一路相伴!

我知道,有一些同学希望多一些实践的案例分析,我是这样思考的,古人常说“源不深而望流之远,根不固而求木之长,不可”。一些理论基础是必要的,如水之源、树之根,是不能跨越的。另外,一个实践案例不能完全涵盖一个理论,相反一个理论可以支撑很多的实践案例。正所谓授之以鱼不如授之以渔,我们上数学课不也是要先讲公式的来源,再解决实际问题吗?相信对理论知识活学活用后,你在实际工作中,会收获难能可贵的经验财富,也会做出更好的技术方案。

回顾这些年的工作,我想和你分享几点我个人的看法。我刚开始工作时,经常听别人说程序员是有年纪限制的,35 岁是程序员的终结年龄,那时说实话我心里是有一些忐忑的,可随着年龄不断增长,我看到越来越多的人在 35 岁之后还在行业中如鱼得水,我想,35 这个数字并非强调个人的年纪,而是泛指一个阶段,强调在那个阶段,我们可能会因为个人的种种原因安于现状,不再更新自己的知识库,这是非常错误的。

化用《礼记》中的话,首先,我们要博学之。 你要不断革新知识,所谓的天花板其实更多的是知识性的天花板,活到老学到老才是你在这个行业的必胜法宝,所以,我们应该利用各种优质平台以及零散的时间学习,但是同时你要注意,现在的知识偏向碎片化,如何有条理、系统地学习,将知识梳理成体系,化作自己的内功,是比较关键和困难的。在这里我给你几点建议:

基础知识要体系化,读书是一种很好的获取体系化知识的途径,比如研读《算法导论》提升对数据结构和算法的理解,研读《TCP/IP 协议详解》深入理解我们最熟悉的 TCP/IP 协议栈等等;

多读一些经典项目的源代码,比如 Dubbo,Spring 等等,从中领会设计思想,你的编码能力会得到极大的提高;

多利用碎片化的时间读一些公众号的文章,补充书里没有实践案例的不足,借此提升技术视野。

其次要慎思之。 诚然,看书拓展知识的过程中我们需要思考,在实际工作中我们也需要深入思考。没有一个理论可以适应所有的突发状况,高并发系统更是如此。它状况百出,我们最好的应对方法就是在理论的指导下,对每一次的突发状况都进行深入的总结和思考。

然后是审问之。 这种问既是“扪心自问”:

这次的突发问题的根本原因是什么?

以后如何避免同类问题的再次发生?

解决这个问题最优的思路是什么?

同时,也应该是一种他问,是与团队合作,头脑风暴之后的一种补充,我们说你有一个苹果,我有一个苹果我们相互交换,每个人依然只有一个苹果,但是你有一种思想,我也有一种思想,我们相互交换,每个人就有两种思想,所以不断进行团队交流也是一种好的提升自我的方式。

接着是明辨之。 进行了广泛的阅读,积累了大量的工作案例,还要将这些内化于心的知识形成清晰的判断力。某个明星微博的突然沦陷,社区系统的突然挂掉,只是分分钟的事情,要想成为一个优秀的架构师,你必须运用自身的本领进行清晰地判断,快速找到解决方案,只有这样才能把损失控制在最小的范围内。而这种清晰的判断力绝对是因人而异的,你有怎样的知识储备,有怎样的深入思考,就会有怎样清晰的判断力。

最后要笃行之。 学了再多的理论,做了再多的思考,也不能确保能够解决所有问题,对于高并发问题,我们还需要在实践中不断提升自己的能力。

相信你经常会看到这样的段子,比如很多人会觉得我们的固定形象就是“带着眼镜,穿着格子衬衫,背着双肩包,去优衣库就是一筐筐买衣服”。调侃归调侃,我们不必认真,也不必对外在过于追求,因为最终影响你职业生涯的,是思考、是内涵、是知识储备。那么如何让自己更精锐呢?

我想首先要有梯度。我们总希望任何工作都能有个进度条,我们的职业生涯也应该有一个有梯度的进度条,比如,从职场菜鸟到大神再到财务自由,每一步要用多久的时间,如何才能一步一步上升,当然,未必人人能够如鱼得水,但有梦想总是好的,这样你才有目标,自己的生活才会有奔头。

有了梯度的目标之后,接下来要有速度,就像产品逼迫你一样,你也要逼迫自己,让自己不断地加油,不断地更新、提升、完善,尽快实现自己的职业目标。

具备了这两点,就有了一定的高度,你是站在一个目标高度俯视自己的生涯,是高屋建瓴,而不是盲目攀爬。之后你需要做到的是深度,有的朋友总想横向拓展自己的知识面,想要学习一些新奇的知识,这会提升技术视野,原本是无可厚非的,可如果因为追逐新的技术而放弃深入理解基础知识,那就有些得不偿失了。要知道,像是算法、操作系统、网络等基础知识很重要,只有在这些知识层面上有深入的理解,才能在学习新技术的时候举一反三,加快学习的速度,能够帮助你更快地提升广度。

你还要有热度。我们白天和产品经理“相爱相杀”,晚上披星戴月回家与家人“相爱相杀”,如果没有足够的工作热度,这样的日子循环往复,你怎么可能吃得消?而只有当你在自己的行业里规划了梯度、提升了速度、强化了深度、拓宽了广度,才会有足够的自信度,而当你有了自信,有了话语权,那时你就有了幸福感,自然会保有热度。在热度的烘焙下,你又开始新一轮规划,如此良性循环,你才会在工作上游刃有余,生活也会幸福快乐。

在文章结尾,我为你准备了一份调查问卷,题目不多,希望你能抽出两三分钟填写一下。我非常希望听听你对这个专栏的意见和建议,期待你的反馈!专栏的结束,也是另一种开始,我会将内容进行迭代,比如 11 月中旬到 12 月末,我有为期一个月的封闭期,在这期间没有来得及回复的留言,我会花时间处理完;再比如,会针对一些同学的共性问题策划一期答疑或者加餐。

最后,我想再次强调一下为什么要努力提升自己,提升业务能力,直白一点儿说,那是希望我们都有自主选择的权利,而不是被迫谋生;我有话语权,而不是被迫执行,随着年纪的增加,我越发觉得成就感和尊严,能够带给我们快乐。