Skip to content

Commit

Permalink
remove redundant words
Browse files Browse the repository at this point in the history
  • Loading branch information
cch123 committed Jul 11, 2019
1 parent 8d23f5b commit 47c9d34
Show file tree
Hide file tree
Showing 12 changed files with 25 additions and 25 deletions.
2 changes: 1 addition & 1 deletion ch5-web/ch5-01-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request
}
```

因为默认的`net/http`包中的`mux`不支持带参数的路由,所以Burrow这个项目使用了非常蹩脚的字符串`Split`和乱七八糟的 `switch case`来达到自己的目的,但实际上却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个`handler`函数逻辑上较简单,最复杂的也就是这个`handleKafka()`但实际上我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。
因为默认的`net/http`包中的`mux`不支持带参数的路由,所以Burrow这个项目使用了非常蹩脚的字符串`Split`和乱七八糟的 `switch case`来达到自己的目的,但却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个`handler`函数逻辑上较简单,最复杂的也就是这个`handleKafka()`而我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。

根据我们的经验,简单地来说,只要你的路由带有参数,并且这个项目的API数目超过了10,就尽量不要使用`net/http`中默认的路由。在Go开源界应用最广泛的router是httpRouter,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。关于httpRouter路由的原理,会在本章节的router一节中进行详细的阐释。

Expand Down
8 changes: 4 additions & 4 deletions ch5-web/ch5-03-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func helloHandler(wr http.ResponseWriter, r *http.Request) {

我们来分析一下,一开始在哪里做错了呢?我们只是一步一步地满足需求,把我们需要的逻辑按照流程写下去呀?

实际上,我们犯的最大的错误是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲,非业务的需求都是在http请求处理前做一些事情,并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢?回到刚开头的例子,我们需要给我们的`helloHandler()`增加超时时间统计,我们可以使用一种叫`function adapter`的方法来对`helloHandler()`进行包装:
我们犯的最大的错误,是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲,非业务的需求都是在http请求处理前做一些事情,并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢?回到刚开头的例子,我们需要给我们的`helloHandler()`增加超时时间统计,我们可以使用一种叫`function adapter`的方法来对`helloHandler()`进行包装:

```go

Expand Down Expand Up @@ -147,7 +147,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
}

```
实际上只要你的handler函数签名是
只要你的handler函数签名是

```go
func (ResponseWriter, *Request)
Expand Down Expand Up @@ -187,7 +187,7 @@ customizedHandler = logger(timeout(ratelimit(helloHandler)))

*图 5-8 请求处理过程*

再直白一些,这个流程在进行请求处理的时候实际上就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:
再直白一些,这个流程在进行请求处理的时候就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:

```
[exec of logger logic] 函数栈: []
Expand Down Expand Up @@ -286,5 +286,5 @@ throttler.go

*图 5-9 gin的中间件仓库*

如果读者去阅读gin的源码的话,可能会发现gin的中间件中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的`handler`也只是针对其框架的一种封装,中间件的原理与本节中的说明是一致的。
如果读者去阅读gin的源码的话,可能会发现gin的中间件中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过gin的`handler`也只是针对其框架的一种封装,中间件的原理与本节中的说明是一致的。

2 changes: 1 addition & 1 deletion ch5-web/ch5-04-validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

*图 5-10 validator流程*

实际上这是一个语言无关的场景,需要进行字段校验的情况有很多,Web系统的Form或JSON提交只是一个典型的例子。我们用Go来写一个类似上图的校验示例。然后研究怎么一步步对其进行改进。
这其实是一个语言无关的场景,需要进行字段校验的情况有很多,Web系统的Form或JSON提交只是一个典型的例子。我们用Go来写一个类似上图的校验示例。然后研究怎么一步步对其进行改进。

## 5.4.1 重构请求校验函数

Expand Down
4 changes: 2 additions & 2 deletions ch5-web/ch5-06-ratelimit.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func main() {
}
```

我们需要衡量一下这个Web服务的吞吐量,再具体一些,实际上就是接口的QPS。借助wrk,在家用电脑 Macbook Pro上对这个 `hello world` 服务进行基准测试,Mac的硬件情况如下:
我们需要衡量一下这个Web服务的吞吐量,再具体一些,就是接口的QPS。借助wrk,在家用电脑 Macbook Pro上对这个 `hello world` 服务进行基准测试,Mac的硬件情况如下:

