从开源项目中学习

对于程序员来说,这个时代是最好的时代。随着软硬件的快速发展,电脑变得越来越便宜,获取一台用于编程的电脑不再是大问题。更重要的是,当你拥有了电脑后,获取编程知识的途径相比以往也更加丰富,有各种在线课程手把手地带你一步一步学习,有各种专栏把总结好的知识教给你。当然这些都是被动地接受知识,要想真正地掌握还是离不开实践,而研究开源项目就是一种好的实践方式。

学习设计模式的使用方法

设计模式的特点是比较抽象,初学起来不那么容易理解。感觉理解了,用起来又会觉得不那么顺畅,一种情况是不知道该用哪种设计模式。另一种是知道用哪种设计模式,但针对自己的场景又不知道该如何实现。要解决这些问题,一种好的方式是先学习别人写的代码,在这基础上去修改,然后逐步尝试自己去实现。

通常情况下,好的开源项目对代码质量要求比较高,可以尝试去阅读源码,找到一些应用设计模式的代码来学习。以开源配置中心 apollo 为例,你可以很容易地找到单例模式相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConfigService {
// 饿汉式,类加载时就会初始化
// 正常情况下一定会用到该实例,不存在浪费内存的情况
private static final ConfigService s_instance = new ConfigService();

private volatile ConfigManager m_configManager;
private volatile ConfigRegistry m_configRegistry;

// DCL双重校验锁,需要用时再加载
private ConfigManager getManager() {
if (m_configManager == null) {
synchronized (this) {
if (m_configManager == null) {
m_configManager = ApolloInjector.getInstance(ConfigManager.class);
}
}
}

return m_configManager;
}

// 忽略其他代码
}

如果你业务中也需要使用单例,那么就可以参考这些代码去实现自己的逻辑

当你需要实时修改配置并做相应业务处理时,比如打开一个开关、修改一个灰度配置,这时你就需要往 apollo 注册一个 listener,用来监听配置变更事件,这里就使用到了观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class AbstractConfigRepository implements ConfigRepository {
// 忽略其他代码

@Override
public void addChangeListener(RepositoryChangeListener listener) {
if (!m_listeners.contains(listener)) {
// 注册一个观察者
m_listeners.add(listener);
}
}

protected void fireRepositoryChange(String namespace, Properties newProperties) {
for (RepositoryChangeListener listener : m_listeners) {
try {
// 向所有观察者通知变更
listener.onRepositoryChange(namespace, newProperties);
} catch (Throwable ex) {
Tracer.logError(ex);
logger.error("Failed to invoke repository change listener {}", listener.getClass(), ex);
}
}
}
}

此外 apollo 还使用了工厂模式、装饰器模式等,可以尝试自己去阅读学习。根据实际情况,不一定局限于这一个项目,比如如果你使用过 RocketMq,那么你在消费消息的时候也需要注册一个 listener,这也是使用的观察者模式,可以通过对比 apollo 里的 listener 和 RocketMq 里的 listener,看看有什么共同点、不同点以及解决了什么问题,通过这种对比和思考可以加深理解,使得对设计模式的理解更进一步。最终通过不断地实践和总结掌握其精髓,不再拘泥于设计模式的形式。

学习底层技术的原理

俗话说:知其然知其所以然。仅仅掌握一门技术的使用方式是不够的,还需要掌握其实现原理,这样当出现问题时不至于手足无措,可以根据其原理去分析,找到问题根源并解决。这也是为什么同样一个问题,有些人折腾一下午也没解决,有些人看一眼就猜到大概是哪里的问题,几分钟就可以搞定。

设想一个场景,客户端要从服务端获取实时信息,都有哪些实现方案?

一个简单的方案是:服务端提供一个查询接口,客户端每隔一段时间去查询一次。为了尽可能实时地获取到数据,客户端查询的间隔要尽可能短,但是这样服务端的 qps 会变高。这就产生了矛盾,所以需要平衡两者,选一个折中的时间。可见这个短轮询方案存在固有的缺陷。

如果你看过 apollo 的源码,那么你应该知道它使用的是长轮询的方式:当服务端收到客户端的请求后,如果没有数据变更,则会等待一段时间(超时)后再返回结果(无变更),如果在这段时间内有变更,则直接返回给客户端,这样客户端就能第一时间获取到最新数据了。与短轮询相比,使用长轮询的方式获取数据无延迟,但是服务端需要维护 http 请求的信息,如果请求数量比较大,则对服务端会产生比较大的压力。

