浏览代码

feat: init

double.huang 4 年之前
当前提交
737d3ebf05
共有 100 个文件被更改,包括 4808 次插入0 次删除
  1. 4 0
      .gitignore
  2. 21 0
      LICENSE
  3. 231 0
      api/fx.md
  4. 140 0
      doc/breaker-algorithms.md
  5. 5 0
      doc/breaker.md
  6. 111 0
      doc/collection.md
  7. 338 0
      doc/fx.md
  8. 314 0
      doc/goctl-model-sql.md
  9. 265 0
      doc/goctl-rpc.md
  10. 244 0
      doc/goctl.md
  11. 二进制
      doc/images/api-gen.png
  12. 二进制
      doc/images/architecture-en.png
  13. 二进制
      doc/images/architecture.png
  14. 二进制
      doc/images/balancer.png
  15. 二进制
      doc/images/benchmark.png
  16. 二进制
      doc/images/bookstore-api.png
  17. 二进制
      doc/images/bookstore-arch.png
  18. 二进制
      doc/images/bookstore-benchmark.png
  19. 二进制
      doc/images/bookstore-model.png
  20. 二进制
      doc/images/bookstore-rpc.png
  21. 二进制
      doc/images/breaker_state.png
  22. 二进制
      doc/images/call_chain.png
  23. 二进制
      doc/images/client_rejection2.png
  24. 二进制
      doc/images/concurrent_denpendency.png
  25. 二进制
      doc/images/datasource.png
  26. 二进制
      doc/images/fx_log.png
  27. 二进制
      doc/images/fx_middle.png
  28. 二进制
      doc/images/fx_reverse.png
  29. 二进制
      doc/images/fx_step_result.png
  30. 二进制
      doc/images/go-zero.png
  31. 二进制
      doc/images/interceptor.png
  32. 二进制
      doc/images/model-gen.png
  33. 二进制
      doc/images/mr.png
  34. 二进制
      doc/images/mr_time.png
  35. 二进制
      doc/images/panel.png
  36. 二进制
      doc/images/prom_up.png
  37. 二进制
      doc/images/prometheus.png
  38. 二进制
      doc/images/qps.png
  39. 二进制
      doc/images/qps_panel.png
  40. 二进制
      doc/images/qq.jpg
  41. 二进制
      doc/images/random_pseudo.png
  42. 二进制
      doc/images/resilience-en.png
  43. 二进制
      doc/images/resilience.jpg
  44. 二进制
      doc/images/resolver.png
  45. 二进制
      doc/images/rpc-gen.png
  46. 二进制
      doc/images/shedding_flying.jpg
  47. 二进制
      doc/images/shorturl-api.png
  48. 二进制
      doc/images/shorturl-arch.png
  49. 二进制
      doc/images/shorturl-benchmark.png
  50. 二进制
      doc/images/shorturl-model.png
  51. 二进制
      doc/images/shorturl-rpc.png
  52. 二进制
      doc/images/timewheel-run.png
  53. 二进制
      doc/images/timewheel-struct.png
  54. 二进制
      doc/images/trie.png
  55. 二进制
      doc/images/variables.png
  56. 二进制
      doc/images/wechat.jpg
  57. 二进制
      doc/images/zrpc.png
  58. 136 0
      doc/jwt.md
  59. 86 0
      doc/keywords.md
  60. 51 0
      doc/loadshedding.md
  61. 123 0
      doc/mapping.md
  62. 190 0
      doc/mapreduce.md
  63. 113 0
      doc/metric.md
  64. 15 0
      doc/periodicalexecutor.md
  65. 167 0
      doc/sharedcalls.md
  66. 543 0
      doc/shorturl-en.md
  67. 588 0
      doc/shorturl.md
  68. 23 0
      doc/sql-cache.md
  69. 289 0
      doc/timingWheel.md
  70. 596 0
      doc/zrpc.md
  71. 191 0
      docs/.vuepress/config.js
  72. 二进制
      docs/.vuepress/public/logo.png
  73. 24 0
      docs/README.md
  74. 二进制
      docs/images/api-gen.png
  75. 二进制
      docs/images/architecture-en.png
  76. 二进制
      docs/images/architecture.png
  77. 二进制
      docs/images/balancer.png
  78. 二进制
      docs/images/benchmark.png
  79. 二进制
      docs/images/bookstore-api.png
  80. 二进制
      docs/images/bookstore-arch.png
  81. 二进制
      docs/images/bookstore-benchmark.png
  82. 二进制
      docs/images/bookstore-model.png
  83. 二进制
      docs/images/bookstore-rpc.png
  84. 二进制
      docs/images/concurrent_denpendency.png
  85. 二进制
      docs/images/datasource.png
  86. 二进制
      docs/images/fx_log.png
  87. 二进制
      docs/images/fx_middle.png
  88. 二进制
      docs/images/fx_reverse.png
  89. 二进制
      docs/images/fx_step_result.png
  90. 二进制
      docs/images/go-zero.png
  91. 二进制
      docs/images/interceptor.png
  92. 二进制
      docs/images/model-gen.png
  93. 二进制
      docs/images/mr.png
  94. 二进制
      docs/images/mr_time.png
  95. 二进制
      docs/images/panel.png
  96. 二进制
      docs/images/prom_up.png
  97. 二进制
      docs/images/prometheus.png
  98. 二进制
      docs/images/qps.png
  99. 二进制
      docs/images/qps_panel.png
  100. 二进制
      docs/images/qq.jpg

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+**/.DS_Store
+.idea
+node_modules
+docs/.vuepress/dist

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 好未来技术
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 231 - 0
api/fx.md

@@ -0,0 +1,231 @@
+## Functions
+
+### type Stream
+
+```go
+Stream struct {
+	source <-chan interface{}
+}
+```
+
+`Stream` Returns a readable stream that can be read in and processed, then flowing to the next stream.
+
+### func From
+
+```go
+type GenerateFunc func(source chan<- interface{})
+
+func From(generate GenerateFunc) Stream
+```
+
+Create a stream that the following functions can read in. As the beginning of the flow.
+
+### func Just
+
+```go
+func Just(items ...interface{}) Stream
+```
+
+Converts the given arbitrary items to a Stream.
+
+### func Range
+
+```go
+func Range(source <-chan interface{}) Stream
+```
+
+Converts the given channel to a Stream.
+
+### type Option
+
+```go
+rxOptions struct {
+  // whether to limit the parallel number 
+  // [true: defaultWorkers(16); false: option.workers]
+  unlimitedWorkers bool
+  // count of parallel workers
+  workers          int
+}
+
+type Option func(opts *rxOptions)
+```
+
+### func WithWorkers
+
+```go
+func WithWorkers(workers int) Option
+```
+
+Lets the caller to customize the concurrent workers. Will sets the minimum number of parallelism: `minWorkers(1)`
+
+### func (Stream) Buffer
+
+```go
+func (p Stream) Buffer(n int) Stream
+```
+
+Buffers the items into a queue with size n.It can balance the producer and the consumer if their processing throughput don't match.
+
+### func (Stream) Count
+
+```go
+func (p Stream) Count() (int)
+```
+
+Counts the number of elements in the result.
+
+### func (Stream) Distinct
+
+```go
+type KeyFunc    func(item interface{}) interface{}
+
+func (p Stream) Distinct(fn KeyFunc) Stream
+```
+
+Removes the duplicated items base on the given `KeyFunc`.
+
+### func (Stream) Done
+
+```go
+func (p Stream) Done()
+```
+
+Waits all upstreaming operations to be done.
+
+### func (Stream) Filter
+
+```go
+type FilterFunc func(item interface{}) bool
+
+func (p Stream) Filter(fn FilterFunc, opts ...Option) Stream
+```
+
+Filters the items by the given `FilterFunc`.
+
+### func (Stream) ForAll
+
+```go
+type ForAllFunc func(pipe <-chan interface{})
+
+func (Stream) ForAll(fn ForAllFunc)
+```
+
+Handles the streaming elements from the source and no later streams.
+
+### func (Stream) ForEach
+
+```go
+type ForEachFunc  func(item interface{})
+
+func (p Stream) ForEach(fn ForEachFunc)
+```
+
+Seals the Stream with the ForEachFunc on each item, no successive operations.
+
+### func (Stream) Group
+
+```go
+type KeyFunc  func(item interface{}) interface{}
+
+func (p Stream) Group(fn KeyFunc) Stream
+```
+
+Groups the elements into different groups based on their keys.
+
+### func (Stream) Head
+
+```go
+func (p Stream) Head(int64) Stream
+```
+
+The first few elements in the stream are taken out in order, and return a new `Stream`.
+
+### func (Stream) Map
+
+```go
+type MapFunc func(item interface{}) interface{}
+
+func (p Stream) Map(fn MapFunc, opts ...Option) Stream
+```
+
+Converts each item to another corresponding item, which means it's a 1:1 model.
+
+### func (Stream) Merge
+
+```go
+func (p Stream) Merge() Stream
+```
+
+Merges all the items into a slice and generates a new stream.
+
+### func (Stream) Parallel
+
+```go
+type ParallelFunc func(item interface{})
+
+func (p Stream) Parallel(fn ParallelFunc, opts ...Option)
+```
+
+Applies the given `ParallelFunc` to each item concurrently with given number of workers.Finally, execute `Done()`
+
+### func (Stream) Reduce
+
+```go
+type ReduceFunc func(pipe <-chan interface{}) (interface{}, error)
+
+func (p Stream) Reduce(fn ReduceFunc) (interface{}, error)
+```
+
+`Reduce` is a utility method to let the caller deal with the underlying channel.
+
+### func (Stream) Reduce
+
+```go
+func (p Stream) Reverse() Stream
+```
+
+Reverse reverses the elements in the stream.
+
+### func (Stream) Reverse
+
+```go
+func (p Stream) Reverse() Stream
+```
+
+Reverses the elements in the stream.
+
+### func (Stream) Sort
+
+```go
+type 	LessFunc func(a, b interface{}) bool
+
+func (p Stream) Sort(less LessFunc) Stream
+```
+
+Sorts the items from the underlying source.
+
+### func (Stream) Split
+
+```go
+func (p Stream) Split(int) Stream
+```
+
+Split splits the elements into chunk with size up to n, might be less than n on tailing elements.
+
+### func (Stream) Split
+
+```go
+func (p Stream) Tail(n int64) Stream
+```
+
+Outputs the last N elements of the stream in reverse order to the next stream
+
+### func (Stream) Walk
+
+```go
+type WalkFunc  func(item interface{}, pipe chan<- interface{})
+
+func (p Stream) Walk(fn WalkFunc, opts ...Option) Stream
+```
+
+Lets the callers handle each item, the caller may write zero, one or more items base on the given item.

+ 140 - 0
doc/breaker-algorithms.md