```shell
CPU: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
Expand Down Expand Up @@ -148,7 +148,7 @@ func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {}

## 5.6.2 原理

从功能上来看,令牌桶模型实际上就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,有小小的思想负担。如果我们对Go语言已经比较熟悉的话,很容易想到可以用buffered channel来完成简单的加令牌取令牌操作:
从功能上来看,令牌桶模型就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,有小小的思想负担。如果我们对Go语言已经比较熟悉的话,很容易想到可以用buffered channel来完成简单的加令牌取令牌操作:

```go
var tokenBucket = make(chan struct{}, capacity)
Expand Down
4 changes: 2 additions & 2 deletions ch5-web/ch5-07-layout-of-web-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func HTTPCreateOrderHandler(wr http.ResponseWriter, r *http.Request) {

理论上我们可以用同一个请求结构体组合上不同的tag,来达到一个结构体来给不同的协议复用的目的。不过遗憾的是在thrift中,请求结构体也是通过IDL生成的,其内容在自动生成的ttypes.go文件中,我们还是需要在thrift的入口将这个自动生成的结构体映射到我们logic入口所需要的结构体上。gRPC也是类似。这部分代码还是需要的。

聪明的读者可能已经可以看出来了,协议细节处理这一层实际上有大量重复劳动,每一个接口在协议这一层的处理,无非是把数据从协议特定的结构体(例如`http.Request`,thrift的被包装过了) 读出来,再绑定到我们协议无关的结构体上,再把这个结构体映射到Controller入口的结构体上,这些代码实际上长得都差不多。差不多的代码都遵循着某种模式,那么我们可以对这些模式进行简单的抽象,用代码生成的方式,把繁复的协议处理代码从工作内容中抽离出去。
聪明的读者可能已经可以看出来了,协议细节处理这一层有大量重复劳动,每一个接口在协议这一层的处理,无非是把数据从协议特定的结构体(例如`http.Request`,thrift的被包装过了) 读出来,再绑定到我们协议无关的结构体上,再把这个结构体映射到Controller入口的结构体上,这些代码长得都差不多。差不多的代码都遵循着某种模式,那么我们可以对这些模式进行简单的抽象,用代码生成的方式,把繁复的协议处理代码从工作内容中抽离出去。

先来看看HTTP对应的结构体、thrift对应的结构体和我们协议无关的结构体分别长什么样子:

Expand Down Expand Up @@ -106,7 +106,7 @@ type CreateOrderParams struct {

```

我们需要通过一个源结构体来生成我们需要的HTTP和thrift入口代码。再观察一下上面定义的三种结构体,实际上我们只要能用一个结构体生成thrift的IDL,以及HTTP服务的“IDL(实际上就是带json或form相关tag的结构体定义)” 就可以了。这个初始的结构体我们可以把结构体上的HTTP的tag和thrift的tag揉在一起:
我们需要通过一个源结构体来生成我们需要的HTTP和thrift入口代码。再观察一下上面定义的三种结构体,我们只要能用一个结构体生成thrift的IDL,以及HTTP服务的“IDL(只要能包含json或form相关tag的结构体定义信息)” 就可以了。这个初始的结构体我们可以把结构体上的HTTP的tag和thrift的tag揉在一起:

```go
type FeatureSetParams struct {
Expand Down
2 changes: 1 addition & 1 deletion ch5-web/ch5-08-interface-and-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type OrderCreator interface {

我们只要把之前写过的步骤函数签名都提到一个接口中,就可以完成抽象了。

在进行抽象之前,我们应该想明白的一点是,引入接口对我们的系统本身是否有意义,这是要按照场景去进行分析的。假如我们的系统只服务一条产品线,并且内部的代码只是针对很具体的场景进行定制化开发,那么实际上引入接口是不会带来任何收益的。至于说是否方便测试,这一点我们会在之后的章节来讲。
在进行抽象之前,我们应该想明白的一点是,引入接口对我们的系统本身是否有意义,这是要按照场景去进行分析的。假如我们的系统只服务一条产品线,并且内部的代码只是针对很具体的场景进行定制化开发,那么引入接口是不会带来任何收益的。至于说是否方便测试,这一点我们会在之后的章节来讲。

如果我们正在做的是平台系统,需要由平台来定义统一的业务流程和业务规范,那么基于接口的抽象就是有意义的。举个例子:

Expand Down
4 changes: 2 additions & 2 deletions ch5-web/ch5-09-gated-launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

*图 5-20 分组部署*

为什么要用2倍?这样能够保证我们不管有多少台机器,都不会把组划分得太多。例如1024台机器,实际上也就只需要1-2-4-8-16-32-64-128-256-512部署十次就可以全部部署完毕。
为什么要用2倍?这样能够保证我们不管有多少台机器,都不会把组划分得太多。例如1024台机器,也就只需要1-2-4-8-16-32-64-128-256-512部署十次就可以全部部署完毕。

这样我们上线最开始影响到的用户在整体用户中占的比例也不大,比如1000台机器的服务,我们上线后如果出现问题,也只影响1/1000的用户。如果10组完全平均分,那一上线立刻就会影响1/10的用户,1/10的业务出问题,那可能对于公司来说就已经是一场不可挽回的事故了。

Expand Down Expand Up @@ -249,7 +249,7 @@ PASS
ok _/Users/caochunhui/test/go/hash_bench 7.050s
```

可见murmurhash相比其它的算法有三倍以上的性能提升。显然做负载均衡的话,用murmurhash要比md5和sha1都要好,实际上这些年社区里还有另外一些更高效的哈希算法出现,感兴趣的读者可以自行调研。
可见murmurhash相比其它的算法有三倍以上的性能提升。显然做负载均衡的话,用murmurhash要比md5和sha1都要好,这些年社区里还有另外一些更高效的哈希算法涌现,感兴趣的读者可以自行调研。

### 5.9.3.3 分布是否均匀

Expand Down
2 changes: 1 addition & 1 deletion ch6-cloud/ch6-01-dist-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Twitter的snowflake算法是这种场景下的一个典型解法。先来看看s

数据中心加上实例id共有10位,可以支持我们每数据中心部署32台机器,所有数据中心共1024台实例。

表示`timestamp`的41位,可以支持我们使用69年。当然,我们的时间毫秒计数不会真的从1970年开始记,那样我们的系统跑到`2039/9/7 23:47:35`就不能用了,所以这里的`timestamp`实际上只是相对于某个时间的增量,比如我们的系统上线是2018-08-01,那么我们可以把这个timestamp当作是从`2018-08-01 00:00:00.000`的偏移量。
表示`timestamp`的41位,可以支持我们使用69年。当然,我们的时间毫秒计数不会真的从1970年开始记,那样我们的系统跑到`2039/9/7 23:47:35`就不能用了,所以这里的`timestamp`只是相对于某个时间的增量,比如我们的系统上线是2018-08-01,那么我们可以把这个timestamp当作是从`2018-08-01 00:00:00.000`的偏移量。

## 6.1.1 worker_id分配

Expand Down
4 changes: 2 additions & 2 deletions ch6-cloud/ch6-02-lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func main() {

在单机系统中,trylock并不是一个好选择。因为大量的goroutine抢锁可能会导致CPU无意义的资源浪费。有一个专有名词用来描述这种抢锁的场景:活锁。

活锁指的是程序看起来在正常执行,但实际上CPU周期被浪费在抢锁,而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。
活锁指的是程序看起来在正常执行,但CPU周期被浪费在抢锁,而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。

## 6.2.3 基于Redis的setnx

Expand Down Expand Up @@ -226,7 +226,7 @@ current counter is 2028
unlock success!
```

通过代码和执行结果可以看到,我们远程调用`setnx`实际上和单机的trylock非常相似,如果获取锁失败,那么相关的任务逻辑就不应该继续向前执行。
通过代码和执行结果可以看到,我们远程调用`setnx`运行流程上和单机的trylock非常相似,如果获取锁失败,那么相关的任务逻辑就不应该继续向前执行。

`setnx`很适合在高并发场景下,用来争抢一些“唯一”的资源。比如交易撮合系统中卖家发起订单,而多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不管是用户设备的时间,还是分布式场景下的各台机器的时间,都是没有办法在合并后保证正确的时序的。哪怕是我们同一个机房的集群,不同的机器的系统时间可能也会有细微的差别。

Expand Down
4 changes: 2 additions & 2 deletions ch6-cloud/ch6-03-delay-job.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
1. 实现一套类似crontab的分布式定时任务管理系统。
2. 实现一个支持定时发送消息的消息队列。

两种思路进而衍生出了一些不同的系统,但其本质是差不多的。都是需要实现一个定时器(timer)。在单机的场景下定时器其实并不少见,例如我们在和网络库打交道的时候经常会调用`SetReadDeadline()`函数,这实际上就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。
两种思路进而衍生出了一些不同的系统,但其本质是差不多的。都是需要实现一个定时器(timer)。在单机的场景下定时器其实并不少见,例如我们在和网络库打交道的时候经常会调用`SetReadDeadline()`函数,就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。

下面我们从定时器开始,探究延时任务系统的实现。

Expand All @@ -25,7 +25,7 @@

*图 6-4 二叉堆结构*

小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是`O(1)`
小顶堆的好处是什么呢?对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是`O(1)`

当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是`O(LgN)`

Expand Down
Loading

0 comments on commit 47c9d34

Please sign in to comment.