# 后端开发工程师

# 架构设计

# 缓存

通读 (read-through) 缓存对应用透明,常见的有:CDN、反向代理缓存。旁路 (cache-aside) 缓存对应用不透明,应用首先从缓存中读数据,若没有命中则去数据源取数据,取完将结果存入缓存。

旁路缓存可以由应用使用哈希表来实现,这时候数据缓存在应用的堆空间内。大型互联网应用需要的缓存数据量太大,堆空间装不下,这时候可以使用远程的分布式缓存 (如:Redis、Memcached)。

分布式缓存会将数据“拆分”并存储在多台机器上,因此读取数据前要先进行路由计算,数据拆分算法和路由算法本质都是哈希算法。余数哈希算法虽然简单,但无法很好地处理服务器扩容的场景,因此我们可以采用一致性哈希算法。

缓存会带来数据脏读的问题,主要解决办法有:过期失效、失效通知。一般业务场景都可以容忍短时间的数据不一致,所以过期失效是最常用的办法,失效通知方法会增加系统设计复杂度。

# RPC

RPC 就是让业务像调用本地函数一样去使用远程计算机提供的服务。发起 RPC 调用请求的那一方叫做调用方,被调用的一方叫做服务提供方。

RPC 的核心有两点:

  • 数据序列化框架
  • 网络传输协议

传输协议可以直接用 HTTP 协议,也可以基于 TCP 定制私有协议。

序列化/反序列化的方法有很多,各语言都有自带方法序列化为二进制。但事情远没有那么简单,要考虑到性能、压缩率、跨语言、向下兼容、大小端等。所以出现了一些序列化框架:

  • Protobuf
  • Hessian
  • Thrift
  • Avro
  • 等等

可以从以下几个角度来决定如何选择序列化框架:

  • 序列化/反序列化的性能
  • 二进制数据的体积大小,这决定了网络传输的效率
  • 向下兼容,当消息格式升级后,旧版本的客户端还需要能正常使用

# 数据库

# SQL

SQL 基于关系代数,所以只能用于 RDBMS。SQL 是一个标准,目前常用的是 SQL 92 和 SQL 99。SQL 分为 DDL、DML、DCL、TCL,水还是很深的,最常用的 select 语句并不属于这四者,而是属于 DQL。

# 索引

索引可以提高数据查询的效率,内部实现有哈希表、有序数组、搜索树、跳表。

哈希表只适用于等值查询的场景 (如 Redis、Memcached),还需要解决哈希冲突的问题。

有序数组通过二分查找对应的值,适用于范围查询的场景 O(logN),更新/插入数据效率很低 O(N)。因此更适用于不会再变化的静态数据 (如世界人口历史数据)。

搜索树又分为:二叉搜索树、N叉搜索树、红黑树、B+树、LSM。

# KV 数据库

【存什么数据】键值数据库中,数据一定是以 Key-Value 的形式存储的,Key 一定是 String 类型,而 Value 不一定。

  • Redis 的 Value 可以是 String、哈希表、列表、集合等类型
  • Memcached 的 Value 只能是 String 类型

【存在哪里】数据可以存在内存或外存中。

  • Redis、Memcached 都是放在内存里面;速度很快,但数据有丢失风险;适用于缓存等数据丢失不敏感场景
  • 放在外存里面
  • 另一种方案是购买非易失性内存 (很贵)

【如何访问】单机型数据库还是联机型数据库?

  • RocksDB 是单机型数据库,提供动态库文件给业务访问
  • Memcached 和 Redis 是联机型数据库,通过网络协议来访问

【如何根据 key 找到 value】这就涉及到索引的概念,索引的常见实现包括哈希表、B+树、跳表、字典树。

  • Redis、Memcached 采用哈希表
  • RocksDB 采用跳表

# Redis 的哈希表索引

作为 KV 数据库,Redis 使用哈希表实现 Key 到 Value 的快速索引,使用拉链法解决哈希冲突。

【rehash】拉链法中的链表过长,会导致性能/吞吐量下降。为了解决这个问题,Redis 使用了 rehash 的机制。内部有 hash1 和 hash2 两个哈希表,当 hash1 的链表过长时,为 hash2 分配两倍的空间,同时将 hash1 的数据拷贝到 hash2。通过两个哈希表轮换使用,实现了哈希表的扩容,减少冲突/减少链表长度。

【渐进式rehash】rehash 涉及到哈希表的整体复制,会导致线程阻塞/单次响应时间长。为了解决这个问题,Redis 采用渐进式 rehash 机制,将大量数据拷贝操作分摊到每次请求中。

# Redis 的单线程

我们平时说 Redis 单线程,指的是其网络 I/O 和数据读写操作是放在同一个线程里面的。通过多路复用,保证单线程下也能实现高吞吐量。

其内部是基于非阻塞式 Socket + select/epoll 实现的。

# 聚合查询

在 SQL 中,通过 group by + 聚合函数来实现。

在 MongoDB 中,通过 Aggregation Pipeline 来实现,一个 Pipeline 包含多个 stage。

# 集群

集群在数据库中有两个用途:

  • 提高可用性
  • 提升数据读写性能

MongoDB 通过副本集 (opens new window)的方式,当主节点挂了,从节点会进行选举选出一个新的主节点。增加副本集的节点数量可以提高读性能,不能提高写性能。

Redis 有哨兵 (Sentinel) 机制,主库挂了之后会选一个从库转为主库。

Redis 3.0 官方实现了一个切片集群:Redis Cluster (opens new window)。但在这之前民间也实现了一些切片集群。

# 数据备份和恢复

在 Redis 中有两种备份机制

  • RDB 是对内存做一个全量备份,恢复的时候直接恢复即可
  • AOF 是将每个命令 append 到日志文件的末尾,恢复的时候将命令一个个取出执行即可恢复

下面讨论一个重要议题:备份过程中是否会阻塞数据库访问?

# Java 生态

# Web 后端开发

# Web 容器

JBoss 和 WebLogic 不仅包含 Servlet 容器的功能,还包含 EJB 容器的功能,是完整的 Java EE 应用服务器。Tomcat 和 Jetty 只包含 Servlet 容器的功能,不包含 EJB 容器的功能,所以可以称之为轻量级容器。

在早期 Java 程序员按照 Servlet + JSP 的规范编写代码,将编译后的 .class 文件打包成压缩文件 (.war 后缀) 发给运维人员;运维人员将 .war 文件放到 Tomcat 规定的某个目录下,然后启动 Tomcat,这样程序就上线了。

而在今天人们喜欢用程序内嵌的方式启动 Servlet 容器,这样 Tomcat 这种轻量级的容器就很吃香 (占用空间小)。

# Servlet

Servlet 规范约定一套使用 HTTP 协议的流程,定义了一些接口,业务代码基于这些接口编程,Tomcat 实现这些接口。这样业务代码就无需关心复杂的 HTTP 协议了,但这套流程会让我们的业务代码就不够灵活,难以实现个性化需求,为此 Servlet 提供了两种扩展机制:Filter 过滤器、Listener 监听器。

为了加深对 Servlet 的理解,我们做一个实验不使用 IDE 编写一个 Servlet 应用 (opens new window)

# ORM

Hibernate 是一个 ORM 框架,问世时间早于 JPA 规范。JPA 规范出现后,Hibernate 被视为 JPA 规范的一种实现。

# Redis

在 Java 中通过 Jedis、Lettuce 访问 Redis。