@@ -0,0 +1,140 @@
+# 熔断原理与实现
+
+在微服务中服务间依赖非常常见,比如评论服务依赖审核服务而审核服务又依赖反垃圾服务,当评论服务调用审核服务时,审核服务又调用反垃圾服务,而这时反垃圾服务超时了,由于审核服务依赖反垃圾服务,反垃圾服务超时导致审核服务逻辑一直等待,而这个时候评论服务又在一直调用审核服务,审核服务就有可能因为堆积了大量请求而导致服务宕机
+
+<img src="./images/call_chain.png" alt="call_chain" style="zoom:60%;" />
+
+由此可见,在整个调用链中,中间的某一个环节出现异常就会引起上游调用服务出现一些列的问题,甚至导致整个调用链的服务都宕机,这是非常可怕的。因此一个服务作为调用方调用另一个服务时,为了防止被调用服务出现问题进而导致调用服务出现问题,所以调用服务需要进行自我保护,而保护的常用手段就是***熔断***
+
+### 熔断器原理
+
+熔断机制其实是参考了我们日常生活中的保险丝的保护机制,当电路超负荷运行时,保险丝会自动的断开,从而保证电路中的电器不受损害。而服务治理中的熔断机制,指的是在发起服务调用的时候,如果被调用方返回的错误率超过一定的阈值,那么后续的请求将不会真正发起请求,而是在调用方直接返回错误
+
+在这种模式下,服务调用方为每一个调用服务(调用路径)维护一个状态机,在这个状态机中有三个状态:
+
+- 关闭(Closed):在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在某个时间窗口内,失败的失败率达到预设的阈值,则切换到断开状态,此时开启一个超时时间,当到达该时间则切换到半关闭状态,该超时时间是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态。在关闭状态下,调用错误是基于时间的,在特定的时间间隔内会重置,这能够防止偶然错误导致熔断器进去断开状态
+- 打开(Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
+- 半打开(Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到关闭状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮
+
+<img src="./images/breaker_state.png" alt="breaker_state" style="zoom:50%;" />
+
+服务治理中引入熔断机制,使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响,可以快速拒绝可能导致错误的服务调用,而不需要等待真正的错误返回
+
+### 熔断器引入
+
+上面介绍了熔断器的原理,在了解完原理后,你是否有思考我们如何引入熔断器呢?一种方案是在业务逻辑中可以加入熔断器,但显然是不够优雅也不够通用的,因此我们需要把熔断器集成在框架内,在[zRPC](https://github.com/tal-tech/go-zero/tree/master/zrpc)框架内就内置了熔断器
+
+我们知道,熔断器主要是用来保护调用端,调用端在发起请求的时候需要先经过熔断器,而客户端拦截器正好兼具了这个这个功能,所以在zRPC框架内熔断器是实现在客户端拦截器内,拦截器的原理如下图:
+
+<img src="./images/interceptor.png" alt="interceptor" style="zoom:50%;" />
+
+对应的代码为:
+
+```go
+func BreakerInterceptor(ctx context.Context, method string, req, reply interface{},
+	cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+  // 基于请求方法进行熔断
+	breakerName := path.Join(cc.Target(), method)
+	return breaker.DoWithAcceptable(breakerName, func() error {
+    // 真正发起调用
+		return invoker(ctx, method, req, reply, cc, opts...)
+    // codes.Acceptable判断哪种错误需要加入熔断错误计数
+	}, codes.Acceptable)
+}
+```
+
+### 熔断器实现
+
+zRPC中熔断器的实现参考了[Google Sre过载保护算法](https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101),该算法的原理如下:
+
+- 请求数量(requests):调用方发起请求的数量总和
+- 请求接受数量(accepts):被调用方正常处理的请求数量
+
+在正常情况下,这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests),这个时候调用方可以继续发送请求,直到requests = K * accepts,一旦超过这个限制,熔断器就回打开,新的请求会在本地以一定的概率被抛弃直接返回错误,概率的计算公式如下:
+
+<img src="./images/client_rejection2.png" alt="client_rejection2" style="zoom:30%;" />
+
+通过修改算法中的K(倍值),可以调节熔断器的敏感度,当降低该倍值会使自适应熔断算法更敏感,当增加该倍值会使得自适应熔断算法降低敏感度,举例来说,假设将调用方的请求上限从 requests = 2 * acceptst 调整为 requests = 1.1 * accepts 那么就意味着调用方每十个请求之中就有一个请求会触发熔断
+
+代码路径为go-zero/core/breaker
+
+```go
+type googleBreaker struct {
+	k     float64  // 倍值 默认1.5
+	stat  *collection.RollingWindow // 滑动时间窗口,用来对请求失败和成功计数
+	proba *mathx.Proba // 动态概率
+}
+```
+
+自适应熔断算法实现
+
+```go
+func (b *googleBreaker) accept() error {
+	accepts, total := b.history()  // 请求接受数量和请求总量
+	weightedAccepts := b.k * float64(accepts)
+  // 计算丢弃请求概率
+	dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
+	if dropRatio <= 0 {
+		return nil
+	}
+	// 动态判断是否触发熔断
+	if b.proba.TrueOnProba(dropRatio) {
+		return ErrServiceUnavailable
+	}
+
+	return nil
+}
+```
+
+每次发起请求会调用doReq方法,在这个方法中首先通过accept效验是否触发熔断,acceptable用来判断哪些error会计入失败计数,定义如下:
+
+```go
+func Acceptable(err error) bool {
+	switch status.Code(err) {
+	case codes.DeadlineExceeded, codes.Internal, codes.Unavailable, codes.DataLoss: // 异常请求错误
+		return false
+	default:
+		return true
+	}
+}
+```
+
+如果请求正常则通过markSuccess把请求数量和请求接受数量都加一,如果请求不正常则只有请求数量会加一
+
+```go
+func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
+	// 判断是否触发熔断
+  if err := b.accept(); err != nil {
+		if fallback != nil {
+			return fallback(err)
+		} else {
+			return err
+		}
+	}
+
+	defer func() {
+		if e := recover(); e != nil {
+			b.markFailure()
+			panic(e)
+		}
+	}()
+	
+  // 执行真正的调用
+	err := req()
+  // 正常请求计数
+	if acceptable(err) {
+		b.markSuccess()
+	} else {
+    // 异常请求计数
+		b.markFailure()
+	}
+
+	return err
+}
+```
+
+### 总结
+
+调用端可以通过熔断机制进行自我保护,防止调用下游服务出现异常,或者耗时过长影响调用端的业务逻辑,很多功能完整的微服务框架都会内置熔断器。其实,不仅微服务调用之间需要熔断器,在调用依赖资源的时候,比如mysql、redis等也可以引入熔断器的机制。
+
+

+ 5 - 0
doc/breaker.md

@@ -0,0 +1,5 @@
+# 熔断机制设计
+
+## 设计目的
+
+* 依赖的服务出现大规模故障时,调用方应该尽可能少调用,降低故障服务的压力,使之尽快恢复服务

+ 111 - 0
doc/collection.md

@@ -0,0 +1,111 @@
+# 通过 collection.Cache 进行缓存
+
+go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等,本系列文章将分别介绍go-zero框架中工具的使用及其实现原理  
+
+## 进程内缓存工具[collection.Cache](https://github.com/tal-tech/go-zero/tree/master/core/collection/cache.go)
+
+在做服务器开发的时候,相信都会遇到使用缓存的情况,go-zero 提供的简单的缓存封装 **collection.Cache**,简单使用方式如下
+
+```go
+// 初始化 cache,其中 WithLimit 可以指定最大缓存的数量
+c, err := collection.NewCache(time.Minute, collection.WithLimit(10000))
+if err != nil {
+  panic(err)
+}
+
+// 设置缓存
+c.Set("key", user)
+
+// 获取缓存,ok:是否存在
+v, ok := c.Get("key")
+
+// 删除缓存
+c.Del("key")
+
+// 获取缓存,如果 key 不存在的,则会调用 func 去生成缓存
+v, err := c.Take("key", func() (interface{}, error) {
+  return user, nil
+})
+```
+
+cache 实现的建的功能包括
+
+* 缓存自动失效,可以指定过期时间
+* 缓存大小限制,可以指定缓存个数
+* 缓存增删改
+* 缓存命中率统计
+* 并发安全
+* 缓存击穿
+
+实现原理:
+Cache 自动失效,是采用 TimingWheel(https://github.com/tal-tech/go-zero/blob/master/core/collection/timingwheel.go) 进行管理的
+
+``` go
+timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) {
+		key, ok := k.(string)
+		if !ok {
+			return
+		}
+
+		cache.Del(key)
+})
+```
+
+Cache 大小限制,是采用 LRU 淘汰策略,在新增缓存的时候会去检查是否已经超出过限制,具体代码在 keyLru 中实现
+
+``` go
+func (klru *keyLru) add(key string) {
+	if elem, ok := klru.elements[key]; ok {
+		klru.evicts.MoveToFront(elem)
+		return
+	}
+
+	// Add new item
+	elem := klru.evicts.PushFront(key)
+	klru.elements[key] = elem
+
+	// Verify size not exceeded
+	if klru.evicts.Len() > klru.limit {
+		klru.removeOldest()
+	}
+}
+```
+
+Cache 的命中率统计,是在代码中实现 cacheStat,在缓存命中丢失的时候自动统计,并且会定时打印使用的命中率, qps 等状态.
+
+打印的具体效果如下
+
+```go
+cache(proc) - qpm: 2, hit_ratio: 50.0%, elements: 0, hit: 1, miss: 1
+```
+
+缓存击穿包含是使用 syncx.SharedCalls(https://github.com/tal-tech/go-zero/blob/master/core/syncx/sharedcalls.go) 进行实现的,就是将同时请求同一个 key 的请求, 关于 sharedcalls 后续会继续补充。 相关具体实现是在:
+
+```go
+func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
+	val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {
+		v, e := fetch()
+		if e != nil {
+			return nil, e
+		}
+
+		c.Set(key, v)
+		return v, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	if fresh {
+		c.stats.IncrementMiss()
+		return val, nil
+	} else {
+		// got the result from previous ongoing query
+		c.stats.IncrementHit()
+	}
+
+	return val, nil
+}
+```
+
+本文主要介绍了go-zero框架中的 Cache 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。

+ 338 - 0
doc/fx.md

@@ -0,0 +1,338 @@
+# 数据的流处理利器
+
+流处理(Stream processing)是一种计算机编程范式,其允许给定一个数据序列(流处理数据源),一系列数据操作(函数)被应用到流中的每个元素。同时流处理工具可以显著提高程序员的开发效率,允许他们编写有效、干净和简洁的代码。
+
+流数据处理在我们的日常工作中非常常见,举个例子,我们在业务开发中往往会记录许多业务日志,这些日志一般是先发送到Kafka,然后再由Job消费Kafaka写到elasticsearch,在进行日志流处理的过程中,往往还会对日志做一些处理,比如过滤无效的日志,做一些计算以及重新组合日志等等,示意图如下:
+
+![fx_log](https://gitee.com/kevwan/static/raw/master/doc/images/fx_log.png)
+
+### 流处理工具fx
+
+[gozero](https://github.com/tal-tech/go-zero)是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/tal-tech/go-zero/tree/master/core/fx),下面我们通过一个简单的例子来认识下该工具:
+
+```go
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"github.com/tal-tech/go-zero/core/fx"
+)
+
+func main() {
+	ch := make(chan int)
+
+	go inputStream(ch)
+	go outputStream(ch)
+
+	c := make(chan os.Signal, 1)
+	signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
+	<-c
+}
+
+func inputStream(ch chan int) {
+	count := 0
+	for {
+		ch <- count
+		time.Sleep(time.Millisecond * 500)
+		count++
+	}
+}
+
+func outputStream(ch chan int) {
+	fx.From(func(source chan<- interface{}) {
+		for c := range ch {
+			source <- c
+		}
+	}).Walk(func(item interface{}, pipe chan<- interface{}) {
+		count := item.(int)
+		pipe <- count
+	}).Filter(func(item interface{}) bool {
+		itemInt := item.(int)
+		if itemInt%2 == 0 {
+			return true
+		}
+		return false
+	}).ForEach(func(item interface{}) {
+		fmt.Println(item)
+	})
+}
+```
+
+inputStream函数模拟了流数据的产生,outputStream函数模拟了流数据的处理过程,其中From函数为流的输入,Walk函数并发的作用在每一个item上,Filter函数对item进行过滤为true保留为false不保留,ForEach函数遍历输出每一个item元素。
+
+
+
+### 流数据处理中间操作
+
+一个流的数据处理可能存在许多的中间操作,每个中间操作都可以作用在流上。就像流水线上的工人一样,每个工人操作完零件后都会返回处理完成的新零件,同理流处理中间操作完成后也会返回一个新的流。
+
+![fx_middle](https://gitee.com/kevwan/static/raw/master/doc/images/fx_middle.png)
+
+fx的流处理中间操作:
+
+| 操作函数 | 功能                                      | 输入                         |
+| -------- | ----------------------------------------- | ---------------------------- |
+| Distinct | 去除重复的item                            | KeyFunc,返回需要去重的key   |
+| Filter   | 过滤不满足条件的item                      | FilterFunc,Option控制并发量 |
+| Group    | 对item进行分组                            | KeyFunc,以key进行分组       |
+| Head     | 取出前n个item,返回新stream               | int64保留数量                |
+| Map      | 对象转换                                  | MapFunc,Option控制并发量    |
+| Merge    | 合并item到slice并生成新stream             |                              |
+| Reverse  | 反转item                                  |                              |
+| Sort     | 对item进行排序                            | LessFunc实现排序算法         |
+| Tail     | 与Head功能类似,取出后n个item组成新stream | int64保留数量                |
+| Walk     | 作用在每个item上                          | WalkFunc,Option控制并发量   |
+
+下图展示了每个步骤和每个步骤的结果:
+
+![fx_step_result](https://gitee.com/kevwan/static/raw/master/doc/images/fx_step_result.png)
+
+
+### 用法与原理分析
+
+#### From
+
+通过From函数构建流并返回Stream,流数据通过channel进行存储:
+
+```go
+// 例子
+s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
+fx.From(func(source chan<- interface{}) {
+  for _, v := range s {
+    source <- v
+  }
+})
+
+// 源码
+func From(generate GenerateFunc) Stream {
+	source := make(chan interface{})
+
+	go func() {
+		defer close(source)
+    // 构造流数据写入channel
+		generate(source)
+	}()
+
+	return Range(source)
+}
+```
+
+#### Filter
+
+Filter函数提供过滤item的功能,FilterFunc定义过滤逻辑true保留item,false则不保留:
+
+```go
+// 例子 保留偶数
+s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
+fx.From(func(source chan<- interface{}) {
+  for _, v := range s {
+    source <- v
+  }
+}).Filter(func(item interface{}) bool {
+  if item.(int)%2 == 0 {
+    return true
+  }
+  return false
+})
+
+// 源码
+func (p Stream) Filter(fn FilterFunc, opts ...Option) Stream {
+	return p.Walk(func(item interface{}, pipe chan<- interface{}) {
+    // 执行过滤函数true保留,false丢弃
+		if fn(item) {
+			pipe <- item
+		}
+	}, opts...)
+}
+```
+
+#### Group
+
+Group对流数据进行分组,需定义分组的key,数据分组后以slice存入channel:
+
+```go
+// 例子 按照首字符"g"或者"p"分组,没有则分到另一组
+	ss := []string{"golang", "google", "php", "python", "java", "c++"}
+	fx.From(func(source chan<- interface{}) {
+		for _, s := range ss {
+			source <- s
+		}
+	}).Group(func(item interface{}) interface{} {
+		if strings.HasPrefix(item.(string), "g") {
+			return "g"
+		} else if strings.HasPrefix(item.(string), "p") {
+			return "p"
+		}
+		return ""
+	}).ForEach(func(item interface{}) {
+		fmt.Println(item)
+	})
+}
+
+// 源码
+func (p Stream) Group(fn KeyFunc) Stream {
+  // 定义分组存储map
+	groups := make(map[interface{}][]interface{})
+	for item := range p.source {
+    // 用户自定义分组key
+		key := fn(item)
+    // key相同分到一组
+		groups[key] = append(groups[key], item)
+	}
+
+	source := make(chan interface{})
+	go func() {
+		for _, group := range groups {
+      // 相同key的一组数据写入到channel
+			source <- group
+		}
+		close(source)
+	}()
+
+	return Range(source)
+}
+```
+
+#### Reverse
+
+reverse可以对流中元素进行反转处理:
+
+![](https://gitee.com/kevwan/static/raw/master/doc/images/fx_reverse.png)
+
+```go
+// 例子
+fx.Just(1, 2, 3, 4, 5).Reverse().ForEach(func(item interface{}) {
+  fmt.Println(item)
+})
+
+// 源码
+func (p Stream) Reverse() Stream {
+	var items []interface{}
+  // 获取流中数据
+	for item := range p.source {
+		items = append(items, item)
+	}
+	// 反转算法
+	for i := len(items)/2 - 1; i >= 0; i-- {
+		opp := len(items) - 1 - i
+		items[i], items[opp] = items[opp], items[i]
+	}
+	
+  // 写入流
+	return Just(items...)
+}
+```
+
+#### Distinct
+
+distinct对流中元素进行去重,去重在业务开发中比较常用,经常需要对用户id等做去重操作:
+
+```go
+// 例子
+fx.Just(1, 2, 2, 2, 3, 3, 4, 5, 6).Distinct(func(item interface{}) interface{} {
+  return item
+}).ForEach(func(item interface{}) {
+  fmt.Println(item)
+})
+// 结果为 1,2,3,4,5,6
+
+// 源码
+func (p Stream) Distinct(fn KeyFunc) Stream {
+	source := make(chan interface{})
+
+	threading.GoSafe(func() {
+		defer close(source)
+		// 通过key进行去重,相同key只保留一个
+		keys := make(map[interface{}]lang.PlaceholderType)
+		for item := range p.source {
+			key := fn(item)
+      // key存在则不保留
+			if _, ok := keys[key]; !ok {
+				source <- item
+				keys[key] = lang.Placeholder
+			}
+		}
+	})
+
+	return Range(source)
+}
+```
+
+#### Walk
+
+Walk函数并发的作用在流中每一个item上,可以通过WithWorkers设置并发数,默认并发数为16,最小并发数为1,如设置unlimitedWorkers为true则并发数无限制,但并发写入流中的数据由defaultWorkers限制,WalkFunc中用户可以自定义后续写入流中的元素,可以不写入也可以写入多个元素:
+
+```go
+// 例子
+fx.Just("aaa", "bbb", "ccc").Walk(func(item interface{}, pipe chan<- interface{}) {
+  newItem := strings.ToUpper(item.(string))
+  pipe <- newItem
+}).ForEach(func(item interface{}) {
+  fmt.Println(item)
+})
+
+// 源码
+func (p Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream {
+	pipe := make(chan interface{}, option.workers)
+
+	go func() {
+		var wg sync.WaitGroup
+		pool := make(chan lang.PlaceholderType, option.workers)
+
+		for {
+      // 控制并发数量
+			pool <- lang.Placeholder
+			item, ok := <-p.source
+			if !ok {
+				<-pool
+				break
+			}
+
+			wg.Add(1)
+			go func() {
+				defer func() {
+					wg.Done()
+					<-pool
+				}()
+				// 作用在每个元素上
+				fn(item, pipe)
+			}()
+		}
+
+    // 等待处理完成
+		wg.Wait()
+		close(pipe)
+	}()
+
+	return Range(pipe)
+}
+```
+
+### 并发处理
+
+fx工具除了进行流数据处理以外还提供了函数并发功能,在微服务中实现某个功能往往需要依赖多个服务,并发的处理依赖可以有效的降低依赖耗时,提升服务的性能。
+
+![concurrent_denpendency](https://gitee.com/kevwan/static/raw/master/doc//images/concurrent_denpendency.png)
+
+```go
+fx.Parallel(func() {
+  userRPC() // 依赖1
+}, func() {
+  accountRPC() // 依赖2
+}, func() {
+  orderRPC() // 依赖3
+})
+```
+
+注意fx.Parallel进行依赖并行处理的时候不会有error返回,如需有error返回或者有一个依赖报错需要立马结束依赖请求请使用[MapReduce](https://gocn.vip/topics/10941)工具进行处理。
+
+### 总结
+
+本篇文章介绍了流处理的基本概念和gozero中的流处理工具fx,在实际的生产中流处理场景应用也非常多,希望本篇文章能给大家带来一定的启发,更好的应对工作中的流处理场景。
+
+

+ 314 - 0
doc/goctl-model-sql.md

@@ -0,0 +1,314 @@
+# Goctl Model
+
+goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
+
+## 快速开始
+
+* 通过ddl生成
+
+    ```shell script
+    goctl model mysql ddl -src="./*.sql" -dir="./sql/model" -c=true
+    ```
+
+    执行上述命令后即可快速生成CURD代码。
+
+    ```Plain Text
+    model
+    │   ├── error.go
+    │   └── usermodel.go
+    ```
+
+* 通过datasource生成
+
+    ```shell script
+    goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="*"  -dir="./model"
+    ```
+
+* 生成代码示例
+  
+	```go
+
+	package model
+
+	import (
+		"database/sql"
+		"fmt"
+		"strings"
+		"time"
+
+		"github.com/tal-tech/go-zero/core/stores/cache"
+		"github.com/tal-tech/go-zero/core/stores/sqlc"
+		"github.com/tal-tech/go-zero/core/stores/sqlx"
+		"github.com/tal-tech/go-zero/core/stringx"
+		"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
+	)
+
+	var (
+		userFieldNames          = builderx.FieldNames(&User{})
+		userRows                = strings.Join(userFieldNames, ",")
+		userRowsExpectAutoSet   = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",")
+		userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
+
+		cacheUserIdPrefix     = "cache#User#id#"
+		cacheUserNamePrefix   = "cache#User#name#"
+		cacheUserMobilePrefix = "cache#User#mobile#"
+	)
+
+	type (
+		UserModel struct {
+			sqlc.CachedConn
+			table string
+		}
+
+		User struct {
+			Id         int64     `db:"id"`
+			Name       string    `db:"name"`     // 用户名称
+			Password   string    `db:"password"` // 用户密码
+			Mobile     string    `db:"mobile"`   // 手机号
+			Gender     string    `db:"gender"`   // 男|女|未公开
+			Nickname   string    `db:"nickname"` // 用户昵称
+			CreateTime time.Time `db:"create_time"`
+			UpdateTime time.Time `db:"update_time"`
+		}
+	)
+
+	func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) *UserModel {
+		return &UserModel{
+			CachedConn: sqlc.NewConn(conn, c),
+			table:      "user",
+		}
+	}
+
+	func (m *UserModel) Insert(data User) (sql.Result, error) {
+		userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
+		userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
+		ret, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
+			query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet)
+			return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
+		}, userNameKey, userMobileKey)
+		return ret, err
+	}
+
+	func (m *UserModel) FindOne(id int64) (*User, error) {
+		userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
+		var resp User
+		err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
+			query := fmt.Sprintf("select %s from %s where id = ? limit 1", userRows, m.table)
+			return conn.QueryRow(v, query, id)
+		})
+		switch err {
+		case nil:
+			return &resp, nil
+		case sqlc.ErrNotFound:
+			return nil, ErrNotFound
+		default:
+			return nil, err
+		}
+	}
+
+	func (m *UserModel) FindOneByName(name string) (*User, error) {
+		userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
+		var resp User
+		err := m.QueryRowIndex(&resp, userNameKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
+			query := fmt.Sprintf("select %s from %s where name = ? limit 1", userRows, m.table)
+			if err := conn.QueryRow(&resp, query, name); err != nil {
+				return nil, err
+			}
+			return resp.Id, nil
+		}, m.queryPrimary)
+		switch err {
+		case nil:
+			return &resp, nil
+		case sqlc.ErrNotFound:
+			return nil, ErrNotFound
+		default:
+			return nil, err
+		}
+	}
+
+	func (m *UserModel) FindOneByMobile(mobile string) (*User, error) {
+		userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
+		var resp User
+		err := m.QueryRowIndex(&resp, userMobileKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
+			query := fmt.Sprintf("select %s from %s where mobile = ? limit 1", userRows, m.table)
+			if err := conn.QueryRow(&resp, query, mobile); err != nil {
+				return nil, err
+			}
+			return resp.Id, nil
+		}, m.queryPrimary)
+		switch err {
+		case nil:
+			return &resp, nil
+		case sqlc.ErrNotFound:
+			return nil, ErrNotFound
+		default:
+			return nil, err
+		}
+	}
+
+	func (m *UserModel) Update(data User) error {
+		userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
+		_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
+			query := fmt.Sprintf("update %s set %s where id = ?", m.table, userRowsWithPlaceHolder)
+			return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
+		}, userIdKey)
+		return err
+	}
+
+	func (m *UserModel) Delete(id int64) error {
+		data, err := m.FindOne(id)
+		if err != nil {
+			return err
+		}
+
+		userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
+		userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
+		userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
+		_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
+			query := fmt.Sprintf("delete from %s where id = ?", m.table)
+			return conn.Exec(query, id)
+		}, userMobileKey, userIdKey, userNameKey)
+		return err
+	}
+
+	func (m *UserModel) formatPrimary(primary interface{}) string {
+		return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
+	}
+
+	func (m *UserModel) queryPrimary(conn sqlx.SqlConn, v, primary interface{}) error {
+		query := fmt.Sprintf("select %s from %s where id = ? limit 1", userRows, m.table)
+		return conn.QueryRow(v, query, primary)
+	}
+	```
+
+## 用法
+
+```Plain Text
+goctl model mysql -h
+```
+
+```Plain Text
+NAME:
+   goctl model mysql - generate mysql model"
+
+USAGE:
+   goctl model mysql command [command options] [arguments...]
+
+COMMANDS:
+   ddl         generate mysql model from ddl"
+   datasource  generate model from datasource"
+
+OPTIONS:
+   --help, -h  show help
+```
+
+## 生成规则
+
+* 默认规则
+  
+  我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
+* 带缓存模式
+  * ddl
+
+	```shell script
+	goctl model mysql -src={patterns} -dir={dir} -cache=true
+	```
+
+	help
+
+	```
+	NAME:
+	goctl model mysql ddl - generate mysql model from ddl
+
+	USAGE:
+	goctl model mysql ddl [command options] [arguments...]
+
+	OPTIONS:
+	--src value, -s value  the path or path globbing patterns of the ddl
+	--dir value, -d value  the target dir
+	--style value          the file naming style, lower|camel|underline,default is lower
+	--cache, -c            generate code with cache [optional]
+	--idea                 for idea plugin [optional]
+
+	```
+
+  * datasource
+
+	```shell script
+	goctl model mysql datasource -url={datasource} -table={patterns}  -dir={dir} -cache=true
+	```
+
+	help
+
+	```
+	NAME:
+	goctl model mysql datasource - generate model from datasource
+
+	USAGE:
+	goctl model mysql datasource [command options] [arguments...]
+
+	OPTIONS:
+	--url value              the data source of database,like "root:password@tcp(127.0.0.1:3306)/database
+	--table value, -t value  the table or table globbing patterns in the database
+	--cache, -c              generate code with cache [optional]
+	--dir value, -d value    the target dir
+	--style value            the file naming style, lower|camel|snake, default is lower
+	--idea                   for idea plugin [optional]
+
+	```
+
+	示例用法请参考[用法](./example/generator.sh)
+  
+	> NOTE: goctl model mysql ddl/datasource 均新增了一个`--style`参数,用于标记文件命名风格。
+
+  目前仅支持redis缓存,如果选择带缓存模式,即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码,目前仅支持单索引字段(除全文索引外),对于联合索引我们默认认为不需要带缓存,且不属于通用型代码,因此没有放在代码生成行列,如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
+
+* 不带缓存模式
+
+  * ddl
+  
+      ```shell script
+        goctl model -src={patterns} -dir={dir}
+      ```
+
+  * datasource
+  
+      ```shell script
+        goctl model mysql datasource -url={datasource} -table={patterns}  -dir={dir}
+      ```
+
+  or
+  * ddl
+
+      ```shell script
+        goctl model -src={patterns} -dir={dir} -cache=false
+      ```
+
+  * datasource
+  
+      ```shell script
+        goctl model mysql datasource -url={datasource} -table={patterns}  -dir={dir} -cache=false
+      ```
+  
+生成代码仅基本的CURD结构。
+
+## 缓存
+
+  对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
+
+* 缓存会缓存哪些信息?
+
+  对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。
+
+* 数据有更新(`update`)操作会清空缓存吗?
+  
+  会,但仅清空主键缓存的信息,why?这里就不做详细赘述了。
+
+* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码?
+  
+  理论上是没任何问题,但是我们认为,对于model层的数据操作均是以整个结构体为单位,包括查询,我不建议只查询某部分字段(不反对),否则我们的缓存就没有意义了。
+
+* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层?
+  
+  目前,我认为除了基本的CURD外,其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。
+

+ 265 - 0
doc/goctl-rpc.md

@@ -0,0 +1,265 @@
+# Rpc Generation
+
+Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。
+
+## 特性
+
+* 简单易用
+* 快速提升开发效率
+* 出错率低
+* 贴近protoc
+
+
+## 快速开始
+
+### 方式一:快速生成greet服务
+
+  通过命令 `goctl rpc new ${servieName}`生成
+
+  如生成greet rpc服务:
+
+  ```Bash
+  goctl rpc new greet
+  ```
+
+  执行后代码结构如下:
+
+  ```golang
+.
+├── etc             // yaml配置文件
+│   └── greet.yaml
+├── go.mod
+├── greet           // pb.go文件夹①
+│   └── greet.pb.go
+├── greet.go        // main函数
+├── greet.proto     // proto 文件
+├── greetclient     // call logic ②
+│   └── greet.go
+└── internal        
+    ├── config      // yaml配置对应的实体
+    │   └── config.go
+    ├── logic       // 业务代码
+    │   └── pinglogic.go
+    ├── server      // rpc server
+    │   └── greetserver.go
+    └── svc         // 依赖资源
+        └── servicecontext.go
+  ```
+
+> ① pb文件夹名(老版本文件夹固定为pb)称取自于proto文件中option go_package的值最后一层级按照一定格式进行转换,若无此声明,则取自于package的值,大致代码如下:
+
+```go
+  if option.Name == "go_package" {
+    ret.GoPackage = option.Constant.Source
+  }
+  ...
+  if len(ret.GoPackage) == 0 {
+    ret.GoPackage = ret.Package.Name
+  }
+  ret.PbPackage = GoSanitized(filepath.Base(ret.GoPackage))
+  ...
+```
+> GoSanitized方法请参考google.golang.org/protobuf@v1.25.0/internal/strs/strings.go:71
+
+> ② call 层文件夹名称取自于proto中service的名称,如该sercice的名称和pb文件夹名称相等,则会在srervice后面补充client进行区分,使pb和call分隔。
+
+```go
+if strings.ToLower(proto.Service.Name) == strings.ToLower(proto.GoPackage) {
+	callDir = filepath.Join(ctx.WorkDir, strings.ToLower(stringx.From(proto.Service.Name+"_client").ToCamel()))
+}
+```
+
+rpc一键生成常见问题解决,见 <a href="#常见问题解决">常见问题解决</a>
+
+### 方式二:通过指定proto生成rpc服务
+
+* 生成proto模板
+
+  ```Bash
+  goctl rpc template -o=user.proto
+  ```
+
+  ```golang
+  syntax = "proto3";
+
+  package remote;
+
+  message Request {
+    // 用户名
+    string username = 1;
+    // 用户密码
+    string password = 2;
+  }
+
+  message Response {
+    // 用户名称
+    string name = 1;
+    // 用户性别
+    string gender = 2;
+  }
+
+  service User {
+    // 登录
+    rpc Login(Request)returns(Response);
+  }
+  ```
+
+* 生成rpc服务代码
+
+  ```Bash
+  goctl rpc proto -src=user.proto
+  ```
+
+## 准备工作
+
+* 安装了go环境
+* 安装了protoc&protoc-gen-go,并且已经设置环境变量
+* 更多问题请见 <a href="#注意事项">注意事项</a>
+
+## 用法
+
+### rpc服务生成用法
+
+```Bash
+goctl rpc proto -h
+```
+
+```Bash
+NAME:
+   goctl rpc proto - generate rpc from proto
+
+USAGE:
+   goctl rpc proto [command options] [arguments...]
+
+OPTIONS:
+   --src value, -s value         the file path of the proto source file
+   --proto_path value, -I value  native command of protoc, specify the directory in which to search for imports. [optional]
+   --dir value, -d value         the target path of the code
+   --idea                        whether the command execution environment is from idea plugin. [optional]
+```
+
+### 参数说明
+
+* --src 必填,proto数据源,目前暂时支持单个proto文件生成
+* --proto_path 可选,protoc原生子命令,用于指定proto import从何处查找,可指定多个路径,如`goctl rpc -I={path1} -I={path2} ...`,在没有import时可不填。当前proto路径不用指定,已经内置,`-I`的详细用法请参考`protoc -h`
+* --dir 可选,默认为proto文件所在目录,生成代码的目标目录
+* --idea 可选,是否为idea插件中执行,终端执行可以忽略
+
+
+### 开发人员需要做什么
+
+关注业务代码编写,将重复性、与业务无关的工作交给goctl,生成好rpc服务代码后,开发人员仅需要修改
+
+* 服务中的配置文件编写(etc/xx.json、internal/config/config.go)
+* 服务中业务逻辑编写(internal/logic/xxlogic.go)
+* 服务中资源上下文的编写(internal/svc/servicecontext.go)
+
+
+### 注意事项
+
+* `google.golang.org/grpc`需要降级到v1.26.0,且protoc-gen-go版本不能高于v1.3.2(see [https://github.com/grpc/grpc-go/issues/3347](https://github.com/grpc/grpc-go/issues/3347))即
+  
+  ```shell script
+  replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
+  ```
+
+* proto不支持暂多文件同时生成
+* proto不支持外部依赖包引入,message不支持inline
+* 目前main文件、shared文件、handler文件会被强制覆盖,而和开发人员手动需要编写的则不会覆盖生成,这一类在代码头部均有
+
+```shell script
+    // Code generated by goctl. DO NOT EDIT!
+    // Source: xxx.proto
+```
+
+的标识,请注意不要将也写业务性代码写在里面。
+
+## proto import
+* 对于rpc中的requestType和returnType必须在main proto文件定义,对于proto中的message可以像protoc一样import其他proto文件。
+
+proto示例:
+
+### 错误import
+```proto
+syntax = "proto3";
+
+package greet;
+
+import "base/common.proto"
+
+message Request {
+  string ping = 1;
+}
+
+message Response {
+  string pong = 1;
+}
+
+service Greet {
+  rpc Ping(base.In) returns(base.Out);// request和return 不支持import
+}
+
+```
+
+
+### 正确import
+```proto
+syntax = "proto3";
+
+package greet;
+
+import "base/common.proto"
+
+message Request {
+  base.In in = 1;// 支持import
+}
+
+message Response {
+ base.Out out = 2;// 支持import
+}
+
+service Greet {
+  rpc Ping(Request) returns(Response);
+}
+```
+
+## 常见问题解决(go mod工程)
+
+* 错误一:
+
+  ```golang
+  pb/xx.pb.go:220:7: undefined: grpc.ClientConnInterface
+  pb/xx.pb.go:224:11: undefined: grpc.SupportPackageIsVersion6
+  pb/xx.pb.go:234:5: undefined: grpc.ClientConnInterface
+  pb/xx.pb.go:237:24: undefined: grpc.ClientConnInterface
+  ```
+
+  解决方法:请将`protoc-gen-go`版本降至v1.3.2及一下
+
+* 错误二:
+
+  ```golang
+
+  # go.etcd.io/etcd/clientv3/balancer/picker
+  ../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/err.go:25:9: cannot use &errPicker literal (type *errPicker) as type Picker in return argument:*errPicker does not implement Picker (wrong type for Pick method)
+    have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
+    want Pick(balancer.PickInfo) (balancer.PickResult, error)
+    ../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/roundrobin_balanced.go:33:9: cannot use &rrBalanced literal (type *rrBalanced) as type Picker in return argument:
+    *rrBalanced does not implement Picker (wrong type for Pick method)
+		have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
+    want Pick(balancer.PickInfo) (balancer.PickResult, error)
+    #github.com/tal-tech/go-zero/zrpc/internal/balancer/p2c
+    ../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:41:32: not enough arguments in call to base.NewBalancerBuilder
+	have (string, *p2cPickerBuilder)
+  want (string, base.PickerBuilder, base.Config)
+  ../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:58:9: cannot use &p2cPicker literal (type *p2cPicker) as type balancer.Picker in return argument:
+	*p2cPicker does not implement balancer.Picker (wrong type for Pick method)
+		have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
+		want Pick(balancer.PickInfo) (balancer.PickResult, error)
+  ```
+
+  解决方法:
+  
+    ```golang
+    replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
+    ```

+ 244 - 0
doc/goctl.md

@@ -0,0 +1,244 @@
+# goctl使用
+
+## goctl用途
+
+* 定义api请求
+* 根据定义的api自动生成golang(后端), java(iOS & Android), typescript(web & 小程序),dart(flutter)
+* 生成MySQL CURD+Cache
+* 生成MongoDB CURD+Cache
+
+## goctl使用说明
+
+### 快速生成服务
+
+* api: goctl api new xxxx
+* rpc: goctl rpc new xxxx
+
+#### goctl参数说明
+
+  `goctl api [go/java/ts] [-api user/user.api] [-dir ./src]`
+
+  > api 后面接生成的语言,现支持go/java/typescript
+  >
+  > -api 自定义api所在路径
+  >
+  > -dir 自定义生成目录
+
+如需自定义模板,运行如下命令生成`api gateway`模板:
+
+```shell
+goctl api go template
+```
+
+生成的模板放在`$HOME/.goctl`目录下,根据需要自行修改模板,下次运行`goctl`生成代码时会优先采用模板文件的内容
+
+#### API 语法说明
+
+``` golang
+info(
+    title: doc title
+    desc: >
+    doc description first part,
+    doc description second part<
+    version: 1.0
+)
+
+type int userType
+
+type user {
+	name string `json:"user"` // 用户姓名
+}
+
+type student {
+	name string `json:"name"` // 学生姓名
+}
+
+type teacher {
+}
+
+type (
+	address {
+		city string `json:"city"`
+	}
+
+	innerType {
+		image string `json:"image"`
+	}
+
+	createRequest {
+		innerType
+		name    string    `form:"name"`
+		age     int       `form:"age,optional"`
+		address []address `json:"address,optional"`
+	}
+
+	getRequest {
+		name string `path:"name"`
+		age  int    `form:"age,optional"`
+	}
+
+	getResponse {
+		code    int     `json:"code"`
+		desc    string  `json:"desc,omitempty"`
+		address address `json:"address"`
+		service int     `json:"service"`
+	}
+)
+
+service user-api {
+    @doc(
+        summary: user title
+        desc: >
+        user description first part,
+        user description second part,
+        user description second line
+    )
+    @server(
+        handler: GetUserHandler
+        group: user
+    )
+    get /api/user/:name(getRequest) returns(getResponse)
+
+    @server(
+        handler: CreateUserHandler
+        group: user
+    )
+    post /api/users/create(createRequest)
+}
+
+@server(
+    jwt: Auth
+    group: profile
+)
+service user-api {
+    @doc(summary: user title)
+    @handler GetProfileHandler
+    get /api/profile/:name(getRequest) returns(getResponse)
+
+    @handler CreateProfileHandler
+    post /api/profile/create(createRequest)
+}
+
+service user-api {
+    @doc(summary: desc in one line)
+    @handler PingHandler
+    head /api/ping()
+}
+
+```
+
+1. info部分:描述了api基本信息,比如Auth,api是哪个用途。
+
+2. type部分:type类型声明和golang语法兼容。
+
+3. service部分:
+   
+   * service代表一组服务,一个服务可以由多组名称相同的service组成,可以针对每一组service配置jwt和auth认证。
+   
+   * 通过group属性可以指定service生成所在子目录。
+   
+   * service里面包含api路由,比如上面第一组service的第一个路由,doc用来描述此路由的用途,GetProfileHandler表示处理这个路由的handler,
+     `get /api/profile/:name(getRequest) returns(getResponse)` 中get代表api的请求方式(get/post/put/delete), `/api/profile/:name` 描述了路由path,`:name`通过
+     请求getRequest里面的属性赋值,getResponse为返回的结构体,这两个类型都定义在2描述的类型中。
+   
+   * server 标签支持配置middleware,示例如下:
+   
+     ```go
+     @server(
+         middleware: AuthUser
+     )
+     ```
+   
+   添加完middleware后需要设置ServiceContext 中middleware变量的值,middleware实现可以参考测试用例 `TestWithMiddleware` 或者 `TestMultiMiddlewares`。
+   
+   * handler 支持缩写,实例如下:
+   
+     ```golang
+     @handler CreateProfileHandler
+     post /api/profile/create(createRequest)
+     ```
+
+4. 支持在info下面和type顶部import外部api文件,被import的文件只支持类型定义,import语法:` import xxxx.api `
+
+#### goland/vscode插件
+
+开发者可以在 goland 或 vscode 中搜索 goctl 的 api 插件,它们提供了 api 语法高亮,语法检测和格式化相关功能,插件安装及使用相关资料请点击[这里](https://github.com/tal-tech/goctl-plugins)。
+
+插件支持:
+
+ 1. 语法高亮和类型导航。
+ 2. 语法检测,格式化 api 会自动检测 api 编写错误地方。
+ 3. api 文档格式化( vscode 默认快捷键 `option+command+f`, goland 默认快捷键 `option+command+l`)。
+ 4. 上下文菜单,goland 插件提供了生成代码的快捷菜单。
+
+#### 根据定义好的api文件生成golang代码
+
+  命令如下:  
+  `goctl api go -api user/user.api -dir user`
+
+  ```Plain Text
+	.
+    ├── internal
+    │   ├── config
+    │   │   └── config.go
+    │   ├── handler
+    │   │   ├── pinghandler.go
+    │   │   ├── profile
+    │   │   │   ├── createprofilehandler.go
+    │   │   │   └── getprofilehandler.go
+    │   │   ├── routes.go
+    │   │   └── user
+    │   │       ├── createuserhandler.go
+    │   │       └── getuserhandler.go
+    │   ├── logic
+    │   │   ├── pinglogic.go
+    │   │   ├── profile
+    │   │   │   ├── createprofilelogic.go
+    │   │   │   └── getprofilelogic.go
+    │   │   └── user
+    │   │       ├── createuserlogic.go
+    │   │       └── getuserlogic.go
+    │   ├── svc
+    │   │   └── servicecontext.go
+    │   └── types
+    │       └── types.go
+    └── user.go
+  ```
+
+  生成的代码可以直接跑,有几个地方需要改:
+
+* 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
+* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
+
+#### 根据定义好的api文件生成java代码
+
+```shell
+goctl api java -api user/user.api -dir ./src
+```
+
+#### 根据定义好的api文件生成typescript代码
+
+```shell
+goctl api ts -api user/user.api -dir ./src -webapi ***
+
+ts需要指定webapi所在目录
+```
+
+#### 根据定义好的api文件生成Dart代码
+
+```shell
+goctl api dart -api user/user.api -dir ./src
+```
+
+## 根据mysql ddl或者datasource生成model文件
+
+```shell script
+goctl model mysql -src={filename} -dir={dir} -c
+```
+
+详情参考[model文档](goctl-model-sql.md)
+
+
+## goctl rpc生成
+
+见[goctl rpc](goctl-rpc.md)

二进制
doc/images/api-gen.png


二进制
doc/images/architecture-en.png


二进制
doc/images/architecture.png


二进制
doc/images/balancer.png


二进制
doc/images/benchmark.png


二进制
doc/images/bookstore-api.png


二进制
doc/images/bookstore-arch.png


二进制
doc/images/bookstore-benchmark.png


二进制
doc/images/bookstore-model.png


二进制
doc/images/bookstore-rpc.png


二进制
doc/images/breaker_state.png


二进制
doc/images/call_chain.png


二进制
doc/images/client_rejection2.png


二进制
doc/images/concurrent_denpendency.png


二进制
doc/images/datasource.png


二进制
doc/images/fx_log.png


二进制
doc/images/fx_middle.png


二进制
doc/images/fx_reverse.png


二进制
doc/images/fx_step_result.png


二进制
doc/images/go-zero.png


二进制
doc/images/interceptor.png


二进制
doc/images/model-gen.png


二进制
doc/images/mr.png


二进制
doc/images/mr_time.png


二进制
doc/images/panel.png


二进制
doc/images/prom_up.png


二进制
doc/images/prometheus.png


二进制
doc/images/qps.png


二进制
doc/images/qps_panel.png


二进制
doc/images/qq.jpg


二进制
doc/images/random_pseudo.png


二进制
doc/images/resilience-en.png


二进制
doc/images/resilience.jpg


二进制
doc/images/resolver.png


二进制
doc/images/rpc-gen.png


二进制
doc/images/shedding_flying.jpg


二进制
doc/images/shorturl-api.png


二进制
doc/images/shorturl-arch.png


二进制
doc/images/shorturl-benchmark.png


二进制
doc/images/shorturl-model.png


二进制
doc/images/shorturl-rpc.png


二进制
doc/images/timewheel-run.png


二进制
doc/images/timewheel-struct.png


二进制
doc/images/trie.png


二进制
doc/images/variables.png


二进制
doc/images/wechat.jpg


二进制
doc/images/zrpc.png


+ 136 - 0
doc/jwt.md

@@ -0,0 +1,136 @@
+# 基于go-zero实现JWT认证
+
+关于JWT是什么,大家可以看看[官网](https://jwt.io/),一句话介绍下:是可以实现服务器无状态的鉴权认证方案,也是目前最流行的跨域认证解决方案。
+
+要实现JWT认证,我们需要分成如下两个步骤
+
+* 客户端获取JWT token。
+* 服务器对客户端带来的JWT token认证。
+
+## 1.  客户端获取JWT Token
+
+我们定义一个协议供客户端调用获取JWT token,我们新建一个目录jwt然后在目录中执行 `goctl api -o jwt.api`,将生成的jwt.api改成如下:
+
+````go
+type JwtTokenRequest struct {
+}
+
+type JwtTokenResponse struct {
+  AccessToken  string `json:"access_token"`
+  AccessExpire int64  `json:"access_expire"`
+  RefreshAfter int64  `json:"refresh_after"` // 建议客户端刷新token的绝对时间
+}
+
+type GetUserRequest struct { 
+  UserId string `json:"userId"`
+}
+
+type GetUserResponse struct {
+  Name string `json:"name"`
+}
+
+service jwt-api {
+  @handler JwtHandler
+  post /user/token(JwtTokenRequest) returns (JwtTokenResponse)
+}
+
+@server(
+  jwt: JwtAuth
+)
+service jwt-api {
+  @handler GetUserHandler
+  post /user/info(GetUserRequest) returns (GetUserResponse)
+}
+````
+
+在服务jwt目录中执行:`goctl api go -api jwt.api -dir .`
+打开jwtlogic.go文件,修改 `func (l *JwtLogic) Jwt(req types.JwtTokenRequest) (*types.JwtTokenResponse, error) {` 方法如下:
+
+```go
+
+func (l *JwtLogic) Jwt(req types.JwtTokenRequest) (*types.JwtTokenResponse, error) {
+	var accessExpire = l.svcCtx.Config.JwtAuth.AccessExpire
+
+	now := time.Now().Unix()
+	accessToken, err := l.GenToken(now, l.svcCtx.Config.JwtAuth.AccessSecret, nil, accessExpire)
+	if err != nil {
+		return nil, err
+	}
+
+	return &types.JwtTokenResponse{
+    AccessToken:  accessToken,
+    AccessExpire: now + accessExpire,
+    RefreshAfter: now + accessExpire/2,
+  }, nil
+}
+
+func (l *JwtLogic) GenToken(iat int64, secretKey string, payloads map[string]interface{}, seconds int64) (string, error) {
+	claims := make(jwt.MapClaims)
+	claims["exp"] = iat + seconds
+	claims["iat"] = iat
+	for k, v := range payloads {
+		claims[k] = v
+	}
+
+	token := jwt.New(jwt.SigningMethodHS256)
+	token.Claims = claims
+
+	return token.SignedString([]byte(secretKey))
+}
+```
+
+在启动服务之前,我们需要修改etc/jwt-api.yaml文件如下:
+```yaml
+Name: jwt-api
+Host: 0.0.0.0
+Port: 8888
+JwtAuth:
+  AccessSecret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+  AccessExpire: 604800
+```
+启动服务器,然后测试下获取到的token。
+
+```sh
+➜ curl --location --request POST '127.0.0.1:8888/user/token'
+{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDEyNjE0MjksImlhdCI6MTYwMDY1NjYyOX0.6u_hpE_4m5gcI90taJLZtvfekwUmjrbNJ-5saaDGeQc","access_expire":1601261429,"refresh_after":1600959029}
+```
+
+## 2. 服务器验证JWT token
+
+1. 在api文件中通过`jwt: JwtAuth`标记的service表示激活了jwt认证。
+2. 可以阅读rest/handler/authhandler.go文件了解服务器jwt实现。
+3. 修改getuserlogic.go如下:
+
+```go
+func (l *GetUserLogic) GetUser(req types.GetUserRequest) (*types.GetUserResponse, error) {
+	return &types.GetUserResponse{Name: "kim"}, nil
+}
+```
+
+* 我们先不带JWT Authorization header请求头测试下,返回http status code是401,符合预期。
+
+```sh
+➜ curl -w  "\nhttp: %{http_code} \n" --location --request POST '127.0.0.1:8888/user/info' \
+--header 'Content-Type: application/json' \
+--data-raw '{
+    "userId": "a"
+}'
+
+http: 401
+```
+
+* 加上Authorization header请求头测试。
+
+```sh
+➜ curl -w  "\nhttp: %{http_code} \n" --location --request POST '127.0.0.1:8888/user/info' \
+--header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDEyNjE0MjksImlhdCI6MTYwMDY1NjYyOX0.6u_hpE_4m5gcI90taJLZtvfekwUmjrbNJ-5saaDGeQc' \
+--header 'Content-Type: application/json' \
+--data-raw '{
+    "userId": "a"
+}'
+{"name":"kim"}
+http: 200
+```
+
+综上所述:基于go-zero的JWT认证完成,在真实生产环境部署时候,AccessSecret, AccessExpire, RefreshAfter根据业务场景通过配置文件配置,RefreshAfter 是告诉客户端什么时候该刷新JWT token了,一般都需要设置过期时间前几天。
+

+ 86 - 0
doc/keywords.md

@@ -0,0 +1,86 @@
+# 高效的关键词替换和敏感词过滤工具
+
+## 1. 算法介绍
+
+利用高效的Trie树建立关键词树,如下图所示,然后依次查找字符串中的相连字符是否形成树的一条路径
+
+<img src="https://gitee.com/kevwan/static/raw/master/doc/images/trie.png" alt="trie" width="350" />
+
+发现掘金上[这篇文章](https://juejin.im/post/6844903750490914829)写的比较详细,可以一读,具体原理在此不详述。
+
+## 2. 关键词替换
+
+支持关键词重叠,自动选用最长的关键词,代码示例如下:
+
+```go
+replacer := stringx.NewReplacer(map[string]string{
+  "日本":    "法国",
+  "日本的首都": "东京",
+  "东京":    "日本的首都",
+})
+fmt.Println(replacer.Replace("日本的首都是东京"))
+```
+
+可以得到:
+
+```Plain Text
+东京是日本的首都
+```
+
+示例代码见`stringx/replace/replace.go`
+
+## 3. 查找敏感词
+
+代码示例如下:
+
+```go
+filter := stringx.NewTrie([]string{
+  "AV演员",
+  "苍井空",
+  "AV",
+  "日本AV女优",
+  "AV演员色情",
+})
+keywords := filter.FindKeywords("日本AV演员兼电视、电影演员。苍井空AV女优是xx出道, 日本AV女优们最精彩的表演是AV演员色情表演")
+fmt.Println(keywords)
+```
+
+可以得到:
+
+```Plain Text
+[苍井空 日本AV女优 AV演员色情 AV AV演员]
+```
+
+## 4. 敏感词过滤
+
+代码示例如下:
+
+```go
+filter := stringx.NewTrie([]string{
+  "AV演员",
+  "苍井空",
+  "AV",
+  "日本AV女优",
+  "AV演员色情",
+}, stringx.WithMask('?')) // 默认替换为*
+safe, keywords, found := filter.Filter("日本AV演员兼电视、电影演员。苍井空AV女优是xx出道, 日本AV女优们最精彩的表演是AV演员色情表演")
+fmt.Println(safe)
+fmt.Println(keywords)
+fmt.Println(found)
+```
+
+可以得到:
+
+```Plain Text
+日本????兼电视、电影演员。?????女优是xx出道, ??????们最精彩的表演是??????表演
+[苍井空 日本AV女优 AV演员色情 AV AV演员]
+true
+```
+
+示例代码见`stringx/filter/filter.go`
+
+## 5. Benchmark
+
+| Sentences | Keywords | Regex    | go-zero |
+| --------- | -------- | -------- | ------- |
+| 10000     | 10000    | 16min10s | 27.2ms  |

+ 51 - 0
doc/loadshedding.md

@@ -0,0 +1,51 @@
+# 服务自适应降载保护设计
+
+## 设计目的
+
+* 保证系统不被过量请求拖垮
+* 在保证系统稳定的前提下,尽可能提供更高的吞吐量
+
+## 设计考虑因素
+
+* 如何衡量系统负载
+  * 是否处于虚机或容器内,需要读取cgroup相关负载
+  * 用1000m表示100%CPU,推荐使用800m表示系统高负载
+* 尽可能小的Overhead,不显著增加RT
+* 不考虑服务本身所依赖的DB或者缓存系统问题,这类问题通过熔断机制来解决
+
+## 机制设计
+
+* 计算CPU负载时使用滑动平均来降低CPU负载抖动带来的不稳定,关于滑动平均见参考资料
+  * 滑动平均就是取之前连续N次值的近似平均,N取值可以通过超参beta来决定
+  * 当CPU负载大于指定值时触发降载保护机制
+* 时间窗口机制,用滑动窗口机制来记录之前时间窗口内的QPS和RT(response time)
+  * 滑动窗口使用5秒钟50个桶的方式,每个桶保存100ms时间内的请求,循环利用,最新的覆盖最老的
+  * 计算maxQPS和minRT时需要过滤掉最新的时间没有用完的桶,防止此桶内只有极少数请求,并且RT处于低概率的极小值,所以计算maxQPS和minRT时按照上面的50个桶的参数只会算49个
+* 满足以下所有条件则拒绝该请求
+	1. 当前CPU负载超过预设阈值,或者上次拒绝时间到现在不超过1秒(冷却期)。冷却期是为了不能让负载刚下来就马上增加压力导致立马又上去的来回抖动
+	2. `averageFlying > max(1, QPS*minRT/1e3)`
+		* averageFlying = MovingAverage(flying)
+		* 在算MovingAverage(flying)的时候,超参beta默认取值为0.9,表示计算前十次的平均flying值
+		* 取flying值的时候,有三种做法:
+			1. 请求增加后更新一次averageFlying,见图中橙色曲线
+			2. 请求结束后更新一次averageFlying,见图中绿色曲线
+			3. 请求增加后更新一次averageFlying,请求结束后更新一次averageFlying
+
+			我们使用的是第二种,这样可以更好的防止抖动,如图:
+			![flying策略对比](https://gitee.com/kevwan/static/raw/master/doc/images/shedding_flying.jpg)
+		* QPS = maxPass * bucketsPerSecond
+			* maxPass表示每个有效桶里的成功的requests
+			* bucketsPerSecond表示每秒有多少个桶
+		* 1e3表示1000毫秒,minRT单位也是毫秒,QPS*minRT/1e3得到的就是平均每个时间点有多少并发请求
+
+## 降载的使用
+
+* 已经在rest和zrpc框架里增加了可选激活配置
+  * CpuThreshold,如果把值设置为大于0的值,则激活该服务的自动降载机制
+* 如果请求被drop,那么错误日志里会有`dropreq`关键字
+
+## 参考资料
+
+* [滑动平均](https://www.cnblogs.com/wuliytTaotao/p/9479958.html)
+* [Sentinel自适应限流](https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81)
+* [Kratos自适应限流保护](https://github.com/bilibili/kratos/blob/master/doc/wiki-cn/ratelimit.md)

+ 123 - 0
doc/mapping.md

@@ -0,0 +1,123 @@
+# 文本序列化和反序列化
+
+go-zero针对文本的序列化和反序列化主要在三个地方使用
+
+* http api请求体的反序列化
+* http api返回体的序列化
+* 配置文件的反序列化
+
+本文假定读者已经定义过api文件以及修改过配置文件,如不熟悉,可参照
+
+* [快速构建高并发微服务](shorturl.md)
+* [快速构建高并发微服务](../docs/frame/bookstore.md)
+
+## 1. http api请求体的反序列化
+
+在反序列化的过程中的针对请求数据的`数据格式`以及`数据校验`需求,go-zero实现了自己的一套反序列化机制
+
+### 1.1 `数据格式`以订单order.api文件为例
+
+```go
+type (
+	createOrderReq struct {
+		token     string `path:"token"`     // 用户token
+		productId string `json:"productId"` // 商品ID
+		num       int    `json:"num"`       // 商品数量
+	}
+	createOrderRes struct {
+		success bool `json:"success"` // 是否成功
+	}
+	findOrderReq struct {
+		token    string `path:"token"`    // 用户token
+		page     int    `form:"page"`     // 页数
+		pageSize int8   `form:"pageSize"` // 页大小
+	}
+	findOrderRes struct {
+		orderInfo []orderInfo `json:"orderInfo"` // 商品ID
+	}
+	orderInfo struct {
+		productId   string `json:"productId"`   // 商品ID
+		productName string `json:"productName"` // 商品名称
+		num         int    `json:"num"`         // 商品数量
+	}
+	deleteOrderReq struct {
+		id string `path:"id"`
+	}
+	deleteOrderRes struct {
+		success bool `json:"success"` // 是否成功
+	}
+)
+
+service order {
+    @doc(
+        summary: 创建订单
+    )
+    @handler CreateOrderHandler
+    post /order/add/:token(createOrderReq) returns(createOrderRes)
+
+    @doc(
+        summary: 获取订单
+    )
+    @handler FindOrderHandler
+    get /order/find/:token(findOrderReq) returns(findOrderRes)
+
+    @doc(
+        summary: 删除订单
+    )
+    @handler: DeleteOrderHandler
+    delete /order/:id(deleteOrderReq) returns(deleteOrderRes)
+}
+```
+
+http api请求体的反序列化的tag有三种:
+
+* `path`:http url 路径中参数反序列化
+  * `/order/add/1234567`会解析出来token为1234567
+* `form`:http  form表单反序列化,需要 header头添加  Content-Type: multipart/form-data
+  * `/order/find/1234567?page=1&pageSize=20`会解析出来token为1234567,page为1,pageSize为20
+
+* `json`:http request json body反序列化,需要 header头添加  Content-Type: application/json
+  * `{"productId":"321","num":1}`会解析出来productId为321,num为1
+
+### 1.2 `数据校验`以用户user.api文件为例
+
+```go
+type (
+	createUserReq struct {
+		age    int8   `json:"age,default=20,range=(12:100]"` // 年龄
+		name   string `json:"name"`                          // 名字
+		alias  string `json:"alias,optional"`                // 别名
+		sex    string `json:"sex,options=male|female"`       // 性别
+		avatar string `json:"avatar,default=default.png"`    // 头像
+	}
+	createUserRes struct {
+		success bool `json:"success"` // 是否成功
+	}
+)
+
+service user {
+    @doc(
+        summary: 创建订单
+    )
+    @handler CreateUserHandler
+    post /user/add(createUserReq) returns(createUserRes)
+}
+```
+
+数据校验有很多种方式,包括以下但不限:
+
+* `age`:默认不输入为20,输入则取值范围为(12:100],前开后闭
+* `name`:必填,不可为空
+* `alias`:选填,可为空
+* `sex`:必填,取值为`male`或`female`
+* `avatar`:选填,默认为`default.png`
+
+更多详情参见[unmarshaler_test.go](../core/mapping/unmarshaler_test.go)
+
+## 2. http api返回体的序列化
+
+* 使用官方默认的`encoding/json`包序列化,在此不再累赘
+
+## 3. 配置文件的反序列化
+
+* `配置文件的反序列化`和`http api请求体的反序列化`使用同一套解析规则,可参照`http api请求体的反序列化`

+ 190 - 0
doc/mapreduce.md

@@ -0,0 +1,190 @@
+# 通过MapReduce降低服务响应时间
+
+在微服务中开发中,api网关扮演对外提供restful api的角色,而api的数据往往会依赖其他服务,复杂的api更是会依赖多个甚至数十个服务。虽然单个被依赖服务的耗时一般都比较低,但如果多个服务串行依赖的话那么整个api的耗时将会大大增加。
+
+那么通过什么手段来优化呢?我们首先想到的是通过并发来的方式来处理依赖,这样就能降低整个依赖的耗时,Go基础库中为我们提供了 [WaitGroup](https://golang.org/pkg/sync/#WaitGroup) 工具用来进行并发控制,但实际业务场景中多个依赖如果有一个出错我们期望能立即返回而不是等所有依赖都执行完再返回结果,而且WaitGroup中对变量的赋值往往需要加锁,每个依赖函数都需要添加Add和Done对于新手来说比较容易出错
+
+基于以上的背景,go-zero框架中为我们提供了并发处理工具[MapReduce](https://github.com/tal-tech/go-zero/blob/master/core/mr/mapreduce.go),该工具开箱即用,不需要做什么初始化,我们通过下图看下使用MapReduce和没使用的耗时对比:
+
+![依赖耗时对比](https://gitee.com/kevwan/static/raw/master/doc/images/mr_time.png)
+
+相同的依赖,串行处理的话需要200ms,使用MapReduce后的耗时等于所有依赖中最大的耗时为100ms,可见MapReduce可以大大降低服务耗时,而且随着依赖的增加效果就会越明显,减少处理耗时的同时并不会增加服务器压力
+
+## 并发处理工具[MapReduce](https://github.com/tal-tech/go-zero/tree/master/core/mr)
+
+[MapReduce](https://zh.wikipedia.org/wiki/MapReduce)是Google提出的一个软件架构,用于大规模数据集的并行运算,go-zero中的MapReduce工具正是借鉴了这种架构思想  
+
+go-zero框架中的MapReduce工具主要用来对批量数据进行并发的处理,以此来提升服务的性能  
+
+![mapreduce原理图](https://gitee.com/kevwan/static/raw/master/doc/images/mr.png)
+
+我们通过几个示例来演示MapReduce的用法  
+
+MapReduce主要有三个参数,第一个参数为generate用以生产数据,第二个参数为mapper用以对数据进行处理,第三个参数为reducer用以对mapper后的数据做聚合返回,还可以通过opts选项设置并发处理的线程数量  
+
+场景一: 某些功能的结果往往需要依赖多个服务,比如商品详情的结果往往会依赖用户服务、库存服务、订单服务等等,一般被依赖的服务都是以rpc的形式对外提供,为了降低依赖的耗时我们往往需要对依赖做并行处理  
+
+```go
+func productDetail(uid, pid int64) (*ProductDetail, error) {
+	var pd ProductDetail
+	err := mr.Finish(func() (err error) {
+		pd.User, err = userRpc.User(uid)
+		return
+	}, func() (err error) {
+		pd.Store, err = storeRpc.Store(pid)
+		return
+	}, func() (err error) {
+		pd.Order, err = orderRpc.Order(pid)
+		return
+	})
+
+	if err != nil {
+		log.Printf("product detail error: %v", err)
+		return nil, err
+	}
+
+	return &pd, nil
+}
+```
+
+该示例中返回商品详情依赖了多个服务获取数据,因此做并发的依赖处理,对接口的性能有很大的提升  
+
+场景二: 很多时候我们需要对一批数据进行处理,比如对一批用户id,效验每个用户的合法性并且效验过程中有一个出错就认为效验失败,返回的结果为效验合法的用户id  
+
+```go
+func checkLegal(uids []int64) ([]int64, error) {
+	r, err := mr.MapReduce(func(source chan<- interface{}) {
+		for _, uid := range uids {
+			source <- uid
+		}
+	}, func(item interface{}, writer mr.Writer, cancel func(error)) {
+		uid := item.(int64)
+		ok, err := check(uid)
+		if err != nil {
+			cancel(err)
+		}
+		if ok {
+			writer.Write(uid)
+		}
+	}, func(pipe <-chan interface{}, writer mr.Writer, cancel func(error)) {
+		var uids []int64
+		for p := range pipe {
+			uids = append(uids, p.(int64))
+		}
+		writer.Write(uids)
+	})
+	if err != nil {
+        log.Printf("check error: %v", err)
+		return nil, err
+	}
+
+	return r.([]int64), nil
+}
+
+func check(uid int64) (bool, error) {
+	// do something check user legal
+	return true, nil
+}
+```
+
+该示例中,如果check过程出现错误则通过cancel方法结束效验过程,并返回error整个效验过程结束,如果某个uid效验结果为false则最终结果不返回该uid
+
+***MapReduce使用注意事项***
+
+* mapper和reducer中都可以调用cancel,参数为error,调用后立即返回,返回结果为nil, error
+* mapper中如果不调用writer.Write则item最终不会被reducer聚合
+* reducer中如果不调用writer.Wirte则返回结果为nil, ErrReduceNoOutput
+* reducer为单线程,所有mapper出来的结果在这里串行聚合
+
+***实现原理分析:***
+
+MapReduce中首先通过buildSource方法通过执行generate(参数为无缓冲channel)产生数据,并返回无缓冲的channel,mapper会从该channel中读取数据
+
+```go
+func buildSource(generate GenerateFunc) chan interface{} {
+    source := make(chan interface{})
+    go func() {
+        defer close(source)
+        generate(source)
+    }()
+
+    return source
+}
+```
+
+在MapReduceWithSource方法中定义了cancel方法,mapper和reducer中都可以调用该方法,调用后主线程收到close信号会立马返回  
+
+```go
+cancel := once(func(err error) {
+    if err != nil {
+        retErr.Set(err)
+    } else {
+        // 默认的error
+        retErr.Set(ErrCancelWithNil)
+    }
+
+    drain(source)
+    // 调用close(ouput)主线程收到Done信号,立马返回
+    finish()
+})
+```
+
+在mapperDispatcher方法中调用了executeMappers,executeMappers消费buildSource产生的数据,每一个item都会起一个goroutine单独处理,默认最大并发数为16,可以通过WithWorkers进行设置  
+
+```go
+var wg sync.WaitGroup
+defer func() {
+    wg.Wait() // 保证所有的item都处理完成
+    close(collector)
+}()
+
+pool := make(chan lang.PlaceholderType, workers)
+writer := newGuardedWriter(collector, done) // 将mapper处理完的数据写入collector
+for {
+    select {
+    case <-done: // 当调用了cancel会触发立即返回
+        return
+    case pool <- lang.Placeholder: // 控制最大并发数
+        item, ok := <-input
+        if !ok {
+            <-pool
+            return
+        }
+
+        wg.Add(1)
+        go func() {
+            defer func() {
+                wg.Done()
+                <-pool
+            }()
+
+            mapper(item, writer) // 对item进行处理,处理完调用writer.Write把结果写入collector对应的channel中
+        }()
+    }
+}
+```
+
+reducer单goroutine对数mapper写入collector的数据进行处理,如果reducer中没有手动调用writer.Write则最终会执行finish方法对output进行close避免死锁
+
+```go
+go func() {
+    defer func() {
+        if r := recover(); r != nil {
+            cancel(fmt.Errorf("%v", r))
+        } else {
+            finish()
+        }
+    }()
+    reducer(collector, writer, cancel)
+}()
+```
+
+在该工具包中还提供了许多针对不同业务场景的方法,实现原理与MapReduce大同小异,感兴趣的同学可以查看源码学习
+
+* MapReduceVoid 功能和MapReduce类似但没有结果返回只返回error
+* Finish 处理固定数量的依赖,返回error,有一个error立即返回
+* FinishVoid 和Finish方法功能类似,没有返回值
+* Map 只做generate和mapper处理,返回channel
+* MapVoid 和Map功能类似,无返回
+
+本文主要介绍了go-zero框架中的MapReduce工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。

+ 113 - 0
doc/metric.md

@@ -0,0 +1,113 @@
+# 基于prometheus的微服务指标监控
+
+服务上线后我们往往需要对服务进行监控,以便能及早发现问题并做针对性的优化,监控又可分为多种形式,比如日志监控,调用链监控,指标监控等等。而通过指标监控能清晰的观察出服务指标的变化趋势,了解服务的运行状态,对于保证服务稳定起着非常重要的作用
+
+[prometheus](https://prometheus.io/)是一个开源的系统监控和告警工具,支持强大的查询语言PromQL允许用户实时选择和汇聚时间序列数据,时间序列数据是服务端通过HTTP协议主动拉取获得,也可以通过中间网关来推送时间序列数据,可以通过静态配置文件或服务发现来获取监控目标
+
+## Prometheus 的架构
+
+Prometheus 的整体架构以及生态系统组件如下图所示:
+
+![prometheus](https://gitee.com/kevwan/static/raw/master/doc/images/prometheus.png)
+
+Prometheus Server直接从监控目标中或者间接通过推送网关来拉取监控指标,它在本地存储所有抓取到样本数据,并对此数据执行一系列规则,以汇总和记录现有数据的新时间序列或生成告警。可以通过 [Grafana](https://grafana.com/) 或者其他工具来实现监控数据的可视化
+
+## go-zero基于prometheus的服务指标监控
+
+[go-zero](https://github.com/tal-tech/go-zero) 框架中集成了基于prometheus的服务指标监控,下面我们通过go-zero官方的示例[shorturl](https://github.com/tal-tech/go-zero/blob/master/doc/shorturl.md)来演示是如何对服务指标进行收集监控的:
+
+- 第一步需要先安装Prometheus,安装步骤请参考[官方文档](https://prometheus.io/)
+- go-zero默认不开启prometheus监控,开启方式很简单,只需要在shorturl-api.yaml文件中增加配置如下,其中Host为Prometheus Server地址为必填配置,Port端口不填默认9091,Path为用来拉取指标的路径默认为/metrics
+
+```go
+Prometheus:
+  Host: 127.0.0.1
+  Port: 9091
+  Path: /metrics
+```
+
+- 编辑prometheus的配置文件prometheus.yml,添加如下配置,并创建targets.json
+
+```go
+- job_name: 'file_ds'
+    file_sd_configs:
+    - files:
+      - targets.json
+```
+
+- 编辑targets.json文件,其中targets为shorturl配置的目标地址,并添加了几个默认的标签
+
+```go
+[
+    {
+        "targets": ["127.0.0.1:9091"],
+        "labels": {
+            "job": "shorturl-api",
+            "app": "shorturl-api",
+            "env": "test",
+            "instance": "127.0.0.1:8888"
+        }
+    }
+]
+```
+
+- 启动prometheus服务,默认侦听在9090端口
+
+```go
+prometheus --config.file=prometheus.yml
+```
+
+- 在浏览器输入http://127.0.0.1:9090/,然后点击Status -> Targets即可看到状态为Up的Job,并且Lables栏可以看到我们配置的默认的标签
+
+![job状态为up](https://gitee.com/kevwan/static/raw/master/doc/images/prom_up.png)
+
+通过以上几个步骤我们完成了prometheus对shorturl服务的指标监控收集的配置工作,为了演示简单我们进行了手动的配置,在实际的生产环境中一般采用定时更新配置文件或者服务发现的方式来配置监控目标,篇幅有限这里不展开讲解,感兴趣的同学请自行查看相关文档
+
+## go-zero监控的指标类型
+
+go-zero中目前在http的中间件和rpc的拦截器中添加了对请求指标的监控。
+
+主要从请求耗时和请求错误两个维度,请求耗时采用了Histogram指标类型定义了多个Buckets方便进行分位统计,请求错误采用了Counter类型,并在http metric中添加了path标签rpc metric中添加了method标签以便进行细分监控。
+
+接下来演示如何查看监控指标:
+
+首先在命令行多次执行如下命令
+
+```go
+curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
+```
+
+打开Prometheus切换到Graph界面,在输入框中输入{path="/shorten"}指令,即可查看监控指标,如下图
+
+![查询面板](https://gitee.com/kevwan/static/raw/master/doc/images/panel.png)
+
+我们通过PromQL语法查询过滤path为/shorten的指标,结果中显示了指标名以及指标数值,其中http_server_requests_code_total指标中code值为http的状态码,200表明请求成功,http_server_requests_duration_ms_bucket中对不同bucket结果分别进行了统计,还可以看到所有的指标中都添加了我们配置的默认指标
+
+Console界面主要展示了查询的指标结果,Graph界面为我们提供了简单的图形化的展示界面,在实际的生产环境中我们一般使用Grafana做图形化的展示
+
+## grafana可视化界面
+
+[grafana](https://grafana.com/)是一款可视化工具,功能强大,支持多种数据来源Prometheus、Elasticsearch、Graphite等,安装比较简单请参考[官方文档](https://grafana.com/docs/grafana/latest/),grafana默认端口3000,安装好后再浏览器输入http://localhost:3000/,默认账号和密码都为admin
+
+下面演示如何基于以上指标进行可视化界面的绘制:
+
+- 点击左侧边栏Configuration->Data Source->Add data source进行数据源添加,其中HTTP的URL为数据源的地址
+
+![datasource](https://gitee.com/kevwan/static/raw/master/doc/images/datasource.png)
+
+- 点击左侧边栏添加dashboard,然后添加Variables方便针对不同的标签进行过滤筛选比如添加app变量用来过滤不同的服务
+
+![variables](https://gitee.com/kevwan/static/raw/master/doc/images/variables.png)
+
+- 进入dashboard点击右上角Add panel添加面板,以path维度统计接口的qps
+
+![qps](https://gitee.com/kevwan/static/raw/master/doc/images/qps.png)
+
+- 最终的效果如下所示,可以通过服务名称过滤不同的服务,面板展示了path为/shorten的qps变化趋势
+
+![qps panel](https://gitee.com/kevwan/static/raw/master/doc/images/qps_panel.png)
+
+## 总结
+
+以上演示了go-zero中基于prometheus+grafana服务指标监控的简单流程,生产环境中可以根据实际的场景做不同维度的监控分析。现在go-zero的监控指标主要还是针对http和rpc,这对于服务的整体监控显然还是不足的,比如容器资源的监控,依赖的mysql、redis等资源的监控,以及自定义的指标监控等等,go-zero在这方面后续还会持续优化。希望这篇文章能够给您带来帮助
+

+ 15 - 0
doc/periodicalexecutor.md

@@ -0,0 +1,15 @@
+# PeriodicalExecutor设计
+
+## 添加任务
+
+* 当前没有未执行的任务
+  * 添加并启动定时器
+* 已有未执行的任务
+  * 添加并检查是否到达最大缓存数
+    * 如到,执行所有缓存任务
+    * 未到,只添加
+
+## 定时器到期
+
+* 清除并执行所有缓存任务
+* 再等待N个定时周期,如果等待过程中一直没有新任务,则退出

+ 167 - 0
doc/sharedcalls.md

@@ -0,0 +1,167 @@
+# 防止缓存击穿之进程内共享调用
+
+go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等。
+
+本文主要讲述进程内共享调用神器[SharedCalls](https://github.com/tal-tech/go-zero/blob/master/core/syncx/sharedcalls.go)。  
+
+## 使用场景
+
+并发场景下,可能会有多个线程(协程)同时请求同一份资源,如果每个请求都要走一遍资源的请求过程,除了比较低效之外,还会对资源服务造成并发的压力。举一个具体例子,比如缓存失效,多个请求同时到达某服务请求某资源,该资源在缓存中已经失效,此时这些请求会继续访问DB做查询,会引起数据库压力瞬间增大。而使用SharedCalls可以使得同时多个请求只需要发起一次拿结果的调用,其他请求"坐享其成",这种设计有效减少了资源服务的并发压力,可以有效防止缓存击穿。
+
+高并发场景下,当某个热点key缓存失效后,多个请求会同时从数据库加载该资源,并保存到缓存,如果不做防范,可能会导致数据库被直接打死。针对这种场景,go-zero框架中已经提供了实现,具体可参看[sqlc](https://github.com/tal-tech/go-zero/blob/master/core/stores/sqlc/cachedsql.go)和[mongoc](https://github.com/tal-tech/go-zero/blob/master/core/stores/mongoc/cachedcollection.go)等实现代码。
+
+为了简化演示代码,我们通过多个线程同时去获取一个id来模拟缓存的场景。如下:
+
+```go
+func main() {
+  const round = 5
+  var wg sync.WaitGroup
+  barrier := syncx.NewSharedCalls()
+
+  wg.Add(round)
+  for i := 0; i < round; i++ {
+    // 多个线程同时执行
+    go func() {
+      defer wg.Done()
+      // 可以看到,多个线程在同一个key上去请求资源,获取资源的实际函数只会被调用一次
+      val, err := barrier.Do("once", func() (interface{}, error) {
+        // sleep 1秒,为了让多个线程同时取once这个key上的数据
+        time.Sleep(time.Second)
+        // 生成了一个随机的id
+        return stringx.RandId(), nil
+      })
+      if err != nil {
+        fmt.Println(err)
+      } else {
+        fmt.Println(val)
+      }
+    }()
+  }
+
+  wg.Wait()
+}
+```
+
+运行,打印结果为:
+
+```
+837c577b1008a0db
+837c577b1008a0db
+837c577b1008a0db
+837c577b1008a0db
+837c577b1008a0db
+```
+
+可以看出,只要是同一个key上的同时发起的请求,都会共享同一个结果,对获取DB数据进缓存等场景特别有用,可以有效防止缓存击穿。
+
+## 关键源码分析
+
+- SharedCalls interface提供了Do和DoEx两种方法的抽象
+
+  ```go
+  // SharedCalls接口提供了Do和DoEx两种方法
+  type SharedCalls interface {
+    Do(key string, fn func() (interface{}, error)) (interface{}, error)
+    DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error)
+  }
+  ```
+
+- SharedCalls interface的具体实现sharedGroup
+
+  ```go
+  // call代表对指定资源的一次请求
+  type call struct {
+    wg  sync.WaitGroup  // 用于协调各个请求goroutine之间的资源共享
+    val interface{}     // 用于保存请求的返回值
+    err error           // 用于保存请求过程中发生的错误
+  }
+  
+  type sharedGroup struct {
+    calls map[string]*call
+    lock  sync.Mutex
+  }
+  ```
+
+- sharedGroup的Do方法
+
+  - key参数:可以理解为资源的唯一标识。
+  - fn参数:真正获取资源的方法。
+  - 处理过程分析:
+
+  ```go
+  // 当多个请求同时使用Do方法请求资源时
+  func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
+    // 先申请加锁
+    g.lock.Lock()
+  
+    // 根据key,获取对应的call结果,并用变量c保存
+    if c, ok := g.calls[key]; ok {
+      // 拿到call以后,释放锁,此处call可能还没有实际数据,只是一个空的内存占位
+      g.lock.Unlock()
+      // 调用wg.Wait,判断是否有其他goroutine正在申请资源,如果阻塞,说明有其他goroutine正在获取资源
+      c.wg.Wait()
+      // 当wg.Wait不再阻塞,表示资源获取已经结束,可以直接返回结果
+      return c.val, c.err
+    }
+
+    // 没有拿到结果,则调用makeCall方法去获取资源,注意此处仍然是锁住的,可以保证只有一个goroutine可以调用makecall
+    c := g.makeCall(key, fn)
+    // 返回调用结果
+    return c.val, c.err
+  }
+  ```
+  
+- sharedGroup的DoEx方法
+
+  - 和Do方法类似,只是返回值中增加了布尔值表示值是调用makeCall方法直接获取的,还是取的共享成果
+
+  ```go
+  func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) {
+    g.lock.Lock()
+    if c, ok := g.calls[key]; ok {
+      g.lock.Unlock()
+      c.wg.Wait()
+      return c.val, false, c.err
+    }
+
+    c := g.makeCall(key, fn)
+    return c.val, true, c.err
+  }
+  ```
+
+- sharedGroup的makeCall方法
+
+  - 该方法由Do和DoEx方法调用,是真正发起资源请求的方法。
+  
+  ```go
+  // 进入makeCall的一定只有一个goroutine,因为要拿锁锁住的
+  func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call {
+    // 创建call结构,用于保存本次请求的结果
+    c := new(call)
+    // wg加1,用于通知其他请求资源的goroutine等待本次资源获取的结束
+    c.wg.Add(1)
+    // 将用于保存结果的call放入map中,以供其他goroutine获取
+    g.calls[key] = c
+    // 释放锁,这样其他请求的goroutine才能获取call的内存占位
+    g.lock.Unlock()
+  
+    defer func() {
+      // delete key first, done later. can't reverse the order, because if reverse,
+      // another Do call might wg.Wait() without get notified with wg.Done()
+      g.lock.Lock()
+      delete(g.calls, key)
+      g.lock.Unlock()
+
+      // 调用wg.Done,通知其他goroutine可以返回结果,这样本批次所有请求完成结果的共享
+      c.wg.Done()
+    }()
+  
+    // 调用fn方法,将结果填入变量c中
+    c.val, c.err = fn()
+    return c
+  }
+  ```
+
+## 最后
+
+本文主要介绍了go-zero框架中的 SharedCalls工具,对其应用场景和关键代码做了简单的梳理,希望本篇文章能给大家带来一些收获。

+ 543 - 0
doc/shorturl-en.md

@@ -0,0 +1,543 @@
+# Rapid development of microservices
+
+English | [简体中文](shorturl.md)
+
+## 0. Why building microservices are so difficult
+
+To build a well working microservice, we need lots of knowledges from different aspects.
+
+* basic functionalities
+  1. concurrency control and rate limit, to avoid being brought down by unexpected inbound
+  2. service discovery, make sure new or terminated nodes are detected asap
+  3. load balancing, balance the traffic base on the throughput of nodes
+  4. timeout control, avoid the nodes continue to process the timed out requests
+  5. circuit breaker, load shedding, fail fast, protects the failure nodes to recover asap
+
+* advanced functionalities
+  1. authorization, make sure users can only access their own data
+  2. tracing, to understand the whole system and locate the specific problem quickly
+  3. logging, collects data and helps to backtrace problems
+  4. observability, no metrics, no optimization
+
+For any point listed above, we need a long article to describe the theory and the implementation. But for us, the developers, it’s very difficult to understand all the concepts and make it happen in our systems. Although, we can use the frameworks that have been well served busy sites. [go-zero](https://github.com/tal-tech/go-zero) is born for this purpose, especially for cloud-native microservice systems.
+
+As well, we always adhere to the idea that **prefer tools over conventions and documents**. We hope to reduce the boilerplate code as much as possible, and let developers focus on developing the business related code. For this purpose, we developed the tool  `goctl`.
+
+Let’s take the shorturl microservice as a quick example to demonstrate how to quickly create microservices by using [go-zero](https://github.com/tal-tech/go-zero). After finishing this tutorial, you’ll find that it’s so easy to write microservices!
+
+## 1. What is a shorturl service
+
+A shorturl service is that it converts a long url into a short one, by well designed algorithms.
+
+Writting this shorturl service is to demonstrate the complete flow of creating a microservice by using go-zero. But algorithms and detail implementations are quite simplified, and this shorturl service is not suitable for production use.
+
+## 2. Architecture of shorturl microservice
+
+<img src="images/shorturl-arch.png" alt="Architecture" width="800" />
+
+* In this tutorial, I only use one rpc service, transform, to demonstrate. It’s not telling that one API Gateway only can call one RPC service, it’s only for simplicity here.
+* In production, we should try best to isolate the data belongs to services, that means each service should only use its own database.
+
+## 3. goctl generated code overview
+
+All modules with green background are generated, and will be enabled when necessary. The modules with red background are handwritten code, which is typically business logic code.
+
+* API Gateway
+
+  <img src="images/api-gen.png" alt="api" width="800" />
+
+* RPC
+
+  <img src="images/rpc-gen.png" alt="rpc" width="800" />
+
+* model
+
+  <img src="images/model-gen.png" alt="model" width="800" />
+
+And now, let’s walk through the complete flow of quickly create a microservice with go-zero.
+
+## 4. Get started
+
+* install etcd, mysql, redis
+
+* install protoc-gen-go
+
+  ```
+  go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2
+  ```
+
+* install goctl
+
+  ```shell
+  GO111MODULE=on go get -u github.com/tal-tech/go-zero/tools/goctl
+  ```
+
+* create the working dir `shorturl` and `shorturl/api`
+
+* in `shorturl` dir, execute `go mod init shorturl` to initialize `go.mod`
+
+## 5. Write code for API Gateway
+
+* use goctl to generate `api/shorturl.api`
+
+  ```shell
+  goctl api -o shorturl.api
+  ```
+
+  for simplicity, the leading `info` block is removed, and the code looks like:
+
+  ```go
+  type (
+  	expandReq {
+  		shorten string `form:"shorten"`
+  	}
+  
+  	expandResp {
+  		url string `json:"url"`
+  	}
+  )
+  
+  type (
+  	shortenReq {
+  		url string `form:"url"`
+  	}
+  
+  	shortenResp {
+  		shorten string `json:"shorten"`
+  	}
+  )
+  
+  service shorturl-api {
+  	@server(
+  		handler: ShortenHandler
+  	)
+  	get /shorten(shortenReq) returns(shortenResp)
+  
+  	@server(
+  		handler: ExpandHandler
+  	)
+  	get /expand(expandReq) returns(expandResp)
+  }
+  ```
+
+  the usage of `type` keyword is the same as that in go, service is used to define get/post/head/delete api requests, described below:
+
+  * `service shorturl-api {` defines the service name
+  * `@server` defines the properties that used in server side
+  * `handler` defines the handler name
+  * `get /shorten(shortenReq) returns(shortenResp)` defines this is a GET request, the request parameters, and the response parameters
+
+* generate the code for API Gateway by using goctl
+
+  ```shell
+  goctl api go -api shorturl.api -dir .
+  ```
+
+  the generated file structure looks like:
+
+  ```Plain Text
+  .
+  ├── api
+  │   ├── etc
+  │   │   └── shorturl-api.yaml         // configuration file
+  │   ├── internal
+  │   │   ├── config
+  │   │   │   └── config.go             // configuration definition
+  │   │   ├── handler
+  │   │   │   ├── expandhandler.go      // implements expandHandler
+  │   │   │   ├── routes.go             // routes definition
+  │   │   │   └── shortenhandler.go     // implements shortenHandler
+  │   │   ├── logic
+  │   │   │   ├── expandlogic.go        // implements ExpandLogic
+  │   │   │   └── shortenlogic.go       // implements ShortenLogic
+  │   │   ├── svc
+  │   │   │   └── servicecontext.go     // defines ServiceContext
+  │   │   └── types
+  │   │       └── types.go              // defines request/response
+  │   ├── shorturl.api
+  │   └── shorturl.go                   // main entrance
+  ├── go.mod
+  └── go.sum
+  ```
+
+* start API Gateway service, listens on port 8888 by default
+
+  ```shell
+  go run shorturl.go -f etc/shorturl-api.yaml
+  ```
+
+* test API Gateway service
+
+  ```shell
+  curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
+  ```
+
+  response like:
+
+  ```http
+  HTTP/1.1 200 OK
+  Content-Type: application/json
+  Date: Thu, 27 Aug 2020 14:31:39 GMT
+  Content-Length: 15
+  
+  {"shortUrl":""}
+  ```
+
+  You can see that the API Gateway service did nothing except returned a zero value. And let’s implement the business logic in rpc service.
+
+* you can modify `internal/svc/servicecontext.go` to pass dependencies if needed
+
+* implement logic in package `internal/logic`
+
+* you can use goctl to generate code for clients base on the .api file
+
+* till now, the client engineer can work with the api, don’t need to wait for the implementation of server side
+
+## 6. Write code for transform rpc service
+
+- under directory `shorturl` create dir `rpc`
+
+* under directory `rpc/transform` create `transform.proto` file
+
+  ```shell
+  goctl rpc template -o transform.proto
+  ```
+  
+  edit the file and make the code looks like:
+
+  ```protobuf
+  syntax = "proto3";
+  
+  package transform;
+  
+  message expandReq {
+      string shorten = 1;
+  }
+  
+  message expandResp {
+      string url = 1;
+  }
+  
+  message shortenReq {
+      string url = 1;
+  }
+  
+  message shortenResp {
+      string shorten = 1;
+  }
+  
+  service transformer {
+      rpc expand(expandReq) returns(expandResp);
+      rpc shorten(shortenReq) returns(shortenResp);
+  }
+  ```
+  
+* use goctl to generate the rpc code, execute the following command in `rpc/transofrm`
+
+  ```shell
+  goctl rpc proto -src transform.proto -dir .
+  ```
+
+  the generated file structure looks like:
+
+  ```Plain Text
+  rpc/transform
+  ├── etc
+  │   └── transform.yaml              // configuration file
+  ├── internal
+  │   ├── config
+  │   │   └── config.go               // configuration definition
+  │   ├── logic
+  │   │   ├── expandlogic.go          // implements expand logic
+  │   │   └── shortenlogic.go         // implements shorten logic
+  │   ├── server
+  │   │   └── transformerserver.go    // rpc handler
+  │   └── svc
+  │       └── servicecontext.go       // defines service context, like dependencies
+  ├── pb
+  │   └── transform.pb.go
+  ├── transform.go                    // rpc main entrance
+  ├── transform.proto
+  └── transformer
+      ├── transformer.go              // defines how rpc clients call this service
+      ├── transformer_mock.go         // mock file, for test purpose
+      └── types.go                    // request/response definition
+  ```
+
+  just run it, looks like:
+
+  ```shell
+  $ go run transform.go -f etc/transform.yaml
+  Starting rpc server at 127.0.0.1:8080...
+  ```
+
+  you can change the listening port in file `etc/transform.yaml`.
+
+## 7. Modify API Gateway to call transform rpc service
+
+* modify the configuration file `shorturl-api.yaml`, add the following:
+
+  ```yaml
+  Transform:
+    Etcd:
+      Hosts:
+        - localhost:2379
+      Key: transform.rpc
+  ```
+
+  automatically discover the transform service by using etcd.
+
+* modify the file `internal/config/config.go`, add dependency on transform service:
+
+  ```go
+  type Config struct {
+  	rest.RestConf
+  	Transform zrpc.RpcClientConf     // manual code
+  }
+  ```
+
+* modify the file `internal/svc/servicecontext.go`, like below:
+
+  ```go
+  type ServiceContext struct {
+  	Config    config.Config
+  	Transformer transformer.Transformer  // manual code
+  }
+  
+  func NewServiceContext(c config.Config) *ServiceContext {
+  	return &ServiceContext{
+  		Config:    c,
+      Transformer: transformer.NewTransformer(zrpc.MustNewClient(c.Transform)), // manual code
+  	}
+  }
+  ```
+
+  passing the dependencies among services within ServiceContext.
+
+* modify the method `Expand` in the file `internal/logic/expandlogic.go`, looks like:
+
+  ```go
+  func (l *ExpandLogic) Expand(req types.ExpandReq) (*types.ExpandResp, error) {
+    // manual code start
+  	resp, err := l.svcCtx.Transformer.Expand(l.ctx, &transformer.ExpandReq{
+  		Shorten: req.Shorten,
+  	})
+  	if err != nil {
+  		return nil, err
+  	}
+  
+  	return &types.ExpandResp{
+  		Url: resp.Url,
+  	}, nil
+    // manual code stop
+  }
+  ```
+  
+  by calling the method `Expand` of `transformer` to restore the shortened url.
+  
+* modify the file `internal/logic/shortenlogic.go`, looks like:
+
+  ```go
+  func (l *ShortenLogic) Shorten(req types.ShortenReq) (*types.ShortenResp, error) {
+    // manual code start
+  	resp, err := l.svcCtx.Transformer.Shorten(l.ctx, &transformer.ShortenReq{
+  		Url: req.Url,
+  	})
+  	if err != nil {
+  		return nil, err
+  	}
+  
+  	return &types.ShortenResp{
+  		Shorten: resp.Shorten,
+  	}, nil
+    // manual code stop
+  }
+  ```
+
+  by calling the method `Shorten` of `transformer` to shorten the url.
+
+Till now, we’ve done the modification of API Gateway. All the manually added code are marked.
+
+## 8. Define the database schema, generate the code for CRUD+cache
+
+* under shorturl, create the directory `rpc/transform/model`: `mkdir -p rpc/transform/model`
+
+* under the directory rpc/transform/model create the file called shorturl.sql`, contents as below:
+
+  ```sql
+  CREATE TABLE `shorturl`
+  (
+    `shorten` varchar(255) NOT NULL COMMENT 'shorten key',
+    `url` varchar(255) NOT NULL COMMENT 'original url',
+    PRIMARY KEY(`shorten`)
+  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+  ```
+
+* create DB and table
+
+  ```sql
+  create database gozero;
+  ```
+
+  ```sql
+  source shorturl.sql;
+  ```
+
+* under the directory `rpc/transform/model` execute the following command to genrate CRUD+cache code, `-c` means using `redis cache`
+
+  ```shell
+  goctl model mysql ddl -c -src shorturl.sql -dir .
+  ```
+
+  you can also generate the code from the database url by using `datasource` subcommand instead of `ddl`
+
+  the generated file structure looks like:
+
+  ```Plain Text
+  rpc/transform/model
+  ├── shorturl.sql
+  ├── shorturlmodel.go              // CRUD+cache code
+  └── vars.go                       // const and var definition
+  ```
+
+## 9. Modify shorten/expand rpc to call crud+cache
+
+* modify `rpc/transform/etc/transform.yaml`, add the following:
+
+  ```yaml
+  DataSource: root:@tcp(localhost:3306)/gozero
+  Table: shorturl
+  Cache:
+    - Host: localhost:6379
+  ```
+
+  you can use multiple redis as cache. redis node and cluster are both supported.
+
+* modify `rpc/transform/internal/config.go`, like below:
+
+  ```go
+  type Config struct {
+  	zrpc.RpcServerConf
+  	DataSource string             // manual code
+  	Table      string             // manual code
+  	Cache      cache.CacheConf    // manual code
+  }
+  ```
+
+  added the configuration for mysql and redis cache.
+
+* modify `rpc/transform/internal/svc/servicecontext.go`, like below:
+
+  ```go
+  type ServiceContext struct {
+    c     config.Config
+    Model model.ShorturlModel   // manual code
+  }
+  
+  func NewServiceContext(c config.Config) *ServiceContext {
+  	return &ServiceContext{
+  		c:             c,
+  		Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // manual code
+  	}
+  }
+  ```
+
+* modify `rpc/transform/internal/logic/expandlogic.go`, like below:
+
+  ```go
+  func (l *ExpandLogic) Expand(in *transform.ExpandReq) (*transform.ExpandResp, error) {
+  	// manual code start
+  	res, err := l.svcCtx.Model.FindOne(in.Shorten)
+  	if err != nil {
+  		return nil, err
+  	}
+  
+  	return &transform.ExpandResp{
+  		Url: res.Url,
+  	}, nil
+  	// manual code stop
+  }
+  ```
+
+* modify `rpc/shorten/internal/logic/shortenlogic.go`, looks like:
+
+  ```go
+  func (l *ShortenLogic) Shorten(in *transform.ShortenReq) (*transform.ShortenResp, error) {
+    // manual code start, generates shorturl
+  	key := hash.Md5Hex([]byte(in.Url))[:6]
+  	_, err := l.svcCtx.Model.Insert(model.Shorturl{
+  		Shorten: key,
+  		Url:     in.Url,
+  	})
+  	if err != nil {
+  		return nil, err
+  	}
+  
+  	return &transform.ShortenResp{
+  		Shorten: key,
+  	}, nil
+    // manual code stop
+  }
+  ```
+
+  till now, we finished modifing the code, all the modified code is marked.
+
+## 10. Call shorten and expand services
+
+* call shorten api
+
+  ```shell
+  curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
+  ```
+
+  response like:
+
+  ```http
+  HTTP/1.1 200 OK
+  Content-Type: application/json
+  Date: Sat, 29 Aug 2020 10:49:49 GMT
+  Content-Length: 21
+  
+  {"shorten":"f35b2a"}
+  ```
+
+* call expand api
+
+  ```shell
+  curl -i "http://localhost:8888/expand?shorten=f35b2a"
+  ```
+
+  response like:
+
+  ```http
+  HTTP/1.1 200 OK
+  Content-Type: application/json
+  Date: Sat, 29 Aug 2020 10:51:53 GMT
+  Content-Length: 34
+  
+  {"url":"http://www.xiaoheiban.cn"}
+  ```
+
+## 11. Benchmark
+
+Because benchmarking the write requests depends on the write throughput of mysql, we only benchmarked the expand api. We read the data from mysql and cache it in redis. I chose 100 hot keys hardcoded in shorten.lua to generate the benchmark.
+
+![Benchmark](images/shorturl-benchmark.png)
+
+as shown above, in my MacBook Pro, the QPS is like 30K+.
+
+## 12. Full code
+
+[https://github.com/zeromicro/zero-examples/tree/main/shorturl](https://github.com/zeromicro/zero-examples/tree/main/shorturl)
+
+## 13. Conclusion
+
+We always adhere to **prefer tools over conventions and documents**.
+
+go-zero is not only a framework, but also a tool to simplify and standardize the building of micoservice systems.
+
+We not only keep the framework simple, but also encapsulate the complexity into the framework. And the developers are free from building the difficult and boilerplate code. Then we get the rapid development and less failure.
+
+For the generated code by goctl, lots of microservice components are included, like concurrency control, adaptive circuit breaker, adaptive load shedding, auto cache control etc. And it’s easy to deal with the busy sites.
+
+If you have any ideas that can help us to improve the productivity, tell me any time! 👏

+ 588 - 0
doc/shorturl.md

@@ -0,0 +1,588 @@
+# 快速构建高并发微服务
+
+[English](shorturl-en.md) | 简体中文
+
+## 0. 为什么说做好微服务很难
+
+要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说:
+
+* 基本功能层面
+  1. 并发控制 & 限流,避免服务被突发流量击垮
+  2. 服务注册与服务发现,确保能够动态侦测增减的节点
+  3. 负载均衡,需要根据节点承受能力分发流量
+  4. 超时控制,避免对已超时请求做无用功
+  5. 熔断设计,快速失败,保障故障节点的恢复能力
+
+* 高阶功能层面
+  1. 请求认证,确保每个用户只能访问自己的数据
+  2. 链路追踪,用于理解整个系统和快速定位特定请求的问题
+  3. 日志,用于数据收集和问题定位
+  4. 可观测性,没有度量就没有优化
+
+对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero 微服务框架](https://github.com/tal-tech/go-zero)就是为此而生。
+
+另外,我们始终秉承 **工具大于约定和文档** 的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了 `goctl` 工具。
+
+下面我通过短链微服务来演示通过 [go-zero](https://github.com/tal-tech/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
+
+## 1. 什么是短链服务
+
+短链服务就是将长的 URL 网址,通过程序计算等方式,转换为简短的网址字符串。
+
+写此短链服务是为了从整体上演示 go-zero 构建完整微服务的过程,算法和实现细节尽可能简化了,所以这不是一个高阶的短链服务。
+
+## 2. 短链微服务架构图
+
+<img src="https://gitee.com/kevwan/static/raw/master/doc/images/shorturl-arch.png" alt="架构图" width="800" />
+
+* 这里只用了 `Transform RPC` 一个微服务,并不是说 API Gateway 只能调用一个微服务,只是为了最简演示 API Gateway 如何调用 RPC 微服务而已
+* 在真正项目里要尽可能每个微服务使用自己的数据库,数据边界要清晰
+
+## 3. goctl 各层代码生成一览
+
+所有绿色背景的功能模块是自动生成的,按需激活,红色模块是需要自己写的,也就是增加下依赖,编写业务特有逻辑,各层示意图分别如下:
+
+* API Gateway
+
+  <img src="https://gitee.com/kevwan/static/raw/master/doc/images/shorturl-api.png" alt="api" width="800" />
+
+* RPC
+
+  <img src="https://gitee.com/kevwan/static/raw/master/doc/images/shorturl-rpc.png" alt="架构图" width="800" />
+
+* model
+
+  <img src="https://gitee.com/kevwan/static/raw/master/doc/images/shorturl-model.png" alt="model" width="800" />
+
+下面我们来一起完整走一遍快速构建微服务的流程,Let’s `Go`!🏃‍♂️
+
+## 4. 准备工作
+
+* 安装 etcd, mysql, redis
+
+* 安装 `protoc-gen-go`
+
+  ```shell
+  go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2
+  ```
+* 安装 `protoc`
+  ``` shell
+  wget https://github.com/protocolbuffers/protobuf/releases/download/v3.14.0/protoc-3.14.0-linux-x86_64.zip
+  unzip protoc-3.14.0-linux-x86_64.zip
+  mv bin/protoc /usr/local/bin/
+  ```
+
+* 安装 goctl 工具
+
+  ```shell
+  GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
+  ```
+
+* 创建工作目录 `shorturl` 和 `shorturl/api`
+
+`mkdir -p shorturl/api`
+
+* 在 `shorturl` 目录下执行 `go mod init shorturl` 初始化 `go.mod`
+
+  ```Plain Text
+  module shorturl
+  
+  go 1.15
+  
+  require (
+    github.com/golang/mock v1.4.3
+    github.com/golang/protobuf v1.4.2
+    github.com/tal-tech/go-zero v1.1.4
+    golang.org/x/net v0.0.0-20200707034311-ab3426394381
+    google.golang.org/grpc v1.29.1
+  )
+  ```
+
+  **注意:这里可能存在 grpc 版本依赖的问题,可以用以上配置**
+
+## 5. 编写 API Gateway 代码
+
+* 在 `shorturl/api` 目录下通过 goctl 生成 `api/shorturl.api`:
+
+  ```shell
+  goctl api -o shorturl.api
+  ```
+
+* 编辑 `api/shorturl.api`,为了简洁,去除了文件开头的 `info`,代码如下:
+
+  ```go
+  type (
+    expandReq {
+      shorten string `form:"shorten"`
+    }
+  
+    expandResp {
+      url string `json:"url"`
+    }
+  )
+  
+  type (
+    shortenReq {
+      url string `form:"url"`
+    }
+  
+    shortenResp {
+      shorten string `json:"shorten"`
+    }
+  )
+  
+  service shorturl-api {
+    @server(
+      handler: ShortenHandler
+    )
+    get /shorten(shortenReq) returns(shortenResp)
+  
+    @server(
+      handler: ExpandHandler
+    )
+    get /expand(expandReq) returns(expandResp)
+  }
+  ```
+
+  type 用法和 go 一致,service 用来定义 get/post/head/delete 等 api 请求,解释如下:
+
+  * `service shorturl-api {` 这一行定义了 service 名字
+  * `@server` 部分用来定义 server 端用到的属性
+  * `handler` 定义了服务端 handler 名字
+  * `get /shorten(shortenReq) returns(shortenResp)` 定义了 get 方法的路由、请求参数、返回参数等
+
+* 使用 goctl 生成 API Gateway 代码
+
+  ```shell
+  goctl api go -api shorturl.api -dir .
+  ```
+
+  生成的文件结构如下:
+
+  ```Plain Text
+  .
+  ├── api
+  │   ├── etc
+  │   │   └── shorturl-api.yaml         // 配置文件
+  │   ├── internal
+  │   │   ├── config
+  │   │   │   └── config.go             // 定义配置
+  │   │   ├── handler
+  │   │   │   ├── expandhandler.go      // 实现 expandHandler
+  │   │   │   ├── routes.go             // 定义路由处理
+  │   │   │   └── shortenhandler.go     // 实现 shortenHandler
+  │   │   ├── logic
+  │   │   │   ├── expandlogic.go        // 实现 ExpandLogic
+  │   │   │   └── shortenlogic.go       // 实现 ShortenLogic
+  │   │   ├── svc
+  │   │   │   └── servicecontext.go     // 定义 ServiceContext
+  │   │   └── types
+  │   │       └── types.go              // 定义请求、返回结构体
+  │   ├── shorturl.api
+  │   └── shorturl.go                   // main 入口定义
+  ├── go.mod
+  └── go.sum
+  ```
+
+* 启动 API Gateway 服务,默认侦听在 8888 端口
+
+  ```shell
+  go run shorturl.go -f etc/shorturl-api.yaml
+  ```
+
+* 测试 API Gateway 服务
+
+  ```shell
+  curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
+  ```
+
+  返回如下:
+
+  ```http
+  HTTP/1.1 200 OK
+  Content-Type: application/json
+  Date: Thu, 27 Aug 2020 14:31:39 GMT
+  Content-Length: 15
+  
+  {"shorten":""}
+  ```
+
+  可以看到我们 API Gateway 其实啥也没干,就返回了个空值,接下来我们会在 rpc 服务里实现业务逻辑
+
+* 可以修改 `internal/svc/servicecontext.go` 来传递服务依赖(如果需要)
+
+* 实现逻辑可以修改 `internal/logic` 下的对应文件
+
+* 可以通过 `goctl` 生成各种客户端语言的 api 调用代码
+
+* 到这里,你已经可以通过 goctl 生成客户端代码给客户端同学并行开发了,支持多种语言,详见文档
+
+## 6. 编写 transform rpc 服务
+
+- 在 `shorturl` 目录下创建 `rpc` 目录
+
+* 在 `rpc/transform` 目录下编写 `transform.proto` 文件
+
+  可以通过命令生成 proto 文件模板
+
+  ```shell
+  goctl rpc template -o transform.proto
+  ```
+
+  修改后文件内容如下:
+
+  ```protobuf
+  syntax = "proto3";
+  
+  package transform;
+  
+  message expandReq {
+      string shorten = 1;
+  }
+  
+  message expandResp {
+      string url = 1;
+  }
+  
+  message shortenReq {
+      string url = 1;
+  }
+  
+  message shortenResp {
+      string shorten = 1;
+  }
+  
+  service transformer {
+      rpc expand(expandReq) returns(expandResp);
+      rpc shorten(shortenReq) returns(shortenResp);
+  }
+  ```
+
+* 用 `goctl` 生成 rpc 代码,在 `rpc/transform` 目录下执行命令
+
+  ```shell
+  goctl rpc proto -src transform.proto -dir .
+  ```
+
+  **注意:不能在 GOPATH 目录下执行以上命令**
+
+  文件结构如下:
+
+  ```Plain Text
+  rpc/transform
+  ├── etc
+  │   └── transform.yaml              // 配置文件
+  ├── internal
+  │   ├── config
+  │   │   └── config.go               // 配置定义
+  │   ├── logic
+  │   │   ├── expandlogic.go          // expand 业务逻辑在这里实现
+  │   │   └── shortenlogic.go         // shorten 业务逻辑在这里实现
+  │   ├── server
+  │   │   └── transformerserver.go    // 调用入口, 不需要修改
+  │   └── svc
+  │       └── servicecontext.go       // 定义 ServiceContext,传递依赖
+  ├── pb
+  │   └── transform.pb.go
+  ├── transform.go                    // rpc 服务 main 函数
+  ├── transform.proto
+  └── transformer
+      ├── transformer.go              // 提供了外部调用方法,无需修改
+      ├── transformer_mock.go         // mock 方法,测试用
+      └── types.go                    // request/response 结构体定义
+  ```
+
+  直接可以运行,如下:
+
+  ```shell
+  $ go run transform.go -f etc/transform.yaml
+  Starting rpc server at 127.0.0.1:8080...
+  ```
+  查看服务是否注册
+  ```
+  $ETCDCTL_API=3 etcdctl get transform.rpc --prefix
+  transform.rpc/7587851893787585061
+  127.0.0.1:8080
+  ```
+  `etc/transform.yaml` 文件里可以修改侦听端口等配置
+
+## 7. 修改 API Gateway 代码调用 transform rpc 服务
+
+* 修改配置文件 `shorturl-api.yaml`,增加如下内容
+
+  ```yaml
+  Transform:
+    Etcd:
+      Hosts:
+        - localhost:2379
+      Key: transform.rpc
+  ```
+
+  通过 etcd 自动去发现可用的 transform 服务
+
+* 修改 `internal/config/config.go` 如下,增加 transform 服务依赖
+
+  ```go
+  type Config struct {
+    rest.RestConf
+    Transform zrpc.RpcClientConf     // 手动代码
+  }
+  ```
+
+* 修改 `internal/svc/servicecontext.go`,如下:
+
+  ```go
+  type ServiceContext struct {
+    Config    config.Config
+    Transformer transformer.Transformer                                          // 手动代码
+  }
+  
+  func NewServiceContext(c config.Config) *ServiceContext {
+    return &ServiceContext{
+      Config:    c,
+      Transformer: transformer.NewTransformer(zrpc.MustNewClient(c.Transform)),  // 手动代码
+    }
+  }
+  ```
+
+  通过 ServiceContext 在不同业务逻辑之间传递依赖
+
+* 修改 `internal/logic/expandlogic.go` 里的 `Expand` 方法,如下:
+
+  ```go
+  func (l *ExpandLogic) Expand(req types.ExpandReq) (types.ExpandResp, error) {
+    // 手动代码开始
+	  resp, err := l.svcCtx.Transformer.Expand(l.ctx, &transformer.ExpandReq{
+	    Shorten: req.Shorten,
+	  })
+	  if err != nil {
+	    return types.ExpandResp{}, err
+	  }
+  
+	  return types.ExpandResp{
+	    Url: resp.Url,
+	  }, nil
+	  // 手动代码结束
+  }
+  ```
+
+通过调用 `transformer` 的 `Expand` 方法实现短链恢复到 url
+
+* 修改 `internal/logic/shortenlogic.go`,如下:
+
+  ```go
+  func (l *ShortenLogic) Shorten(req types.ShortenReq) (types.ShortenResp, error) {
+    // 手动代码开始
+	  resp, err := l.svcCtx.Transformer.Shorten(l.ctx, &transformer.ShortenReq{
+		  Url: req.Url,
+	  })
+	  if err != nil {
+	    return types.ShortenResp{}, err
+	  }
+
+	  return types.ShortenResp{
+	    Shorten: resp.Shorten,
+	  }, nil
+	  // 手动代码结束
+  }
+  ```
+有的版本生成返回值可能是指针类型,需要自己调整下
+
+通过调用 `transformer` 的 `Shorten` 方法实现 url 到短链的变换
+
+至此,API Gateway 修改完成,虽然贴的代码多,但是其中修改的是很少的一部分,为了方便理解上下文,我贴了完整代码,接下来处理 CRUD+cache
+
+## 8. 定义数据库表结构,并生成 CRUD+cache 代码
+
+* shorturl 下创建 `rpc/transform/model` 目录:`mkdir -p rpc/transform/model`
+
+* 在 `rpc/transform/model` 目录下编写创建 shorturl 表的 sql 文件 `shorturl.sql`,如下:
+
+  ```sql
+  CREATE TABLE `shorturl`
+  (
+    `shorten` varchar(255) NOT NULL COMMENT 'shorten key',
+    `url` varchar(255) NOT NULL COMMENT 'original url',
+    PRIMARY KEY(`shorten`)
+  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+  ```
+
+* 创建 DB 和 table
+
+  ```sql
+  create database gozero;
+  ```
+
+  ```sql
+  source shorturl.sql;
+  ```
+
+* 在 `rpc/transform/model` 目录下执行如下命令生成 CRUD+cache 代码,`-c` 表示使用 `redis cache`
+
+  ```shell
+  goctl model mysql ddl -c -src shorturl.sql -dir .
+  ```
+
+  也可以用 `datasource` 命令代替 `ddl` 来指定数据库链接直接从 schema 生成
+
+  生成后的文件结构如下:
+
+  ```Plain Text
+  rpc/transform/model
+  ├── shorturl.sql
+  ├── shorturlmodel.go              // CRUD+cache 代码
+  └── vars.go                       // 定义常量和变量
+  ```
+
+## 9. 修改 shorten/expand rpc 代码调用 crud+cache 代码
+
+* 修改 `rpc/transform/etc/transform.yaml`,增加如下内容:
+
+  ```yaml
+  DataSource: root:password@tcp(localhost:3306)/gozero
+  Table: shorturl
+  Cache:
+    - Host: localhost:6379
+  ```
+
+  可以使用多个 redis 作为 cache,支持 redis 单点或者 redis 集群
+
+* 修改 `rpc/transform/internal/config/config.go`,如下:
+
+  ```go
+  type Config struct {
+    zrpc.RpcServerConf
+    DataSource string             // 手动代码
+    Table      string             // 手动代码
+    Cache      cache.CacheConf    // 手动代码
+  }
+  ```
+
+  增加了 mysql 和 redis cache 配置
+
+* 修改 `rpc/transform/internal/svc/servicecontext.go`,如下:
+
+  ```go
+  type ServiceContext struct {
+    c     config.Config
+    Model model.ShorturlModel   // 手动代码
+  }
+  
+  func NewServiceContext(c config.Config) *ServiceContext {
+    return &ServiceContext{
+      c:             c,
+      Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache), // 手动代码
+    }
+  }
+  ```
+
+* 修改 `rpc/transform/internal/logic/expandlogic.go`,如下:
+
+  ```go
+  func (l *ExpandLogic) Expand(in *transform.ExpandReq) (*transform.ExpandResp, error) {
+    // 手动代码开始
+    res, err := l.svcCtx.Model.FindOne(in.Shorten)
+    if err != nil {
+      return nil, err
+    }
+  
+    return &transform.ExpandResp{
+      Url: res.Url,
+    }, nil
+    // 手动代码结束
+  }
+  ```
+
+* 修改 `rpc/transform/internal/logic/shortenlogic.go`,如下:
+
+  ```go
+  func (l *ShortenLogic) Shorten(in *transform.ShortenReq) (*transform.ShortenResp, error) {
+    // 手动代码开始,生成短链接
+    key := hash.Md5Hex([]byte(in.Url))[:6]
+    _, err := l.svcCtx.Model.Insert(model.Shorturl{
+      Shorten: key,
+      Url:     in.Url,
+    })
+    if err != nil {
+      return nil, err
+    }
+  
+    return &transform.ShortenResp{
+      Shorten: key,
+    }, nil
+    // 手动代码结束
+  }
+  ```
+
+  至此代码修改完成,凡是手动修改的代码我加了标注
+
+  **注意:**
+  1. undefined cache,你需要 `import "github.com/tal-tech/go-zero/core/stores/cache"`
+  2. undefined model, sqlx, hash 等,你需要在文件中
+  
+  ```golang
+  import "shorturl/rpc/transform/model"
+  
+  import "github.com/tal-tech/go-zero/core/stores/sqlx"
+  ```
+
+## 10. 完整调用演示
+
+* shorten api 调用
+
+  ```shell
+  curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
+  ```
+
+  返回如下:
+
+  ```http
+  HTTP/1.1 200 OK
+  Content-Type: application/json
+  Date: Sat, 29 Aug 2020 10:49:49 GMT
+  Content-Length: 21
+  
+  {"shorten":"f35b2a"}
+  ```
+
+* expand api 调用
+
+  ```shell
+  curl -i "http://localhost:8888/expand?shorten=f35b2a"
+  ```
+
+  返回如下:
+
+  ```http
+  HTTP/1.1 200 OK
+  Content-Type: application/json
+  Date: Sat, 29 Aug 2020 10:51:53 GMT
+  Content-Length: 34
+  
+  {"url":"http://www.xiaoheiban.cn"}
+  ```
+
+## 11. Benchmark
+
+因为写入依赖于 mysql 的写入速度,就相当于压 mysql 了,所以压测只测试了 expand 接口,相当于从 mysql 里读取并利用缓存,shorten.lua 里随机从 db 里获取了 100 个热 key 来生成压测请求
+
+![Benchmark](https://gitee.com/kevwan/static/raw/master/doc/images/shorturl-benchmark.png)
+
+可以看出在我的 MacBook Pro 上能达到 3 万 + 的 qps。
+
+## 12. 完整代码
+
+[https://github.com/zeromicro/zero-examples/tree/main/shorturl](https://github.com/zeromicro/zero-examples/tree/main/shorturl)
+
+## 12. 总结
+
+我们一直强调 **工具大于约定和文档**。
+
+go-zero 不只是一个框架,更是一个建立在框架 + 工具基础上的,简化和规范了整个微服务构建的技术体系。
+
+我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。
+
+通过 go-zero+goctl 生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。
+
+有任何好的提升工程效率的想法,随时欢迎交流!👏
+

+ 23 - 0
doc/sql-cache.md

@@ -0,0 +1,23 @@
+# DB缓存机制
+
+## QueryRowIndex
+
+* 没有查询条件到Primary映射的缓存
+  * 通过查询条件到DB去查询行记录,然后
+    * **把Primary到行记录的缓存写到redis里**
+    * **把查询条件到Primary的映射保存到redis里**,*框架的Take方法自动做了*
+  * 可能的过期顺序
+    * 查询条件到Primary的映射缓存未过期
+      * Primary到行记录的缓存未过期
+        * 直接返回缓存行记录
+      * Primary到行记录的缓存已过期
+        * 通过Primary到DB获取行记录,并写入缓存
+          * 此时存在的问题是,查询条件到Primary的缓存可能已经快要过期了,短时间内的查询又会触发一次数据库查询
+          * 要避免这个问题,可以让**上面粗体部分**第一个过期时间略长于第二个,比如5秒
+    * 查询条件到Primary的映射缓存已过期,不管Primary到行记录的缓存是否过期
+      * 查询条件到Primary的映射会被重新获取,获取过程中会自动写入新的Primary到行记录的缓存,这样两种缓存的过期时间都是刚刚设置
+* 有查询条件到Primary映射的缓存
+  * 没有Primary到行记录的缓存
+    * 通过Primary到DB查询行记录,并写入缓存
+  * 有Primary到行记录的缓存
+    * 直接返回缓存结果

+ 289 - 0
doc/timingWheel.md

@@ -0,0 +1,289 @@
+# go-zero 如何应对海量定时/延迟任务?
+
+一个系统中存在着大量的调度任务,同时调度任务存在时间的滞后性,而大量的调度任务如果每一个都使用自己的调度器来管理任务的生命周期的话,浪费cpu的资源而且很低效。
+
+本文来介绍 `go-zero` 中 **延迟操作**,它可能让开发者调度多个任务时,**只需关注具体的业务执行函数和执行时间「立即或者延迟」**。而 **延迟操作**,通常可以采用两个方案:
+
+1. `Timer`:定时器维护一个优先队列,到时间点执行,然后把需要执行的 task 存储在 map 中
+2. `collection` 中的 `timingWheel` ,维护一个存放任务组的数组,每一个槽都维护一个存储task的双向链表。开始执行时,计时器每隔指定时间执行一个槽里面的tasks。
+
+方案2把维护task从 `优先队列 O(nlog(n))` 降到 `双向链表 O(1)`,而执行task也只要轮询一个时间点的tasks `O(N)`,不需要像优先队列,放入和删除元素 `O(nlog(n))`。
+
+我们先看看 `go-zero` 中自己对 `timingWheel` 的使用 :
+
+## cache 中的 timingWheel
+
+首先我们先来在 `collection` 的 `cache` 中关于 `timingWheel` 的使用:
+
+```go
+timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) {
+  key, ok := k.(string)
+  if !ok {
+    return
+  }
+  cache.Del(key)
+})
+if err != nil {
+  return nil, err
+}
+
+cache.timingWheel = timingWheel
+```
+
+这是 `cache` 初始化中也同时初始化 `timingWheel` 做key的过期处理,参数依次代表:
+
+- `interval`:时间划分刻度
+- `numSlots`:时间槽
+- `execute`:时间点执行函数
+
+在 `cache` 中执行函数则是 **删除过期key**,而这个过期则由 `timingWheel` 来控制推进时间。
+
+**接下来,就通过 `cache` 对 `timingWheel` 的使用来认识。**
+
+### 初始化
+
+```go
+// 真正做初始化
+func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execute, ticker timex.Ticker) (
+	*TimingWheel, error) {
+	tw := &TimingWheel{
+		interval:      interval,                     // 单个时间格时间间隔
+		ticker:        ticker,                       // 定时器,做时间推动,以interval为单位推进
+		slots:         make([]*list.List, numSlots), // 时间轮
+		timers:        NewSafeMap(),                 // 存储task{key, value}的map [执行execute所需要的参数]
+		tickedPos:     numSlots - 1,                 // at previous virtual circle
+		execute:       execute,                      // 执行函数
+		numSlots:      numSlots,                     // 初始化 slots num
+		setChannel:    make(chan timingEntry),       // 以下几个channel是做task传递的
+		moveChannel:   make(chan baseEntry),
+		removeChannel: make(chan interface{}),
+		drainChannel:  make(chan func(key, value interface{})),
+		stopChannel:   make(chan lang.PlaceholderType),
+	}
+	// 把 slot 中存储的 list 全部准备好
+	tw.initSlots()
+	// 开启异步协程,使用 channel 来做task通信和传递
+	go tw.run()
+
+	return tw, nil
+}
+```
+
+![](https://gitee.com/kevwan/static/raw/master/doc/images/timewheel-struct.png)
+
+以上比较直观展示 `timingWheel` 的 **“时间轮”**,后面会围绕这张图解释其中推进的细节。
+
+ `go tw.run()` 开一个协程做时间推动:
+
+```go
+func (tw *TimingWheel) run() {
+	for {
+		select {
+      // 定时器做时间推动 -> scanAndRunTasks()
+		case <-tw.ticker.Chan():
+			tw.onTick()
+      // add task 会往 setChannel 输入task
+		case task := <-tw.setChannel:
+			tw.setTask(&task)
+		...
+		}
+	}
+}
+```
+
+可以看出,在初始化的时候就开始了 `timer` 执行,并以`internal`时间段转动,然后底层不停的获取来自 `slot` 中的  `list` 的task,交给 `execute` 执行。
+
+![](https://gitee.com/kevwan/static/raw/master/doc/images/timewheel-run.png)
+
+### Task Operation
+
+紧接着就是设置 `cache key` :
+
+```go
+func (c *Cache) Set(key string, value interface{}) {
+	c.lock.Lock()
+	_, ok := c.data[key]
+	c.data[key] = value
+	c.lruCache.add(key)
+	c.lock.Unlock()
+
+	expiry := c.unstableExpiry.AroundDuration(c.expire)
+	if ok {
+		c.timingWheel.MoveTimer(key, expiry)
+	} else {
+		c.timingWheel.SetTimer(key, value, expiry)
+	}
+}
+```
+
+1. 先看在 `data map` 中有没有存在这个key
+2. 存在,则更新 `expire`   -> `MoveTimer()`
+3. 第一次设置key   ->   `SetTimer()`
+
+所以对于 `timingWheel` 的使用上就清晰了,开发者根据需求可以 `add` 或是 `update`。
+
+同时我们跟源码进去会发现:`SetTimer() MoveTimer()` 都是将task输送到channel,由 `run()` 中开启的协程不断取出 `channel` 的task操作。
+
+> `SetTimer() -> setTask()`:
+>
+> - not exist task:`getPostion -> pushBack to list -> setPosition`
+> - exist task:`get from timers -> moveTask() `
+>
+> `MoveTimer() -> moveTask()`
+
+由上面的调用链,有一个都会调用的函数:`moveTask()`
+
+```go
+func (tw *TimingWheel) moveTask(task baseEntry) {
+	// timers: Map => 通过key获取 [positionEntry「pos, task」]
+	val, ok := tw.timers.Get(task.key)
+	if !ok {
+		return
+	}
+
+	timer := val.(*positionEntry)
+  	// {delay < interval} => 延迟时间比一个时间格间隔还小,没有更小的刻度,说明任务应该立即执行
+	if task.delay < tw.interval {
+		threading.GoSafe(func() {
+			tw.execute(timer.item.key, timer.item.value)
+		})
+		return
+	}
+	// 如果 > interval,则通过 延迟时间delay 计算其出时间轮中的 new pos, circle
+	pos, circle := tw.getPositionAndCircle(task.delay)
+	if pos >= timer.pos {
+		timer.item.circle = circle
+                // 记录前后的移动offset。为了后面过程重新入队
+		timer.item.diff = pos - timer.pos
+	} else if circle > 0 {
+		// 转移到下一层,将 circle 转换为 diff 一部分
+		circle--
+		timer.item.circle = circle
+		// 因为是一个数组,要加上 numSlots [也就是相当于要走到下一层]
+		timer.item.diff = tw.numSlots + pos - timer.pos
+	} else {
+		// 如果 offset 提前了,此时 task 也还在第一层
+		// 标记删除老的 task,并重新入队,等待被执行
+		timer.item.removed = true
+		newItem := &timingEntry{
+			baseEntry: task,
+			value:     timer.item.value,
+		}
+		tw.slots[pos].PushBack(newItem)
+		tw.setTimerPosition(pos, newItem)
+	}
+}
+```
+
+以上过程有以下几种情况:
+
+- `delay < internal`:因为 < 单个时间精度,表示这个任务已经过期,需要马上执行
+- 针对改变的 `delay`:
+  - `new >= old`:`<newPos, newCircle, diff>`
+  - `newCircle > 0`:计算diff,并将 circle 转换为 下一层,故diff + numslots
+  - 如果只是单纯延迟时间缩短,则将老的task标记删除,重新加入list,等待下一轮loop被execute
+
+### Execute
+
+之前在初始化中,`run()` 中定时器的不断推进,推进的过程主要就是把 list中的 task 传给执行的 `execute func`。我们从定时器的执行开始看:
+
+```go
+// 定时器 「每隔 internal 会执行一次」
+func (tw *TimingWheel) onTick() {
+        // 每次执行更新一下当前执行 tick 位置
+	tw.tickedPos = (tw.tickedPos + 1) % tw.numSlots
+        // 获取此时 tick位置 中的存储task的双向链表
+	l := tw.slots[tw.tickedPos]
+	tw.scanAndRunTasks(l)
+}
+```
+
+紧接着是如何去执行 `execute`:
+
+```go
+func (tw *TimingWheel) scanAndRunTasks(l *list.List) {
+	// 存储目前需要执行的task{key, value}  [execute所需要的参数,依次传递给execute执行]
+	var tasks []timingTask
+
+	for e := l.Front(); e != nil; {
+		task := e.Value.(*timingEntry)
+                // 标记删除,在 scan 中做真正的删除 「删除map的data」
+		if task.removed {
+			next := e.Next()
+			l.Remove(e)
+			tw.timers.Del(task.key)
+			e = next
+			continue
+		} else if task.circle > 0 {
+			// 当前执行点已经过期,但是同时不在第一层,所以当前层即然已经完成了,就会降到下一层
+                        // 但是并没有修改 pos
+			task.circle--
+			e = e.Next()
+			continue
+		} else if task.diff > 0 {
+			// 因为之前已经标注了diff,需要再进入队列
+			next := e.Next()
+			l.Remove(e)
+			pos := (tw.tickedPos + task.diff) % tw.numSlots
+			tw.slots[pos].PushBack(task)
+			tw.setTimerPosition(pos, task)
+			task.diff = 0
+			e = next
+			continue
+		}
+		// 以上的情况都是不能执行的情况,能够执行的会被加入tasks中
+		tasks = append(tasks, timingTask{
+			key:   task.key,
+			value: task.value,
+		})
+		next := e.Next()
+		l.Remove(e)
+		tw.timers.Del(task.key)
+		e = next
+	}
+	// for range tasks,然后把每个 task->execute 执行即可
+	tw.runTasks(tasks)
+}
+```
+
+具体的分支情况在注释中说明了,在看的时候可以和前面的 `moveTask()` 结合起来,其中 `circle` 下降,`diff` 的计算是关联两个函数的重点。
+
+至于 `diff` 计算就涉及到 `pos, circle` 的计算:
+
+```go
+// interval: 4min, d: 60min, numSlots: 16, tickedPos = 15
+// step = 15, pos = 14, circle = 0
+func (tw *TimingWheel) getPositionAndCircle(d time.Duration) (pos int, circle int) {
+	steps := int(d / tw.interval)
+	pos = (tw.tickedPos + steps) % tw.numSlots
+	circle = (steps - 1) / tw.numSlots
+	return
+}
+```
+
+> 上面的过程可以简化成下面:
+>
+> ```go
+> steps = d / interval
+> pos = step % numSlots - 1
+> circle = (step - 1) / numSlots
+> ```
+
+## 总结
+
+1. `timingWheel` 靠定时器推动,时间前进的同时会取出**当前时间格**中 `list`「双向链表」的task,传递到 `execute` 中执行。因为是是靠 `internal` 固定时间刻度推进,可能就会出现:一个 60s 的task,`internal = 1s`,这样就会空跑59次loop。
+
+2. 而在扩展时间上,采取 `circle` 分层,这样就可以不断复用原有的 `numSlots` ,因为定时器在不断 `loop`,而执行可以把上层的 `slot` 下降到下层,在不断 `loop` 中就可以执行到上层的task。这样的设计可以在不创造额外的数据结构,突破长时间的限制。
+
+> 同时在 `go-zero` 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。
+
+同时欢迎大家使用 `go-zero` 并加入我们,[项目地址](https://github.com/tal-tech/go-zero)
+
+<img src="https://gitee.com/kevwan/static/raw/master/doc/images/wechat.jpg" alt="wechat" width="300" />
+
+
+## 参考资料
+
+- [go-zero](https://github.com/tal-tech/go-zero)
+- [go-zero 文档](https://www.yuque.com/tal-tech/go-zero)
+- [go-zero中 collection.Cache](https://github.com/tal-tech/zero-doc/blob/main/doc/collection.md)

+ 596 - 0
doc/zrpc.md

@@ -0,0 +1,596 @@
+
+
+# 企业级RPC框架zRPC
+
+近期比较火的开源项目[go-zero](https://github.com/tal-tech/go-zero)是一个集成了各种工程实践的包含了Web和RPC协议的功能完善的微服务框架,今天我们就一起来分析一下其中的RPC部分[zRPC](https://github.com/tal-tech/go-zero/tree/master/zrpc)。
+
+zRPC底层依赖gRPC,内置了服务注册、负载均衡、拦截器等模块,其中还包括自适应降载,自适应熔断,限流等微服务治理方案,是一个简单易用的可直接用于生产的企业级RPC框架。
+
+### zRPC初探
+
+zRPC支持直连和基于etcd服务发现两种方式,我们以基于etcd做服务发现为例演示zRPC的基本使用:
+
+##### 配置
+
+创建hello.yaml配置文件,配置如下:
+
+```yaml
+Name: hello.rpc           // 服务名
+ListenOn: 127.0.0.1:9090  // 服务监听地址
+Etcd:
+  Hosts:
+    - 127.0.0.1:2379      // etcd服务地址
+  Key: hello.rpc          // 服务注册key
+```
+
+##### 创建proto文件
+
+创建hello.proto文件,并生成对应的go代码
+
+```protobuf
+syntax = "proto3";
+
+package pb;
+
+service Greeter {
+  rpc SayHello (HelloRequest) returns (HelloReply) {}
+}
+
+message HelloRequest {
+  string name = 1;
+}
+
+message HelloReply {
+  string message = 1;
+}
+```
+
+生成go代码
+
+```go
+protoc --go_out=plugins=grpc:. hello.proto
+```
+
+##### Server端
+
+```go
+package main
+
+import (
+    "context"
+    "flag"
+    "log"
+
+    "example/zrpc/pb"
+
+    "github.com/tal-tech/go-zero/core/conf"
+    "github.com/tal-tech/go-zero/zrpc"
+    "google.golang.org/grpc"
+)
+
+type Config struct {
+    zrpc.RpcServerConf
+}
+
+var cfgFile = flag.String("f", "./hello.yaml", "cfg file")
+
+func main() {
+    flag.Parse()
+
+    var cfg Config
+    conf.MustLoad(*cfgFile, &cfg)
+
+    srv, err := zrpc.NewServer(cfg.RpcServerConf, func(s *grpc.Server) {
+        pb.RegisterGreeterServer(s, &Hello{})
+    })
+    if err != nil {
+        log.Fatal(err)
+    }
+    srv.Start()
+}
+
+type Hello struct{}
+
+func (h *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
+    return &pb.HelloReply{Message: "hello " + in.Name}, nil
+}
+```
+
+##### Client端
+
+```go
+package main
+
+import (
+    "context"
+    "log"
+
+    "example/zrpc/pb"
+
+    "github.com/tal-tech/go-zero/core/discov"
+    "github.com/tal-tech/go-zero/zrpc"
+)
+
+func main() {
+    client := zrpc.MustNewClient(zrpc.RpcClientConf{
+        Etcd: discov.EtcdConf{
+            Hosts: []string{"127.0.0.1:2379"},
+            Key:   "hello.rpc",
+        },
+    })
+
+    conn := client.Conn()
+    hello := pb.NewGreeterClient(conn)
+    reply, err := hello.SayHello(context.Background(), &pb.HelloRequest{Name: "go-zero"})
+    if err != nil {
+        log.Fatal(err)
+    }
+    log.Println(reply.Message)
+}
+```
+
+启动服务,查看服务是否注册:
+
+```go
+ETCDCTL_API=3 etcdctl get hello.rpc --prefix
+```
+
+显示服务已经注册:
+
+```go
+hello.rpc/7587849401504590084
+127.0.0.1:9090
+```
+
+运行客户端即可看到输出:
+
+```go
+hello go-zero
+```
+
+这个例子演示了zRPC的基本使用,可以看到通过zRPC构建RPC服务非常简单,只需要很少的几行代码,接下来我们继续进行探索
+
+### zRPC原理分析
+
+下图展示zRPC的架构图和主要组成部分
+
+![zrpc](https://gitee.com/kevwan/static/raw/master/doc/images/zrpc.png)
+
+zRPC主要有以下几个模块组成:
+
+- discov: 服务发现模块,基于etcd实现服务发现功能
+- resolver: 服务注册模块,实现了gRPC的resolver.Builder接口并注册到gRPC
+- interceptor: 拦截器,对请求和响应进行拦截处理
+- balancer: 负载均衡模块,实现了p2c负载均衡算法,并注册到gRPC 
+- client: zRPC客户端,负责发起请求
+- server: zRPC服务端,负责处理请求 
+
+这里介绍了zRPC的主要组成模块和每个模块的主要功能,其中resolver和balancer模块实现了gRPC开放的接口,实现了自定义的resolver和balancer,拦截器模块是整个zRPC的功能重点,自适应降载、自适应熔断、prometheus服务指标收集等功能都在这里实现
+
+
+
+### Interceptor模块
+
+gRPC提供了拦截器功能,主要是对请求前后进行额外处理的拦截操作,其中拦截器包含客户端拦截器和服务端拦截器,又分为一元(Unary)拦截器和流(Stream)拦截器,这里我们主要讲解一元拦截器,流拦截器同理。
+
+![interceptor](https://gitee.com/kevwan/static/raw/master/doc/images/interceptor.png)
+
+客户端拦截器定义如下:
+
+```go
+type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
+```
+
+其中method为方法名,req,reply分别为请求和响应参数,cc为客户端连接对象,invoker参数是真正执行rpc方法的handler其实在拦截器中被调用执行
+
+服务端拦截器定义如下:
+
+```go
+type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
+```
+
+其中req为请求参数,info中包含了请求方法属性,handler为对server端方法的包装,也是在拦截器中被调用执行
+
+zRPC中内置了丰富的拦截器,其中包括自适应降载、自适应熔断、权限验证、prometheus指标收集等等,由于拦截器较多,篇幅有限没法所有的拦截器给大家一一解析,这里我们主要分析两个,自适应熔断和prometheus服务监控指标收集:
+
+#### 内置拦截器分析
+
+##### 自适应熔断(breaker)
+
+当客户端向服务端发起请求,客户端会记录服务端返回的错误,当错误达到一定的比例,客户端会自行的进行熔断处理,丢弃掉一定比例的请求以保护下游依赖,且可以自动恢复。zRPC中自适应熔断遵循[《Google SRE》](https://landing.google.com/sre/sre-book/chapters/handling-overload)中过载保护策略,算法如下:
+
+<img src="/Users/zhoushuguang/Documents/工作/go-zero文档/overload.png" alt="overload" style="zoom:120%;" />
+
+requests: 总请求数量
+
+accepts: 正常请求数量 
+
+K: 倍值 (Google SRE推荐值为2)
+
+可以通过修改K的值来修改熔断发生的激进程度,降低K的值会使得自适应熔断算法更加激进,增加K的值则自适应熔断算法变得不再那么激进
+
+[熔断拦截器](https://github.com/tal-tech/go-zero/blob/master/zrpc/internal/clientinterceptors/breakerinterceptor.go)定义如下:
+
+```go
+func BreakerInterceptor(ctx context.Context, method string, req, reply interface{},
+	cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+  // target + 方法名
+	breakerName := path.Join(cc.Target(), method)
+	return breaker.DoWithAcceptable(breakerName, func() error {
+    // 真正执行调用
+		return invoker(ctx, method, req, reply, cc, opts...)
+	}, codes.Acceptable)
+}
+```
+
+accept方法实现了Google SRE过载保护算法,判断否进行熔断
+
+```go
+func (b *googleBreaker) accept() error {
+	 // accepts为正常请求数,total为总请求数
+   accepts, total := b.history()
+   weightedAccepts := b.k * float64(accepts)
+   // 算法实现
+   dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
+   if dropRatio <= 0 {
+      return nil
+   }
+	 // 是否超过比例
+   if b.proba.TrueOnProba(dropRatio) {
+      return ErrServiceUnavailable
+   }
+
+   return nil
+}
+```
+
+doReq方法首先判断是否熔断,满足条件直接返回error(circuit breaker is open),不满足条件则对请求数进行累加
+
+```go
+func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
+   if err := b.accept(); err != nil {
+      if fallback != nil {
+         return fallback(err)
+      } else {
+         return err
+      }
+   }
+
+   defer func() {
+      if e := recover(); e != nil {
+         b.markFailure()
+         panic(e)
+      }
+   }()
+	
+   // 此处执行RPC请求
+   err := req()
+   // 正常请求total和accepts都会加1
+   if acceptable(err) {
+      b.markSuccess()
+   } else {
+     // 请求失败只有total会加1
+      b.markFailure()
+   }
+
+   return err
+}
+```
+
+##### prometheus指标收集
+
+服务监控是了解服务当前运行状态以及变化趋势的重要手段,监控依赖于服务指标的收集,通过prometheus进行监控指标的收集是业界主流方案,zRPC中也采用了prometheus来进行指标的收集
+
+[prometheus拦截器](https://github.com/tal-tech/go-zero/blob/master/zrpc/internal/serverinterceptors/prometheusinterceptor.go)定义如下:
+
+这个拦截器主要是对服务的监控指标进行收集,这里主要是对RPC方法的耗时和调用错误进行收集,这里主要使用了Prometheus的Histogram和Counter数据类型
+
+```go
+func UnaryPrometheusInterceptor() grpc.UnaryServerInterceptor {
+	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (
+		interface{}, error) {
+    // 执行前记录一个时间
+		startTime := timex.Now()
+		resp, err := handler(ctx, req)
+    // 执行后通过Since算出执行该调用的耗时
+		metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), info.FullMethod)
+    // 方法对应的错误码
+		metricServerReqCodeTotal.Inc(info.FullMethod, strconv.Itoa(int(status.Code(err))))
+		return resp, err
+	}
+}
+```
+
+#### 添加自定义拦截器
+
+除了内置了丰富的拦截器之外,zRPC同时支持添加自定义拦截器
+
+Client端通过AddInterceptor方法添加一元拦截器:
+
+```go
+func (rc *RpcClient) AddInterceptor(interceptor grpc.UnaryClientInterceptor) {
+	rc.client.AddInterceptor(interceptor)
+}
+```
+
+Server端通过AddUnaryInterceptors方法添加一元拦截器:
+
+```go
+func (rs *RpcServer) AddUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) {
+	rs.server.AddUnaryInterceptors(interceptors...)
+}
+```
+
+### resolver模块
+
+zRPC服务注册架构图:
+
+![resolver](https://gitee.com/kevwan/static/raw/master/doc/images/resolver.png)
+
+zRPC中自定义了resolver模块,用来实现服务的注册功能。zRPC底层依赖gRPC,在gRPC中要想自定义resolver需要实现resolver.Builder接口:
+
+```go
+type Builder interface {
+	Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
+	Scheme() string
+}
+```
+
+其中Build方法返回Resolver,Resolver定义如下:
+
+```go
+type Resolver interface {
+	ResolveNow(ResolveNowOptions)
+	Close()
+}
+```
+
+在zRPC中定义了两种resolver,direct和discov,这里我们主要分析基于etcd做服务发现的discov,自定义的resolver需要通过gRPC提供了Register方法进行注册代码如下:
+
+```go
+func RegisterResolver() {
+	resolver.Register(&dirBuilder)
+	resolver.Register(&disBuilder)
+}
+```
+
+当我们启动我们的zRPC Server的时候,调用Start方法,会像etcd中注册对应的服务地址:
+
+```go
+func (ags keepAliveServer) Start(fn RegisterFn) error {
+  // 注册服务地址
+	if err := ags.registerEtcd(); err != nil {
+		return err
+	}
+	// 启动服务
+	return ags.Server.Start(fn)
+}
+```
+
+当我们启动zRPC客户端的时候,在gRPC内部会调用我们自定义resolver的Build方法,zRPC通过在Build方法内调用执行了resolver.ClientConn的UpdateState方法,该方法会把服务地址注册到gRPC客户端内部:
+
+```go
+func (d *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
+	resolver.Resolver, error) {
+	hosts := strings.FieldsFunc(target.Authority, func(r rune) bool {
+		return r == EndpointSepChar
+	})
+  // 服务发现
+	sub, err := discov.NewSubscriber(hosts, target.Endpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	update := func() {
+		var addrs []resolver.Address
+		for _, val := range subset(sub.Values(), subsetSize) {
+			addrs = append(addrs, resolver.Address{
+				Addr: val,
+			})
+		}
+    // 向gRPC注册服务地址
+		cc.UpdateState(resolver.State{
+			Addresses: addrs,
+		})
+	}
+  // 监听
+	sub.AddListener(update)
+	update()
+	// 返回自定义的resolver.Resolver
+	return &nopResolver{cc: cc}, nil
+}
+```
+
+在discov中,通过调用load方法从etcd中获取指定服务的所有地址:
+
+```go
+func (c *cluster) load(cli EtcdClient, key string) {
+	var resp *clientv3.GetResponse
+	for {
+		var err error
+		ctx, cancel := context.WithTimeout(c.context(cli), RequestTimeout)
+    // 从etcd中获取指定服务的所有地址
+		resp, err = cli.Get(ctx, makeKeyPrefix(key), clientv3.WithPrefix())
+		cancel()
+		if err == nil {
+			break
+		}
+
+		logx.Error(err)
+		time.Sleep(coolDownInterval)
+	}
+
+	var kvs []KV
+	c.lock.Lock()
+	for _, ev := range resp.Kvs {
+		kvs = append(kvs, KV{
+			Key: string(ev.Key),
+			Val: string(ev.Value),
+		})
+	}
+	c.lock.Unlock()
+
+	c.handleChanges(key, kvs)
+}
+```
+
+并通过watch监听服务地址的变化:
+
+```go
+func (c *cluster) watch(cli EtcdClient, key string) {
+	rch := cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix())
+	for {
+		select {
+		case wresp, ok := <-rch:
+			if !ok {
+				logx.Error("etcd monitor chan has been closed")
+				return
+			}
+			if wresp.Canceled {
+				logx.Error("etcd monitor chan has been canceled")
+				return
+			}
+			if wresp.Err() != nil {
+				logx.Error(fmt.Sprintf("etcd monitor chan error: %v", wresp.Err()))
+				return
+			}
+			// 监听变化通知更新
+			c.handleWatchEvents(key, wresp.Events)
+		case <-c.done:
+			return
+		}
+	}
+}
+```
+
+这部分主要介绍了zRPC中是如何自定义的resolver,以及基于etcd的服务发现原理,通过这部分的介绍大家可以了解到zRPC内部服务注册发现的原理,源代码比较多只是粗略的从整个流程上进行了分析,如果大家对zRPC的源码比较感兴趣可以自行进行学习
+
+### balancer模块
+
+负载均衡原理图:
+
+<img src="https://gitee.com/kevwan/static/raw/master/doc/images/balancer.png" alt="balancer" style="zoom:45%;" />
+
+避免过载是负载均衡策略的一个重要指标,好的负载均衡算法能很好的平衡服务端资源。常用的负载均衡算法有轮训、随机、Hash、加权轮训等。但为了应对各种复杂的场景,简单的负载均衡算法往往表现的不够好,比如轮训算法当服务响应时间变长就很容易导致负载不再平衡, 因此zRPC中自定义了默认负载均衡算法P2C(Power of Two Choices),和resolver类似,要想自定义balancer也需要实现gRPC定义的balancer.Builder接口,由于和resolver类似这里不再带大家一起分析如何自定义balancer,感兴趣的朋友可以查看gRPC相关的文档来进行学习
+
+注意,zRPC是在客户端进行负载均衡,常见的还有通过nginx中间代理的方式
+
+zRPC框架中默认的负载均衡算法为P2C,该算法的主要思想是:
+
+1. 从可用节点列表中做两次随机选择操作,得到节点A、B
+2. 比较A、B两个节点,选出负载最低的节点作为被选中的节点
+
+伪代码如下:
+
+<img src="https://gitee.com/kevwan/static/raw/master/doc/images/random_pseudo.png" alt="random_pseudo" style="zoom:80%;" />
+
+主要算法逻辑在Pick方法中实现:
+
+```go
+func (p *p2cPicker) Pick(ctx context.Context, info balancer.PickInfo) (
+	conn balancer.SubConn, done func(balancer.DoneInfo), err error) {
+	p.lock.Lock()
+	defer p.lock.Unlock()
+
+	var chosen *subConn
+	switch len(p.conns) {
+	case 0:
+		return nil, nil, balancer.ErrNoSubConnAvailable
+	case 1:
+		chosen = p.choose(p.conns[0], nil)
+	case 2:
+		chosen = p.choose(p.conns[0], p.conns[1])
+	default:
+		var node1, node2 *subConn
+		for i := 0; i < pickTimes; i++ {
+      // 随机数
+			a := p.r.Intn(len(p.conns))
+			b := p.r.Intn(len(p.conns) - 1)
+			if b >= a {
+				b++
+			}
+      // 随机获取所有节点中的两个节点
+			node1 = p.conns[a]
+			node2 = p.conns[b]
+      // 效验节点是否健康
+			if node1.healthy() && node2.healthy() {
+				break
+			}
+		}
+		// 选择其中一个节点
+		chosen = p.choose(node1, node2)
+	}
+
+	atomic.AddInt64(&chosen.inflight, 1)
+	atomic.AddInt64(&chosen.requests, 1)
+	return chosen.conn, p.buildDoneFunc(chosen), nil
+}
+```
+
+choose方法对随机选择出来的节点进行负载比较从而最终确定选择哪个节点
+
+```go
+func (p *p2cPicker) choose(c1, c2 *subConn) *subConn {
+	start := int64(timex.Now())
+	if c2 == nil {
+		atomic.StoreInt64(&c1.pick, start)
+		return c1
+	}
+
+	if c1.load() > c2.load() {
+		c1, c2 = c2, c1
+	}
+
+	pick := atomic.LoadInt64(&c2.pick)
+	if start-pick > forcePick && atomic.CompareAndSwapInt64(&c2.pick, pick, start) {
+		return c2
+	} else {
+		atomic.StoreInt64(&c1.pick, start)
+		return c1
+	}
+}
+```
+
+上面主要介绍了zRPC默认负载均衡算法的设计思想和代码实现,那自定义的balancer是如何注册到gRPC的呢,resolver提供了Register方法来进行注册,同样balancer也提供了Register方法来进行注册:
+
+```go
+func init() {
+	balancer.Register(newBuilder())
+}
+
+func newBuilder() balancer.Builder {
+	return base.NewBalancerBuilder(Name, new(p2cPickerBuilder))
+}
+```
+
+注册balancer之后gRPC怎么知道使用哪个balancer呢?这里我们需要使用配置项进行配置,在NewClient的时候通过grpc.WithBalancerName方法进行配置:
+
+```go
+func NewClient(target string, opts ...ClientOption) (*client, error) {
+	var cli client
+	opts = append(opts, WithDialOption(grpc.WithBalancerName(p2c.Name)))
+	if err := cli.dial(target, opts...); err != nil {
+		return nil, err
+	}
+
+	return &cli, nil
+}
+```
+
+这部分主要介绍了zRPC中内中的负载均衡算法的实现原理以及具体的实现方式,之后介绍了zRPC是如何注册自定义的balancer以及如何选择自定义的balancer,通过这部分大家应该对负载均衡有了更进一步的认识
+
+### 总结
+
+首先,介绍了zRPC的基本使用方法,可以看到zRPC使用非常简单,只需要少数几行代码就可以构建高性能和自带服务治理能力的RPC服务,当然这里没有面面俱到的介绍zRPC的基本使用,大家可以查看相关文档进行学习
+
+接着,介绍了zRPC的几个重要组成模块以及其实现原理,并分析了部分源码。拦截器模块是整个zRPC的重点,其中内置了丰富的功能,像熔断、监控、降载等等也是构建高可用微服务必不可少的。resolver和balancer模块自定义了gRPC的resolver和balancer,通过该部分可以了解到整个服务注册与发现的原理以及如何构建自己的服务发现系统,同时自定义负载均衡算法也变得不再神秘
+
+最后,zRPC是一个经历过各种工程实践的RPC框架,不论是想要用于生产还是学习其中的设计模式都是一个不可多得的开源项目。希望通过这篇文章的介绍大家能够进一步了解zRPC
+
+
+
+### 
+
+
+
+

+ 191 - 0
docs/.vuepress/config.js

@@ -0,0 +1,191 @@
+const moment = require("moment");
+module.exports = {
+    title: "go-zero",
+    description: "集成各种工程实践的 WEB 和 RPC 框架",
+    head: [
+        ["link", { rel: "icon", href: "/logo.png" }],
+        [
+            "meta",
+            {
+                name: "keywords",
+                content: "Go,golang,zero,go-zero,micro service,gRPC",
+            },
+        ],
+    ],
+
+    markdown: {
+        lineNumbers: true, // 代码块显示行号
+    },
+    themeConfig: {
+        nav: [
+            {
+                text: "首页",
+                link: "/",
+            },
+            {
+                text: "框架文档",
+                link: "/zero/",
+            },
+            {
+                text: "go-zero",link: "https://github.com/tal-tech/go-zero",
+            },
+            {
+                text: "CDS",link: "https://github.com/tal-tech/cds",
+            },
+        ],
+        docsDir: "docs",
+        docsBranch: "main",
+        editLinks: true,
+        editLinkText: "在github.com上编辑此页",
+        sidebar: {
+            '/zero/': getGoZeroSidebar('简介', '开发指南', 'core', 'rest', 'zrpc', 'goctl', '源码解读', 'awesome'),
+        },
+        sidebarDepth: 2,
+        lastUpdated: "上次更新",
+        serviceWorker: {
+            updatePopup: {
+                message: "发现新内容可用",
+                buttonText: "刷新",
+            },
+        },
+    },
+    plugins: [
+        [
+            "@vuepress/last-updated",
+            {
+                transformer: (timestamp, lang) => {
+                    // 不要忘了安装 moment
+                    const moment = require("moment");
+                    moment.locale("zh-cn");
+                    return moment(timestamp).format("YYYY-MM-DD HH:mm:ss");
+                },
+
+                dateOptions: {
+                    hours12: true,
+                },
+            },
+        ],
+        [
+            '@vssue/vuepress-plugin-vssue',
+            {
+                platform: 'github', // v3的platform是github,v4的是github-v4
+                locale: 'zh', // 语言
+                // 其他的 Vssue 配置
+                owner: 'tal-tech', // github账户名
+                repo: 'zero-doc', // github一个项目的名称
+                clientId: '1252229e5b787945392d',   // 注册的Client ID
+                clientSecret: 'c424d19a9cb758d0800f644376b0b4dd24828c94',   // 注册的Client Secret
+                autoCreateIssue: false   // 自动创建评论,默认是false,最好开启,这样首次进入页面的时候就不用去点击创建评论的按钮了。
+            },
+        ],
+        "@vuepress/back-to-top",
+        "@vuepress/active-header-links",
+        "@vuepress/medium-zoom",
+        "@vuepress/nprogress",
+    ],
+};
+
+// go-zero main document file
+function getGoZeroSidebar(A, B, C, D, E, F, G, H) {
+    return [
+        {
+            title: A,
+            collapsable: false,
+            children: [
+                ['', 'go-zero 简介'],
+                'bookstore',
+            ]
+        },
+        {
+            title: B,
+            collapsable: false,
+            children: [
+                '开发指南',
+                '快速开始',
+                '功能描述',
+                ['HTTP Middleware', 'HTTP Middleware'],
+                '自定义错误返回',
+                '创建API服务',
+                'model生成',
+                '用户注册',
+                '用户登陆',
+                'JWT生成',
+                '获取用户信息(JWT鉴权)',
+                '获取用户信息(header)',
+                '中间件使用',
+                'rpc调用',
+            ]
+        },
+        {
+            title: C,
+            collapsable: false,
+            children: [
+                ['logx', 'logx'],
+                'bloom',
+                'executors',
+                'streamapi-fx',
+                ['timingWheel', 'timingWheel'],
+                'periodlimit',
+                'tokenlimit',
+                ['store all', 'store all'],
+                'store mysql',
+                'redis-lock',
+            ]
+        },
+        {
+            title: D,
+            collapsable: false,
+            children: [
+                ['server', 'server'],
+                'JWT鉴权中间件',
+                '自适应融断中间件',
+                '验签中间件',
+                'TraceHandler',
+                'params',
+                'router',
+                'tokenparser',
+                'rest engine',
+            ]
+        },
+        {
+            title: E,
+            collapsable: false,
+            children: [
+                ['zrpc简介', '简介'],
+                ['zrpc目录结构', '目录结构'],
+                '参数配置客户端',
+                '参数配置服务端',
+                '项目创建',
+                ['zrpc服务端', '服务端'],
+                ['zrpc客户端', '客户端'],
+                '自定义拦截器',
+                '服务注册',
+                '负载均衡',
+            ]
+        },
+        {
+            title: F,
+            collapsable: false,
+            children: [
+                'goctl-overview',
+                'goctl-api',
+                'goctl-rpc',
+                'goctl-model',
+                'goctl-template',
+                'goctl-plugin',
+                'goctl-docker',
+                'goctl-kube',
+                '附录1',
+                '附录2',
+                '附录3',
+            ]
+        },
+        {
+            title: H,
+            collapsable: false,
+            children: [
+                '10月3日线上交流问题汇总',
+            ]
+        },
+    ]
+}

二进制
docs/.vuepress/public/logo.png


+ 24 - 0
docs/README.md

@@ -0,0 +1,24 @@
+---
+home: true
+heroImage: /logo.png
+actionText: 快速上手 →
+actionLink: /zero/
+heroText: Go Zero
+tagline: 集成各种工程实践的 WEB 和 RPC 框架
+sidebar: auto
+features:
+- title: 大道至简
+  details: 极简的 API 描述,一键生成各端代码
+- title: 自动治理
+  details: 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力
+- title: 工程哲学
+  details: 微服务治理和工具包
+
+footer: MIT Licensed | Copyright © 2020-present GoZero
+---
+```go
+// 安装 go-zero
+GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero
+
+// 然后就可以开始你的微服务之旅了
+```

二进制
docs/images/api-gen.png


二进制
docs/images/architecture-en.png


二进制
docs/images/architecture.png


二进制
docs/images/balancer.png


二进制
docs/images/benchmark.png


二进制
docs/images/bookstore-api.png


二进制
docs/images/bookstore-arch.png


二进制
docs/images/bookstore-benchmark.png


二进制
docs/images/bookstore-model.png


二进制
docs/images/bookstore-rpc.png


二进制
docs/images/concurrent_denpendency.png


二进制
docs/images/datasource.png


二进制
docs/images/fx_log.png


二进制
docs/images/fx_middle.png


二进制
docs/images/fx_reverse.png


二进制
docs/images/fx_step_result.png


二进制
docs/images/go-zero.png


二进制
docs/images/interceptor.png


二进制
docs/images/model-gen.png


二进制
docs/images/mr.png


二进制
docs/images/mr_time.png


二进制
docs/images/panel.png


二进制
docs/images/prom_up.png


二进制
docs/images/prometheus.png


二进制
docs/images/qps.png


二进制
docs/images/qps_panel.png


二进制
docs/images/qq.jpg


部分文件因为文件数量过多而无法显示