实际上还有另外一种长连接的方式,即客户端和服务端维持一条 tcp 连接通道,当服务端有数据需要通知客户端时,可以直接通过该通道发送过去。和长轮询相比,服务端只需要维护连接的 socket 信息,内存占用相对小很多,通过使用 IO 多路复用技术,一台机器可以处理几十万的 tcp 连接。

如果你对这些技术感兴趣,那么你可以阅读 apollo 的源码,学习它的长轮询的客户端和服务端是如何实现的,可以尝试自己实现一个服务端。也可以阅读 netty 的源码,学习它是如何处理 IO 多路复用的,以及如何实现一个长连接的客户端及服务端,甚至可以尝试实现一个简单聊天室、一个网关。通过这些,你对相关技术的理解就不仅仅是停留在书本上、理论上,而是具体实际的操作经验,能够将理论和实际想结合。

学习解决问题的方法

前面说到学习底层技术的原理,更进一步,我们可以去思考技术的背景,也就是说是在什么样的条件下遇到了什么问题,尝试想下都有哪些解决方案,为什么它选择了这个方案,都有哪些考虑。这并不一定有正式答案,甚至可能是见仁见智,但这不妨碍我们去思考、去总结。

例如对于 apollo 选择长轮询的方案,我的想法是:它比短轮询更实时,又比长连接实现起来更简单。apollo 通常用于公司内部服务,客户端实例的数量可控,维持连接不会对服务端造成太大压力。故综合起来看它是最合适的。

你可以看到,技术的选型,并不一定是选择最好的、最高级的,而是选择最合适的。这也是取舍的艺术。

造轮子是学习一门技术的好方法

对于造轮子这件事,不应使用非黑即白的看法。从个人角度来讲,造轮子能够以最真实的情况去实践,对于提高个人技能、掌握技术原理还是很有帮助的。

以我自己为例,我在从一个大厂来到一个小公司之后,就遇到了服务接口测试困难且不方便的问题,回想之前使用 pigeon 的 4080 端口测试多么方便,我就想能否针对新的 rpc 框架实现一套呢?因此我花了一个周六的时间实现了一个基本功能,证明这件事是可行的,后续又熬了几次夜,终于将 pigeon 的调试功能移植了过去。通过这件事,我对 rpc 框架的理解变得更加深刻了,后面也经常帮别人解决 rpc 调用相关的各种问题。另一方面,这个工具在没有推广的情况下传遍了公司,说明大家是有这个需求的,它也刚好解决了服务测试的痛点。

学习好的架构设计

在一个团队内部,能够主导或参与架构设计的只有极少数人,甚至存在业务发展快基本不做设计的情况。这种情况下如何学习架构设计呢?如果有好的架构案例学习最好,如果没有也不要紧,可以去学习开源框架的架构设计。好的开源项目的设计文档一般比较完善,另外还会有相关的书籍介绍,博客的数量可能更多(要注意甄别质量)。

若要学习网络框架设计,可以研究下 Tomcat、Jetty、Netty 等。例如对于 Tomcat,在了解了其内部组成及原理后,可以去思考其解决了什么问题,以及模块的设计问题

  1. 为什么拆成了连接器+容器
  2. 连接器是如何设计的,都包含了哪些组件,分别是解决什么问题的
  3. 容器为什么设计成层次结构

对于和 Tomcat 功能比较类似的 Jetty,它又是采用了另外一种更灵活的 Handler 组件设计方案,以及 EatWhatYouKill 线程模型,使得它跟 Tomcat 具有完全不同的风格和特点。

若要学习分布式系统设计,可以研究 RPC 框架、mq 框架、分布式协调工具 Zookeeper、redis 等。分布式领域离不开 CAP 原理,可以关注各个系统在 C 一致性 和 A 可用性 方面的取舍。当然事情没有绝对的好与坏,有的只是对各种优劣的取舍。

例如虽然 Zookeeper 更注重 C 一致性,对于 RPC 框架这种场景,显然可用性比一致性更加重要,但是很多公司是采用 Zookeeper 来实现注册中心,这是因为对于一个规模比较小的公司来说,服务的数量比较少,Zookeeper 集群的压力不会太多,绝大多数情况下 Zookeeper 是比较稳定的。另一个考虑因素是可维护性,Zookeeper 太普及了,熟悉的人比较多,而且大部分坑都有人踩过了,相关文档、资源比较丰富,遇到问题好解决。此外如果公司本来就有使用 Zookeeper,那么开发、部署成本会更低,这些都是要考虑的因素。