1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-23 20:02:22 +00:00

remove redundant words

This commit is contained in:
Xargin 2019-07-11 13:23:43 +08:00
parent 8d23f5bbf5
commit 47c9d3459a
12 changed files with 25 additions and 25 deletions

View File

@ -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一节中进行详细的阐释。

View File

@ -98,7 +98,7 @@ func helloHandler(wr http.ResponseWriter, r *http.Request) {
我们来分析一下,一开始在哪里做错了呢?我们只是一步一步地满足需求,把我们需要的逻辑按照流程写下去呀?
实际上,我们犯的最大的错误是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲非业务的需求都是在http请求处理前做一些事情并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢回到刚开头的例子我们需要给我们的`helloHandler()`增加超时时间统计,我们可以使用一种叫`function adapter`的方法来对`helloHandler()`进行包装:
我们犯的最大的错误是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲非业务的需求都是在http请求处理前做一些事情并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢回到刚开头的例子我们需要给我们的`helloHandler()`增加超时时间统计,我们可以使用一种叫`function adapter`的方法来对`helloHandler()`进行包装:
```go
@ -147,7 +147,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
}
```
实际上只要你的handler函数签名是
只要你的handler函数签名是
```go
func (ResponseWriter, *Request)
@ -187,7 +187,7 @@ customizedHandler = logger(timeout(ratelimit(helloHandler)))
*图 5-8 请求处理过程*
再直白一些,这个流程在进行请求处理的时候实际上就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:
再直白一些,这个流程在进行请求处理的时候就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:
```
[exec of logger logic] 函数栈: []
@ -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`也只是针对其框架的一种封装,中间件的原理与本节中的说明是一致的。

View File

@ -6,7 +6,7 @@
*图 5-10 validator流程*
际上这是一个语言无关的场景需要进行字段校验的情况有很多Web系统的Form或JSON提交只是一个典型的例子。我们用Go来写一个类似上图的校验示例。然后研究怎么一步步对其进行改进。
这其实是一个语言无关的场景需要进行字段校验的情况有很多Web系统的Form或JSON提交只是一个典型的例子。我们用Go来写一个类似上图的校验示例。然后研究怎么一步步对其进行改进。
## 5.4.1 重构请求校验函数

View File

@ -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
@ -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)

View File

@ -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对应的结构体和我们协议无关的结构体分别长什么样子
@ -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 {

View File

@ -74,7 +74,7 @@ type OrderCreator interface {
我们只要把之前写过的步骤函数签名都提到一个接口中,就可以完成抽象了。
在进行抽象之前,我们应该想明白的一点是,引入接口对我们的系统本身是否有意义,这是要按照场景去进行分析的。假如我们的系统只服务一条产品线,并且内部的代码只是针对很具体的场景进行定制化开发,那么实际上引入接口是不会带来任何收益的。至于说是否方便测试,这一点我们会在之后的章节来讲。
在进行抽象之前,我们应该想明白的一点是,引入接口对我们的系统本身是否有意义,这是要按照场景去进行分析的。假如我们的系统只服务一条产品线,并且内部的代码只是针对很具体的场景进行定制化开发,那么引入接口是不会带来任何收益的。至于说是否方便测试,这一点我们会在之后的章节来讲。
如果我们正在做的是平台系统,需要由平台来定义统一的业务流程和业务规范,那么基于接口的抽象就是有意义的。举个例子:

View File

@ -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的业务出问题那可能对于公司来说就已经是一场不可挽回的事故了。
@ -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 分布是否均匀

View File

@ -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分配

View File

@ -137,7 +137,7 @@ func main() {
在单机系统中trylock并不是一个好选择。因为大量的goroutine抢锁可能会导致CPU无意义的资源浪费。有一个专有名词用来描述这种抢锁的场景活锁。
活锁指的是程序看起来在正常执行,但实际上CPU周期被浪费在抢锁而非执行任务上从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下不建议使用这种锁。
活锁指的是程序看起来在正常执行但CPU周期被浪费在抢锁而非执行任务上从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下不建议使用这种锁。
## 6.2.3 基于Redis的setnx
@ -226,7 +226,7 @@ current counter is 2028
unlock success!
```
通过代码和执行结果可以看到,我们远程调用`setnx`实际上和单机的trylock非常相似如果获取锁失败那么相关的任务逻辑就不应该继续向前执行。
通过代码和执行结果可以看到,我们远程调用`setnx`运行流程上和单机的trylock非常相似如果获取锁失败那么相关的任务逻辑就不应该继续向前执行。
`setnx`很适合在高并发场景下,用来争抢一些“唯一”的资源。比如交易撮合系统中卖家发起订单,而多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不管是用户设备的时间,还是分布式场景下的各台机器的时间,都是没有办法在合并后保证正确的时序的。哪怕是我们同一个机房的集群,不同的机器的系统时间可能也会有细微的差别。

View File

@ -9,7 +9,7 @@
1. 实现一套类似crontab的分布式定时任务管理系统。
2. 实现一个支持定时发送消息的消息队列。
两种思路进而衍生出了一些不同的系统但其本质是差不多的。都是需要实现一个定时器timer。在单机的场景下定时器其实并不少见例如我们在和网络库打交道的时候经常会调用`SetReadDeadline()`函数,这实际上就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。
两种思路进而衍生出了一些不同的系统但其本质是差不多的。都是需要实现一个定时器timer。在单机的场景下定时器其实并不少见例如我们在和网络库打交道的时候经常会调用`SetReadDeadline()`函数,就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。
下面我们从定时器开始,探究延时任务系统的实现。
@ -25,7 +25,7 @@
*图 6-4 二叉堆结构*
小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是`O(1)`
小顶堆的好处是什么呢?对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是`O(1)`
当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是`O(LgN)`

View File

@ -171,7 +171,7 @@ if field_1 == 1 || field_2 == 2 {
3 < i && x > 10
```
es的`Bool Query`方案,实际上就是用json来表达了这种程序语言中的Boolean Expression为什么可以这么做呢因为json本身是可以表达树形结构的我们的程序代码在被编译器parse之后也会变成AST而AST抽象语法树顾名思义就是树形结构。理论上json能够完备地表达一段程序代码被parse之后的结果。这里的Boolean Expression被编译器Parse之后也会生成差不多的树形结构而且只是整个编译器实现的一个很小的子集。
es的`Bool Query`方案就是用json来表达了这种程序语言中的Boolean Expression为什么可以这么做呢因为json本身是可以表达树形结构的我们的程序代码在被编译器parse之后也会变成AST而AST抽象语法树顾名思义就是树形结构。理论上json能够完备地表达一段程序代码被parse之后的结果。这里的Boolean Expression被编译器Parse之后也会生成差不多的树形结构而且只是整个编译器实现的一个很小的子集。
### 基于client SDK做开发
@ -266,7 +266,7 @@ func deleteDocument(
}
```
因为Lucene的性质本质上搜索引擎内的数据是不可变的所以如果要对文档进行更新实际上是按照id进行完全覆盖的操作,所以与插入的情况是一样的。
因为Lucene的性质本质上搜索引擎内的数据是不可变的所以如果要对文档进行更新Lucene内部是按照id进行完全覆盖(本质是取同一 id 最新的segment中的数据)的操作,所以与插入的情况是一样的。
使用es作为数据库使用时需要注意因为es有索引合并的操作所以数据插入到es中到可以查询的到需要一段时间由es的refresh_interval决定。所以千万不要把es当成强一致的关系型数据库来使用。
@ -369,7 +369,7 @@ SQL的where部分就是boolean expression。我们之前提到过这种bool
*图 6-13 基于时间戳的数据同步*
这种同步方式与业务强绑定例如WMS系统中的出库单我们并不需要非常实时稍微有延迟也可以接受那么我们可以每分钟从MySQL的出库单表中把最近十分钟创建的所有出库单取出批量存入es中具体的逻辑实际上就是一条SQL
这种同步方式与业务强绑定例如WMS系统中的出库单我们并不需要非常实时稍微有延迟也可以接受那么我们可以每分钟从MySQL的出库单表中把最近十分钟创建的所有出库单取出批量存入es中取数据的操作需要执行的逻辑可以表达为下面的SQL
```sql
select * from wms_orders where update_time >= date_sub(now(), interval 10 minute);
@ -393,6 +393,6 @@ select * from wms_orders where update_time >= date_sub(
业界使用较多的是阿里开源的Canal来进行binlog解析与同步。canal会伪装成MySQL的从库然后解析好行格式的binlog再以更容易解析的格式例如json发送到消息队列。
由下游的Kafka消费者负责把上游数据表的自增主键作为es的文档的id进行写入这样可以保证每次接收到binlog时对应id的数据都被覆盖更新为最新。MySQL的Row格式的binlog会将每条记录的所有字段都提供给下游所以实际上在向异构数据目标同步数据时不需要考虑数据是插入还是更新只要一律按id进行覆盖即可。
由下游的Kafka消费者负责把上游数据表的自增主键作为es的文档的id进行写入这样可以保证每次接收到binlog时对应id的数据都被覆盖更新为最新。MySQL的Row格式的binlog会将每条记录的所有字段都提供给下游所以在向异构数据目标同步数据时不需要考虑数据是插入还是更新只要一律按id进行覆盖即可。
这种模式同样需要业务遵守一条数据表规范即表中必须有唯一主键id来保证我们进入es的数据不会发生重复。一旦不遵守该规范那么就会在同步时导致数据重复。当然你也可以为每一张需要的表去定制消费者的逻辑这就不是通用系统讨论的范畴了。

View File

@ -4,7 +4,7 @@
## 6.5.1 常见的负载均衡思路
如果我们不考虑均衡的话现在有n个服务节点我们完成业务流程实际上只需要从这n个中挑出其中的一个。有几种思路:
如果我们不考虑均衡的话现在有n个服务节点我们完成业务流程只需要从这n个中挑出其中的一个。有几种思路:
1. 按顺序挑: 例如上次选了第一台,那么这次就选第二台,下次第三台,如果已经到了最后一台,那么下一次从第一台开始。这种情况下我们可以把服务节点信息都存储在数组中,每次请求完成下游之后,将一个索引后移即可。在移到尽头时再移回数组开头处。
@ -69,7 +69,7 @@ func request(params map[string]interface{}) error {
### 6.5.2.1 错误的洗牌导致的负载不均衡
真的没有问题么?实际上还是有问题的。这段简短的程序里有两个隐藏的隐患:
真的没有问题么?还是有问题的。这段简短的程序里有两个隐藏的隐患:
1. 没有随机种子。在没有随机种子的情况下,`rand.Intn()`返回的伪随机数序列是固定的。
@ -93,7 +93,7 @@ func shuffle(indexes []int) {
}
```
在Go的标准库中实际上已经为我们内置了该算法:
在Go的标准库中已经为我们内置了该算法:
```go
func shuffle(n int) []int {