go快速入门

我本身有啥语言都会点,所以这门语言我会用其他语言的特性来描述,请谨慎阅读

基本抄自https://gfw.go101.org/ 值得一看

golang槽点太多。写出坑来都毫无感觉

[toc]

研究一个语言要关注哪些地方?

  • 使用(生态,命令行,包管理/代理等)
  • 语言特性(核心卖点,人无我有的)
  • 类型系统( 值类型还是引用类型?)
  • 优化点 profile 内存分配器 延迟相关/GC等等

相关命令行

下载包

go get github.com/onsi/gomega

如果离线安装,得克隆到goroot目录里面

更新mod

go mod tidy

语言功能


  • channel以及select-case

select-case分支流程控制代码块

Go中有一个专门为通道设计的 select-case分支流程控制语法。 此语法和 switch-case分支流程控制语法很相似。 比如,select-case流程控制代码块中也可以有若干 case分支和最多一个 default分支。 但是,这两种流程控制也有很多不同点。在一个 select-case流程控制中,

  • select关键字和 {之间不允许存在任何表达式和语句。
  • fallthrough语句不能被使用.
  • 每个 case关键字后必须跟随一个通道接收数据操作或者一个通道发送数据操作。 通道接收数据操作可以做为源值出现在一条简单赋值语句中。 以后,一个 case关键字后跟随的通道操作将被称为一个 case操作。
  • 所有的非阻塞 case操作中将有一个被随机选择执行(而不是按照从上到下的顺序),然后执行此操作对应的 case分支代码块。
  • 在所有的 case操作均为阻塞的情况下,如果 default分支存在,则 default分支代码块将得到执行; 否则,当前协程将被推入所有阻塞操作中相关的通道的发送数据协程队列或者接收数据协程队列中,并进入阻塞状态。

按照上述规则,一个不含任何分支的 select-case代码块 select{}将使当前协程处于永久阻塞状态。

select-case流程控制的实现机理

select-case流程控制是Go中的一个重要和独特的特性。 下面列出了官方标准运行时中 select-case流程控制的实现步骤

  1. 将所有 case操作中涉及到的通道表达式和发送值表达式按照从上到下,从左到右的顺序一一估值。 在赋值语句中做为源值的数据接收操作对应的目标值在此时刻不需要被估值。
  2. 将所有分支随机排序。default分支总是排在最后。 所有 case操作中相关的通道可能会有重复的。
  3. 为了防止在下一步中造成(和其它协程互相)死锁,对所有 case操作中相关的通道进行排序。 排序依据并不重要,官方Go标准编译器使用通道的地址顺序进行排序。 排序结果中前 N个通道不存在重复的情况。 N为所有 case操作中涉及到的不重复的通道的数量。 下面,通道锁顺序***是针对此排序结果中的前 N个通道来说的,通道锁逆序***是指此顺序的逆序。
  4. 按照上一步中的生成通道锁顺序获取所有相关的通道的锁。
  5. 按照第2步中生成的分支顺序检查相应分支:

    1. 如果这是一个 case分支并且相应的通道操作是一个向关闭了的通道发送数据操作,则按照通道锁逆序解锁所有的通道并在当前协程中产生一个恐慌。 跳到第12步。
    2. 如果这是一个 case分支并且相应的通道操作是非阻塞的,则按照通道锁逆序解锁所有的通道并执行相应的 case分支代码块。 (此相应的通道操作可能会唤醒另一个处于阻塞状态的协程。) 跳到第12步。
    3. 如果这是 default分支,则按照通道锁逆序解锁所有的通道并执行此 default分支代码块。 跳到第12步。

      (到这里,default分支肯定是不存在的,并且所有的case操作均为阻塞的。)

  6. 将当前协程(和对应 case分支信息)推入到每个 case操作中对应的通道的发送数据协程队列或接收数据协程队列中。 当前协程可能会被多次推入到同一个通道的这两个队列中,因为多个 case操作中对应的通道可能为同一个。
  7. 使当前协程进入阻塞状态并且按照通道锁逆序解锁所有的通道。
  8. …,当前协程处于阻塞状态,等待其它协程通过通道操作唤醒当前协程,…
  9. 当前协程被另一个协程中的一个通道操作唤醒。 此唤醒通道操作可能是一个通道关闭操作,也可能是一个数据发送/接收操作。 如果它是一个数据发送/接收操作,则(当前正被解释的 select-case流程中)肯定有一个相应 case操作与之配合传递数据。 在此配合过程中,当前协程将从相应 case操作相关的通道的接收/发送数据协程队列中弹出。
  10. 按照第3步中的生成的通道锁顺序获取所有相关的通道的锁。
  11. 将当前协程从各个case

    操作中对应的通道的发送数据协程队列或接收数据协程队列中(可能以非弹出的方式)移除。

    1. 如果当前协程是被一个通道关闭操作所唤醒,则跳到第5步。
    2. 如果当前协程是被一个数据发送/接收操作所唤醒,则相应的 case分支已经在第9步中知晓。 按照通道锁逆序解锁所有的通道并执行此 case分支代码块。
  12. 完毕。

从此实现中,我们得知

  • 一个协程可能同时多次处于同一个通道的发送数据协程队列或接收数据协程队列中。
  • 当一个协程被阻塞在一个 select-case流程控制中并在以后被唤醒时,它可能会从多个通道的发送数据协程队列和接收数据协程队列中被移除。

channel简单用法

通知


package main

import (
	"crypto/rand"
	"fmt"
	"os"
	"sort"
)

func main() {
	values := make([]byte, 32 * 1024 * 1024)
	if _, err := rand.Read(values); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	done := make(chan struct{}) // 也可以是缓冲的

	// 排序协程
	go func() {
		sort.Slice(values, func(i, j int) bool {
			return values[i] < values[j]
		})
		done <- struct{}{} // 通知排序已完成
	}()

	// 并发地做一些其它事情...

	<- done // 等待通知
	fmt.Println(values[0], values[len(values)-1])
}
package main

import "log"
import "time"

type T = struct{}

func worker(id int, ready <-chan T, done chan<- T) {
	<-ready // 阻塞在此,等待通知
	log.Print("Worker#", id, "开始工作")
	// 模拟一个工作负载。
	time.Sleep(time.Second * time.Duration(id+1))
	log.Print("Worker#", id, "工作完成")
	done <- T{} // 通知主协程(N-to-1)
}

func main() {
	log.SetFlags(0)

	ready, done := make(chan T), make(chan T)
	go worker(0, ready, done)
	go worker(1, ready, done)
	go worker(2, ready, done)

	// 模拟一个初始化过程
	time.Sleep(time.Second * 3 / 2)
	// 单对多通知
  close(ready)
	// 等待被多对单通知
	<-done; <-done; <-done
}

  • test的执行顺序 TestMain ->Test* 如果有TestMain, 必须有m.Run()否则本模块内部的Test*不执行
  • go的map默认是引用类型,要想复制修改,必须make
  • go的字符数组也是默认引用的
  • go不能直接赋值给PB的字段,必须构造,否则会segfault panic
  • 和nil指针比较是有类型的
assert.Equal(t, (*metadata)(nil), mymetadata, "meta nill check")
  • 没有参数的捕获默认是引用捕获,深坑,要看能否求值出。这里借用群友的一张图

这里有个解决方案https://zhuanlan.zhihu.com/p/351428978

for i := 0; i < 10; i++ {
  go func(x int) {
    fmt.Println(x)
  }(i)
}
  • time比较

我遇到的场景,是time保存到mongo在读回来,就多了location信息,不能直接比较,不相等

        	Error:      	Not equal: 
        	            	expected: time.Time{wall:0xa4fc540, ext:63761777875, loc:(*time.Location)(nil)}
        	            	actual  : time.Time{wall:0xc0338154ca582af8, ext:10214780624, loc:(*time.Location)(0x15b1ee0)}
        	        
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1,5 +1,136 @@
        	            	 (time.Time) {
        	            	- wall: (uint64) 173000000,
        	            	- ext: (int64) 63761777875,
        	            	- loc: (*time.Location)(<nil>)
        	            	+ wall: (uint64) 13849555480266418936,
        	            	+ ext: (int64) 10214780624,
        	            	+ loc: (*time.Location)({
        	            	+  name: (string) (len=5) "Local",
        	            	+  zone: ([]time.zone) (len=3) {
        	            	+   (time.zone) {
        	            	+    name: (string) (len=3) "LMT",
        	            	+    offset: (int) 29143,
        	            	+    isDST: (bool) false
        	            	+   },
//以下省略一百行timezone信息,太傻逼了

查文档,用time.equal,也不好使,最终用unix来比较

assert.Equal(t, doc.CreateTime.Unix(), createtime.Unix(), "time check  error")

忙活一晚上

  • 判定结构体是否为空
package main
import (
  "fmt"
)

type Person struct {
}

func main() {
  var st Person
  if (Person{} == st) {
      fmt.Println("It is an empty structure")
  } else {
    fmt.Println("It is not an empty structure")
  }
}

如果结构体比较复杂不能直接比较,用deepEqual

package main

import (
  "fmt"
  "reflect"
)

type Person struct {
  age int
}

func (x Person) IsStructureEmpty() bool {
  return reflect.DeepEqual(x, Person{})
}

func main() {
  x := Person{}
  if x.IsStructureEmpty() {
    fmt.Println("Structure is empty")
  } else {
    fmt.Println("Structure is not empty")
  }
}

  • go testify对于结构体的比较,可能会触发内存泄漏
非常长的一段堆栈

goroutine 718 [running]: runtime.systemstack_switch() /usr/local/go/src/runtime/asm_amd64.s:330 fp=0xc000a39e08 sp=0xc000a39e00 pc=0x47afe0 runtime.mallocgc(0x5ff45b34d, 0xe1ac60, 0x1, 0x2b) /usr/local/go/src/runtime/malloc.go:1070 +0x7e6 fp=0xc000a39ea8 sp=0xc000a39e08 pc=0x410f06 runtime.makeslice(0xe1ac60, 0x5ff45b34d, 0x5ff45b34d, 0x2) /usr/local/go/src/runtime/slice.go:98 +0x6f fp=0xc000a39ee0 sp=0xc000a39ea8 pc=0x45a18f bytes.makeSlice(0x5ff45b34d, 0x0, 0x0, 0x0) /usr/local/go/src/bytes/buffer.go:229 +0x73 fp=0xc000a39f20 sp=0xc000a39ee0 pc=0x5143f3 bytes.(*Buffer).grow(0xc000726630, 0x2b, 0x0) /usr/local/go/src/bytes/buffer.go:142 +0x15c fp=0xc000a39f70 sp=0xc000a39f20 pc=0x513d3c bytes.(*Buffer).Write(0xc000726630, 0xc6fbba4d20, 0x2b, 0x2b, 0xc6fbba4d20, 0x2b, 0x2b) /usr/local/go/src/bytes/buffer.go:172 +0xe5 fp=0xc000a39fa8 sp=0xc000a39f70 pc=0x514065 github.com/davecgh/go-spew/spew.(*dumpState).indent(0xc000a40ab0) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:67 +0xc2 fp=0xc000a3a010 sp=0xc000a39fa8 pc=0xd5aca2 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc0001d8640, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:416 +0x3a6 fp=0xc000a3a188 sp=0xc000a3a010 pc=0xd5c2e6 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc0001d8640, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a300 sp=0xc000a3a188 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb2060, 0xc0001d8640, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a478 sp=0xc000a3a300 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpSlice(0xc000a40ab0, 0xe087c0, 0xc000198bb8, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:238 +0x125 fp=0xc000a3a590 sp=0xc000a3a478 pc=0xd5b845 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe087c0, 0xc000198bb8, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:352 +0x9e6 fp=0xc000a3a708 sp=0xc000a3a590 pc=0xd5c926 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4ee0, 0xc000198bb8, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a880 sp=0xc000a3a708 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4e20, 0xc000198bb0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a9f8 sp=0xc000a3a880 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xdf43c0, 0xc000034538, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3ab28 sp=0xc000a3a9f8 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xdf43c0, 0xc000034538, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3aca0 sp=0xc000a3ab28 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed34e0, 0xc000034500, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3ae18 sp=0xc000a3aca0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpSlice(0xc000a40ab0, 0xe08780, 0xc00015d718, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:238 +0x125 fp=0xc000a3af30 sp=0xc000a3ae18 pc=0xd5b845 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe08780, 0xc00015d718, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:352 +0x9e6 fp=0xc000a3b0a8 sp=0xc000a3af30 pc=0xd5c926 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09da0, 0xc00015d6c0, 0x1f9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b220 sp=0xc000a3b0a8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09a20, 0xc00015d6c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b398 sp=0xc000a3b220 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b5a0, 0xc0001db790, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3b4c8 sp=0xc000a3b398 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf6b5a0, 0xc0001db790, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3b640 sp=0xc000a3b4c8 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc0001db780, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b7b8 sp=0xc000a3b640 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc0001db780, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b930 sp=0xc000a3b7b8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb2060, 0xc0001db780, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3baa8 sp=0xc000a3b930 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpSlice(0xc000a40ab0, 0xe087c0, 0xc000198c68, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:238 +0x125 fp=0xc000a3bbc0 sp=0xc000a3baa8 pc=0xd5b845 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe087c0, 0xc000198c68, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:352 +0x9e6 fp=0xc000a3bd38 sp=0xc000a3bbc0 pc=0xd5c926 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4ee0, 0xc000198c68, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3beb0 sp=0xc000a3bd38 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4e20, 0xc000198c60, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c028 sp=0xc000a3beb0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xdf43c0, 0xc000034578, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3c158 sp=0xc000a3c028 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xdf43c0, 0xc000034578, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3c2d0 sp=0xc000a3c158 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed34e0, 0xc000034540, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c448 sp=0xc000a3c2d0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf3f340, 0xc000034540, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3c578 sp=0xc000a3c448 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf3f340, 0xc000034540, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3c6f0 sp=0xc000a3c578 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc0001db740, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c868 sp=0xc000a3c6f0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc0001db740, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c9e0 sp=0xc000a3c868 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb2060, 0xc0001db740, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3cb58 sp=0xc000a3c9e0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf23c00, 0xc0001db740, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3cc88 sp=0xc000a3cb58 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf23c00, 0xc0001db740, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3ce00 sp=0xc000a3cc88 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe631c0, 0xc00007c420, 0x1b5) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:394 +0xe25 fp=0xc000a3cf78 sp=0xc000a3ce00 pc=0xd5cd65 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb33c0, 0xc00007c420, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3d0f0 sp=0xc000a3cf78 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xefc400, 0xc00007c420, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3d220 sp=0xc000a3d0f0 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xefc400, 0xc00007c420, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3d398 sp=0xc000a3d220 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09e80, 0xc00034d2c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3d510 sp=0xc000a3d398 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xebb380, 0xc00034d2c0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3d640 sp=0xc000a3d510 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xebb380, 0xc00034d2c0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3d7b8 sp=0xc000a3d640 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf33260, 0xc0002488c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3d930 sp=0xc000a3d7b8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09da0, 0xc0002488c0, 0x1f9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3daa8 sp=0xc000a3d930 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09a20, 0xc0002488c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3dc20 sp=0xc000a3daa8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b5a0, 0xc000196360, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3dd50 sp=0xc000a3dc20 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf6b5a0, 0xc000196360, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3dec8 sp=0xc000a3dd50 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc000196350, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e040 sp=0xc000a3dec8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc000196350, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e1b8 sp=0xc000a3e040 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed3ae0, 0xc000196350, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e330 sp=0xc000a3e1b8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf5fd00, 0xc000196350, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3e460 sp=0xc000a3e330 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf5fd00, 0xc000196350, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3e5d8 sp=0xc000a3e460 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf28880, 0xc0001b6488, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e750 sp=0xc000a3e5d8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b840, 0xc0001b6488, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3e880 sp=0xc000a3e750 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf6b840, 0xc0001b6488, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3e9f8 sp=0xc000a3e880 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe7f000, 0xc0001161b0, 0x1b5) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:394 +0xe25 fp=0xc000a3eb70 sp=0xc000a3e9f8 pc=0xd5cd65 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf0a200, 0xc0001161b0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3ece8 sp=0xc000a3eb70 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf3f4a0, 0xc0001161b0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3ee18 sp=0xc000a3ece8 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf3f4a0, 0xc0001161b0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3ef90 sp=0xc000a3ee18 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf33260, 0xc00015cfc0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f108 sp=0xc000a3ef90 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09da0, 0xc00015cfc0, 0x1f9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f280 sp=0xc000a3f108 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09a20, 0xc00015cfc0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f3f8 sp=0xc000a3f280 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b5a0, 0xc00017b2e0, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3f528 sp=0xc000a3f3f8 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf6b5a0, 0xc00017b2e0, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3f6a0 sp=0xc000a3f528 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc00017b2d0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f818 sp=0xc000a3f6a0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc00017b2d0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f990 sp=0xc000a3f818 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed3ae0, 0xc00017b2d0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3fb08 sp=0xc000a3f990 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf5fd00, 0xc00017b2d0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3fc38 sp=0xc000a3fb08 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf5fd00, 0xc00017b2d0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3fdb0 sp=0xc000a3fc38 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf28880, 0xc000123148, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3ff28 sp=0xc000a3fdb0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b840, 0xc000828d80, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a40058 sp=0xc000a3ff28 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf6b840, 0xc000828d80, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a401d0 sp=0xc000a40058 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef52a0, 0xc000828d80, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a40348 sp=0xc000a401d0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef0620, 0xc000828d80, 0x199) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a404c0 sp=0xc000a40348 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xee9ea0, 0xc000828d80, 0x16) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a405f0 sp=0xc000a404c0 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xee9ea0, 0xc000828d80, 0x16) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a40768 sp=0xc000a405f0 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe64f00, 0xc0006580c0, 0x95) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:394 +0xe25 fp=0xc000a408e0 sp=0xc000a40768 pc=0xd5cd65 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf73140, 0xc000658000, 0x99) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a40a58 sp=0xc000a408e0 pc=0xd5c474 github.com/davecgh/go-spew/spew.fdump(0x161e140, 0x1111720, 0xc000726630, 0xc000a40c30, 0x1, 0x1) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:465 +0x125 fp=0xc000a40af0 sp=0xc000a40a58 pc=0xd5d845 github.com/davecgh/go-spew/spew.(*ConfigState).Sdump(0x161e140, 0xc0004f6c30, 0x1, 0x1, 0x19, 0x0) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/config.go:281 +0x78 fp=0xc000a40b38 sp=0xc000a40af0 pc=0xd5aa58 github.com/stretchr/testify/assert.diff(0xf73140, 0xc000658000, 0xf73140, 0xc0006581c0, 0x0, 0x0) /root/go/pkg/mod/github.com/stretchr/testify@v1.6.1/assert/assertions.go:1592 +0x1d8 fp=0xc000a40cd8 sp=0xc000a40b38 pc=0xd66e38 github.com/stretchr/testify/assert.Equal(0x1114680, 0xc00054bb00, 0xf73140, 0xc000658000, 0xf73140, 0xc0006581c0, 0xc000a41078, 0x1, 0x1, 0x1)

  • 导入模块开头要大写,不然会报错
cannot refer to unexported name xxxx

错误变量覆盖,深坑,没告警


func (mgr *Manager) fixRunningTask() {
    db := store.GetMongo()
    docs, err := db.GetNoHeartbeatTask()
    if err != nil {
        return
    }
    //proxy连接代码,省略
    for _, doc := range docs {
        task := task.NewTask(doc)
		ErrCount := 0
		var res error
		for {
			err := task.SyncResult(proxy, true, opts...)
			if err == nil || err != ErrSystem {
			// res = task.SyncResult(proxy, true, opts...)
			//if res == nil || res != common.ErrSystem {
				break
			}
			// 如果是框架超时等错误,多试几次
            ErrCount++
            if ErrCount > 10 {
				break
			}
		}
		if err != nil && err != ErrStillRunning {
		// if res != nil && res != ErrStillRunning {
			// 可能发生了迁移或者重启,重新做
            db.InitTask(...)
        }
    }
}

上面的err,覆盖了下面的err。导致永远不会执行InitTask

c/c++程序员习惯于编译器纠错,但go编译器一点反应都没

一个字符数组默认引用的例子

start := Partition.Range.Start
startUint32 := binary.LittleEndian.Uint32(start)
binary.BigEndian.PutUint32(start, startUint32)

这么改,start和 Partition.Range.Start实际上是一个东西

这段代码这么看没啥问题,如果是个复杂一点的逻辑,就会被坑到,比如后面又改了start,但结果把上面的一起改了

go pprof抓内存泄漏

curl localhost:6060/debug/pprof/heap >base.out
# 喝口水,上个厕所
curl localhost:6060/debug/pprof/heap >current.out
go tool pprof -base base.out current.out

# 进入交互
(pprof) top10
(pprof) tree

基本就能查到,还算好用

看懂堆栈

// Declaration
main.Example(slice []string, str string, i int)

// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

类型 + 长度

参考资料

  • https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-03-array-string-and-slice.html
  • https://go-zh.org/doc/effective_go.html
  • https://gfw.go101.org/article/container.html
  • https://go-internals-cn.gitbook.io/go-internals/chapter1_assembly_primer
  • https://www.includehelp.com/golang/how-to-check-if-structure-is-empty.aspx

Read More

dlang入门

我本身有啥语言都会点,所以这门语言我会用其他语言的特性来描述,请谨慎阅读

安装

curl https://dlang.org/install.sh | bash -s

基本的工具 dmd编译,rdmd可以当成shell脚本使用#!/usr/bin/env rdmd 包管理工具dub

包引用语法 import std.stdio : writeln, writefln;基本上大同小异

基本概念

  • 类型,完全等同于c/c++但是存在构造函数.init

    • 每个类型有各种属性,.max .nan 等等,构造函数也是一种属性,.stringof返回自身名字。感觉这很反射,python也有__repr__
    • auto 同,typeof也类似,immutable等同于const
    • 内存管理,内嵌GC,三种内存模式 @system默认 @safe 检查内存安全@trusted三方api互通有点像rust的unsafe
  • 控制流,完全一致,有个foreach相当于range-for
  • 函数,完全一致,支持函数内函数,以及返回auto
  • 结构体/类,this函数就是构造函数,private修饰成员函数,override修饰
    • interface接口以及工厂模式
  • 数组,支持slice,类似go,map就是特殊的数组 int[string] arr;
  • 模版,完全就是go那个德行
auto add(T)(T lhs, T rhs) {
    return lhs + rhs;
}

struct S(T) {
    // ...
}

大概就这么多


ref

  • https://tour.dlang.org/tour/en/basics/functions
  • 魔鬼细节 http://dpldocs.info/this-week-in-d/Blog.Posted_2021_02_15.html

Read More

nim入门

我本身有啥语言都会点,所以这门语言我会用其他语言的特性来描述,请谨慎阅读

基本概念

  • 注释 # /discord”””

  • 字符串 和c++差不多,有raw字符串 r”blahbah\balh”

  • var 变量定义,有点像rust的let 可以指定类型,以及用值初始化

    • 感觉这个parser应该和rust差不多
  • let 和var差不多,但只能用一次,类似c++的const初始化
  • const常量 表示的编译期常量

  • 数字 科学表示法

    • 1_000_000 1.0e9
    • 十六进制字面值前缀是 0x ,二进制字面值用 0b ,八进制用 0o 单独一个前导零不产生八进制,和c++不一样
    • 可以后缀描述 有点像rust
    let
      x = 0     # x是 ``int``
      y = 0'i8  # y是 ``int8``
      z = 0'i64 # z是 ``int64``
      u = 0'u   # u是 ``uint`
    var
      a = 0.0      # a是 ``float``
      b = 0.0'f32  # b是 ``float32``
      c = 0.0'f64  # c是 ``float64``
    
  • 控制流 所有的控制条件都不需要括号,有点像python

    • if 没括号
    let name = readLine(stdin)
    if name == "":
      echo "Poor soul, you lost your name?"
    elif name == "name":
      echo "Very funny, your name is name."
    else:
      echo "Hi, ", name, "!"
    
    • case 有点switch case 那味儿了
    let name = readLine(stdin)
    case name
    of "":
      echo "Poor soul, you lost your name?"
    of "name":
      echo "Very funny, your name is name."
    of "Dave", "Frank":
      echo "Cool name!"
    else:
      echo "Hi, ", name, "!"
    
    • while没啥说的
    • for可以当作迭代器
    echo "Counting to ten: "
    for i in countup(1, 10):
      echo i
    #语法糖
    for i in 0..<10:
      ...  # 0..9
    var s = "some string"
    for i in 0..<s.len:
      ...
    for index, item in ["a","b"].pairs:
      echo item, " at index ", index
    # => a at index 0
    # => b at index 1
    
    • 作用域 block 都是按照空格的,更像python
    • break一样,可以跳出循环,以及block
    • continue不提
    • when 类似c++的if constexpr 或者#ifdef这种
    when system.hostOS == "windows":
      echo "running on Windows!"
    elif system.hostOS == "linux":
      echo "running on Linux!"
    elif system.hostOS == "macosx":
      echo "running on Mac OS X!"
    else:
      echo "unknown operating system"
    
  • 函数 nim里叫procedure 过程 注意还是没有大括号
proc yes(question: string): bool =
  echo question, " (y/n)"
  while true:
    case readLine(stdin)
    of "y", "Y", "yes", "Yes": return true
    of "n", "N", "no", "No": return false
    else: echo "Please be clear: yes or no"

if yes("Should I delete all your important files?"):
  echo "I'm sorry Dave, I'm afraid I can't do that."
else:
  echo "I think you know what the problem is just as well as I do."

这个函数声明,像不像go/rust

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}
//func 函数名(形式参数列表)(返回值列表){
//    函数体
//}
func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}

这样设计,我就当parser好写了

注意 返回值不用必须显式舍弃 或者用修饰

discard yes("May I ask a pointless question?")

proc p(x, y: int): int {.discardable.} =
  return x + y

p(3, 4) # now valid
  • 重载 不仅可以函数重载,还可以操作符号重载,还可以直接调用操作符,和c++的operator是一样的
proc `$` (x: myDataType): string = ...
# 现在$操作符对myDataType生效,重载解析确保$对内置类型像之前一样工作。

#"``"标记也可以来用调用一个像任何其它过程的操作符:
if `==`( `+`(3, 4), 7): echo "True"

函数也需要前向声明 (c/c++陋习)

  • 迭代器 有点像python的生成器
iterator countup(a, b: int): int =
  var res = a
  while res <= b:
    yield res
    inc(res)

还支持引用和切片,有点像c++/go 跳过了

还提供模版和泛型,更像c++了

多态,method

宏比较暴力,不仔细讲

  • with 和python的with差不多,但是是宏

如何对nim做贡献/输出? 引自 https://dev.to/xflywind/how-to-contribute-to-nim-language-4ad8

先看 Nim 文档 .

然后去处理 easy问题 或者丰富文档 documentation.

加功能,写库,可能要看RFC).

解决更复杂的问题,去处理JS codegen 标记的问题

Contributing Guide 文档一定要看


ref

  • 居然有中文版网页了 https://nim-lang-cn.org/docs/tut1.html

Read More

ruby快速入门

我本身有啥语言都会点,所以这门语言我会用其他语言的特性来描述,请谨慎阅读

基本概念

  • 注释 # =begin =end

  • 所有都是对象,要了解内嵌方法

    • 方法本身也是对象
    "Hello".method(:class).class #=> Method
    
    • 运算符号也是方法 可以用opertor dot来调用的
    1.+(3) #=> 4
    10.* 5 #=> 50 
    100.methods.include?(:/) #=> true
    
  • 控制流 所有控制块都用end结尾 条件不用括号

    • If - elsif - end 这个elsif有点离谱
    • while
    counter = 1
    while counter <= 5 do
      puts "iteration #{counter}"
      counter += 1
    end
    
    • for 经典语法糖
    for counter in 1..5
      puts "iteration #{counter}"
    end
    (1..5).each do |counter|
      puts "iteration #{counter}"
    end
      
    array.each do |element|
      puts "#{element} is part of the array"
    end
    hash.each do |key, value|
      puts "#{key} is #{value}"
    end
    

    这个each do后面的是lambda。就是语法有点怪异,c++的lambda

    std::vector<int> v;
    std::for_each(v.begin(),v.end(), [](int i){ std::cout<<i;});
    for(auto var : v) {
      std::cout<<var;
    }
    

    c++的lambda的参数比较明显,是个函数调用,ruby这个竖线包围有点不明不白, 更像range-for这种用法

    ruby甚至还有each_with_index

    # 如果你还需要索引值,可以使用 "each_with_index",并且定义
    # 一个索引变量
    array.each_with_index do |element, index|
      puts "#{element} is number #{index} in the array"
    end
    

    map reduce也是内嵌的。这点python也有,更泛型一些,而不是作为对象方法,ruby就是这么设计的

    array = [1,2,3,4,5]
    doubled = array.map do |element|
      element * 2
    end
    puts doubled
    #=> [2,4,6,8,10]
    puts array
    #=> [1,2,3,4,5]
    

    case when对应switch-case

    异常处理,python的try-except-finally对应begin-rescue-ensure

    begin
      raise NoMemoryError, 'You ran out of memory.'
    rescue NoMemoryError => exception_variable
      puts 'NoMemoryError was raised', exception_variable
    rescue RuntimeError => other_exception_variable
      puts 'RuntimeError was raised now'
    else
      puts 'This runs if no exceptions were thrown at all'
    ensure
      puts 'This code always runs no matter what'
    end
    
  • 函数 可以不用括号

def surround
  puts "{"
  yield
  puts "}"
end

surround { puts 'hello world' }

# {
# hello world
# }
# => nil

# 可以向函数传递一个块
# "&"标记传递的块是一个引用
def guests(&block)
  block.class #=> Proc
  block.call(4)
end

guests { |n| "You have #{n} guests." }
# => "You have 4 guests."

# 可以传递多个参数,这些参数会转成一个数组,
# 这也是使用星号符 ("*") 的原因:
def guests(*array)
  array.each { |guest| puts guest }
end
class Human

  # 一个类变量,它被这个类的所有实例变量共享
  @@species = "H. sapiens"

  # 基本构造函数
  def initialize(name, age = 0)
    # 将参数值赋给实例变量 "name"
    @name = name
    # 如果没有给出 age,那么会采用参数列表中的默认值
    @age = age
  end

  # 基本的 setter 方法
  def name=(name)
    @name = name
  end

  # 基本地 getter 方法
  def name
    @name
  end

  # 以上的功能也可以用下面的 attr_accessor 来封装
  attr_accessor :name

  # Getter/setter 方法也可以像这样单独创建
  attr_reader :name
  attr_writer :name

  # 类方法通过使用 self 与实例方法区别开来。
  # 它只能通过类来调用,不能通过实例调用。
  def self.say(msg)
    puts "#{msg}"
  end

  def species
    @@species
  end
end

对比c++来解释,@@是静态变量,实例共享主要是setter getter设计比较语法糖,齁得慌

继承 <

class Doctor < Human
end

包含和扩展,这个也是语法糖

# '包含'模块后,模块的方法会绑定为类的实例方法
# '扩展'模块后,模块的方法会绑定为类方法

class Person
  include ModuleExample
end

class Book
  extend ModuleExample
end

Person.foo     # => NoMethodError: undefined method `foo' for Person:Class
Person.new.foo # => 'foo'
Book.foo       # => 'foo'
Book.new.foo   # => NoMethodError: undefined method `foo'

一般都继承Stucture,类似python继承object


ref

  • https://learnxinyminutes.com/docs/zh-cn/ruby-cn/

Read More

(转)数据库故障恢复机制的前世今生

转自这个链接 http://mysql.taobao.org/monthly/2019/01/01/

背景

在数据库系统发展的历史长河中,故障恢复问题始终伴随左右,也深刻影响着数据库结构的发展变化。通过故障恢复机制,可以实现数据库的两个至关重要的特性:Durability of Updates以及Failure Atomic,也就是我们常说的的ACID中的A和D。磁盘数据库由于其卓越的性价比一直以来都占据数据库应用的主流位置。然而,由于需要协调内存和磁盘两种截然不同的存储介质,在处理故障恢复问题时也增加了很多的复杂度。随着学术界及工程界的共同努力及硬件本身的变化,磁盘数据库的故障恢复机制也不断的迭代更新,尤其近些年来,随着NVM的浮现,围绕新硬件的研究也如雨后春笋出现。本文希望通过分析不同时间点的关键研究成果,来梳理数据库故障恢复问题的本质,其发展及优化方向,以及随着硬件变化而发生的变化。 文章将首先描述故障恢复问题本身;然后按照基本的时间顺序介绍传统数据库中故障恢复机制的演进及优化;之后思考新硬件带来的机遇与挑战;并引出围绕新硬件的两个不同方向的研究成果;最后进行总结。

问题

数据库系统运行过程中可能遇到的故障类型主要包括,Transaction Failure,Process Failure,System Failure以及Media Failure。其中Transaction Failure可能是主动回滚或者冲突后强制Abort;Process Failure指的是由于各种原因导致的进程退出,进程内存内容会丢失;System Failure来源于操作系统或硬件故障;而Media Failure则是存储介质的不可恢复损坏。数据库系统需要正确合理的处理这些故障,从而保证系统的正确性。为此需要提供两个特性:

  • Durability of Updates:已经Commit的事务的修改,故障恢复后仍然存在;
  • Failure Atomic:失败事务的所有修改都不可见

因此,故障恢复的问题描述为:即使在出现故障的情况下,数据库依然能够通过提供Durability及Atomic特性,保证恢复后的数据库状态正确。然而,要解决这个问题并不是一个简单的事情,为了不显著牺牲数据库性能,长久以来人们对故障恢复机制进行了一系列的探索。

Shadow Paging

1981年,JIM GRAY等人在《The Recovery Manager of the System R Database Manager》中采用了一种非常直观的解决方式Shadow Paging[1]。System R的磁盘数据采用Page为最小的组织单位,一个File由多个Page组成,并通过称为Direcotry的元数据进行索引,每个Directory项纪录了当前文件的Page Table,指向其包含的所有Page。采用Shadow Paging的文件称为Shadow File,如下图中的File B所示,这种文件会包含两个Directory项,Current及Shadow。

事务对文件进行修改时,会获得新的Page,并加入Current的Page Table,所有的修改都只发生在Current Directory;事务Commit时,Current指向的Page刷盘,并通过原子的操作将Current的Page Table合并到Shadow Directory中,之后再返回应用Commit成功;事务Abort时只需要简单的丢弃Current指向的Page;如果过程中发生故障,只需要恢复Shadow Directory,相当于对所有未提交事务的回滚操作。Shadow Paging很方便的实现了:

  • Durability of Updates:事务完成Commit后,所有修改的Page已经落盘,合并到Shadow后,其所有的修改可以在故障后恢复出来。
  • Failure Atomic:回滚的事务由于没有Commit,从未影响Shadow Directory,因此其所有修改不可见。

虽然Shadow Paging设计简单直观,但它的一些缺点导致其并没有成为主流,首先,不支持Page内并发,一个Commit操作会导致其Page上所有事务的修改被提交,因此一个Page内只能包含一个事务的修改;其次,不断修改Page的物理位置,导致很难将相关的页维护在一起,破坏局部性;另外,对大事务而言,Commit过程在关键路径上修改Shadow Directory的开销可能很大,同时这个操作还必须保证原子;最后,增加了垃圾回收的负担,包括对失败事务的Current Pages和提交事务的Old Pages的回收。

WAL

由于传统磁盘顺序访问性能远好于随机访问,采用Logging的故障恢复机制意图利用顺序写的Log来记录对数据库的操作,并在故障恢复后通过Log内容将数据库恢复到正确的状态。简单的说,每次修改数据内容前先顺序写对应的Log,同时为了保证恢复时可以从Log中看到最新的数据库状态,要求Log先于数据内容落盘,也就是常说的Write Ahead Log,WAL。除此之外,事务完成Commit前还需要在Log中记录对应的Commit标记,以供恢复时了解当前的事务状态,因此还需要关注Commit标记和事务中数据内容的落盘顺序。根据Log中记录的内容可以分为三类:Undo-Only,Redo-Only,Redo-Undo。

Undo-Only Logging

Undo-Only Logging的Log记录可以表示未<T, X, v>,事务T修改了X的值,X的旧值是v。事务提交时,需要通过强制Flush保证Commit标记落盘前,对应事务的所有数据落盘,即落盘顺序为Log记录->Data->Commit标记。恢复时可以根据Commit标记判断事务的状态,并通过Undo Log中记录的旧值将未提交事务的修改回滚。我们来审视一下Undo-Only对Durability及Atomic的保证:

  • Durability of Updates:Data强制刷盘保证,已经Commit的事务由于其所有Data都已经在Commit标记之前落盘,因此会一直存在;
  • Failure Atomic:Undo Log内容保证,失败事务的已刷盘的修改会在恢复阶段通过Undo日志回滚,不再可见。

然而Undo-Only依然有不能Page内并发的问题,如果两个事务的修改落到一个Page中,一个事务提交前需要的强制Flush操作,会导致同Page所有事务的Data落盘,可能会早于对应的Log项从而损害WAL。同时,也会导致关键路径上过于频繁的磁盘随机访问。

Redo-Only Logging

不同于Undo-Only,采用Redo-Only的Log中记录的是修改后的新值。对应地,Commit时需要保证,Log中的Commit标记在事务的任何数据之前落盘,即落盘顺序为Log记录->Commit标记->Data。恢复时同样根据Commit标记判断事务状态,并通过Redo Log中记录的新值将已经Commit,但数据没有落盘的事务修改重放。

  • Durability of Updates:Redo Log内容保证,已提交事务的未刷盘的修改,利用Redo Log中的内容重放,之后可见;
  • Failure Atomic:阻止Commit前Data落盘保证,失败事务的修改不会出现在磁盘上,自然不可见。

Redo-Only同样有不能Page内并发的问题,Page中的多个不同事务,只要有一个未提交就不能刷盘,这些数据全部都需要维护在内存中,造成较大的内存压力。

Redo-Undo Logging

可以看出的只有Undo或Redo的问题,主要来自于对Commit标记及Data落盘顺序的限制,而这种限制归根结底来源于Log信息中对新值或旧值的缺失。因此Redo-Undo采用同时记录新值和旧值的方式,来消除Commit和Data之间刷盘顺序的限制

  • Durability of Updates:Redo 内容保证,已提交事务的未刷盘的修改,利用Redo Log中的内容重放,之后可见;
  • Failure Atomic:Undo内容保证,失败事务的已刷盘的修改会在恢复阶段通过Undo日志回滚,不再可见。

如此一来,同Page的不同事务提交就变得非常简单。同时可以将连续的数据攒着进行批量的刷盘已利用磁盘较高的顺序写性能。

Force and Steal

从上面看出,Redo和Undo内容分别可以保证Durability和Atomic两个特性,其中一种信息的缺失需要用严格的刷盘顺序来弥补。这里关注的刷盘顺序包含两个维度:

  • Force or No-Force:Commit时是否需要强制刷盘,采用Force的方式由于所有的已提交事务的数据一定已经存在于磁盘,自然而然地保证了Durability;
  • No-Steal or Steal,Commit前数据是否可以提前刷盘,采用No-Steal的方式由于保证事务提交前修改不会出现在磁盘上,自然而然地保证了Atomic。

总结一下,实现Durability可以通过记录Redo信息或要求Force刷盘顺序,实现Atomic需要记录Undo信息或要求No-Steal刷盘顺序,组合得到如下四种模式,如下图所示:

ARIES,一统江湖

1992年,IBM的研究员们发表了《ARIES: a transaction recovery method supporting fine-granularity locking and partial rollbacks using write-ahead logging》[2],其中提出的ARIES逐步成为磁盘数据库实现故障恢复的标配,ARIES本质是一种Redo-Undo的WAL实现。 Normal过程:修改数据之前先追加Log记录,Log内容同时包括Redo和Undo信息,每个日志记录产生一个标记其在日志中位置的递增LSN(Log Sequence Number);数据Page中记录最后修改的日志项LSN,以此来判断Page中的内容的新旧程度,实现幂等。故障恢复阶段需要通过Log中的内容恢复数据库状态,为了减少恢复时需要处理的日志量,ARIES会在正常运行期间周期性的生成Checkpoint,Checkpoint中除了当前的日志LSN之外,还需要记录当前活跃事务的最新LSN,以及所有脏页,供恢复时决定重放Redo的开始位置。需要注意的是,由于生成Checkpoint时数据库还在正常提供服务(Fuzzy Checkpoint),其中记录的活跃事务及Dirty Page信息并不一定准确,因此需要Recovery阶段通过Log内容进行修正。

Recover过程:故障恢复包含三个阶段:Analysis,Redo和Undo。Analysis阶段的任务主要是利用Checkpoint及Log中的信息确认后续Redo和Undo阶段的操作范围,通过Log修正Checkpoint中记录的Dirty Page集合信息,并用其中涉及最小的LSN位置作为下一步Redo的开始位置RedoLSN。同时修正Checkpoint中记录的活跃事务集合(未提交事务),作为Undo过程的回滚对象;Redo阶段从Analysis获得的RedoLSN出发,重放所有的Log中的Redo内容,注意这里也包含了未Commit事务;最后Undo阶段对所有未提交事务利用Undo信息进行回滚,通过Log的PrevLSN可以顺序找到事务所有需要回滚的修改。

除此之外,ARIES还包含了许多优化设计,例如通过特殊的日志记录类型CLRs避免嵌套Recovery带来的日志膨胀,支持细粒度锁,并发Recovery等。[3]认为,ARIES有两个主要的设计目标:

  • Feature:提供丰富灵活的实现事务的接口:包括提供灵活的存储方式、提供细粒度的锁、支持基于Savepoint的事务部分回滚、通过Logical Undo以获得更高的并发、通过Page-Oriented Redo实现简单的可并发的Recovery过程。
  • Performance:充分利用内存和磁盘介质特性,获得极致的性能:采用No-Force避免大量同步的磁盘随机写、采用Steal及时重用宝贵的内存资源、基于Page来简化恢复和缓存管理。

NVM带来的机遇与挑战

从Shadow Paging到WAL,再到ARIES,一直围绕着两个主题:减少同步写以及尽量用顺序写代替随机写。而这些正是由于磁盘性能远小于内存,且磁盘顺序访问远好于随机访问。然而随着NVM磁盘的出现以及对其成为主流的预期,使得我们必须要重新审视我们所做的一切。相对于传统的HDD及SSD,NVM最大的优势在于:

  • 接近内存的高性能
  • 顺序访问和随机访问差距不大
  • 按字节寻址而不是Block

在这种情况下,再来看ARIES的实现:

  • No-force and Steal:同时维护Redo, Undo和数据造成的三倍写放大,来换取磁盘顺序写的性能,但在NVM上这种取舍变得很不划算;
  • Pages:为了迁就磁盘基于Block的访问接口,采用Page的存储管理方式,而内存本身是按字节寻址的,因此,这种适配也带来很大的复杂度。在同样按字节寻址的NVM上可以消除。

近年来,众多的研究尝试为NVM量身定制更合理的故障恢复机制,我们这里介绍其中两种比较有代表性的研究成果,MARS希望充分利用NVM并发及内部带宽的优势,将更多的任务交给硬件实现;而WBL则尝试重构当前的Log方式。

MARS

发表在2013年的SOSP上的《“From ARIES to MARS: Transaction support for next-generation, solid-state drives.” 》提出了一种尽量保留ARIES特性,但更适合NVM的故障恢复算法MARS[3]。MARS取消了Undo Log,保留的Redo Log也不同于传统的Append-Only,而是可以随机访问的。如下图所示,每个事务会占有一个唯一的TID,对应的Metadata中记录了Log和Data的位置

正常访问时,所有的数据修改都在对应的Redo Log中进行,不影响真实数据,由于没有Undo Log,需要采用No-Steal的方式,阻止Commit前的数据写回;Commit时会先设置事务状态为COMMITTED,之后利用NVM的内部带宽将Redo中的所有内容并发拷贝回Metadata中记录的数据位置。如果在COMMITED标记设置后发生故障,恢复时可以根据Redo Log中的内容重放。其本质是一种Redo加No-Steal的实现方式

  • Durability of Updates: Redo实现,故障后重放Redo;
  • Failure Atomic:未Commit事务的修改只存在于Redo Log,重启后会被直接丢弃。

可以看出,MARS的Redo虽然称之为Log,但其实已经不同于传统意义上的顺序写文件,允许随机访问,更像是一种临时的存储空间,类似于Shadow的东西。之所以在Commit时进行数据拷贝而不是像Shadow Paging一样的元信息修改,笔者认为是为了保持数据的局部性,并且得益于硬件优异的内部带宽。

WBL

不同于MARS保留Redo信息的思路,2016年VLDB上的《 “Write Behind Logging” 》只保留了Undo信息。笔者认为这篇论文中关于WBL的介绍里,用大量笔墨介绍了算法上的优化部分,为了抓住其本质,这里先介绍最基本的WBL算法:WBL去掉了传统的Append Only的Redo和Undo日志,但仍然需要保留Undo信息用来回滚未提交事务。事务Commit前需要将其所有的修改强制刷盘,之后在Log中记录Commit标记,也就是这里说的Write Behind Log。恢复过程中通过分析Commit标记将为提交的事务通过Undo信息回滚。可以看出WBL算法本身非常简单,在这个基础上,WBL做了如下优化:

  • Group Commit:周期性的检查内存中的修改,同样在所有修改刷盘之后再写Log,Log项中记录Commit并落盘的最新事务TimeStamp cp,保证早于cp的所有事务修改都已经落盘;同时记录当前分配出去的最大TimeStamp cd;也就是说此时所有有修改但未提交的事务Timestamp都落在cp和cd之间。Reovery的时候只需对这部分数据进行回滚;
  • 针对多版本数据库,多版本信息已经起到了undo的作用,因此不需要再额外记录undo信息;
  • 延迟回滚:Recovery后不急于对未提交事务进行回滚,而是直接提供服务,一组(cp, cd)称为一个gap,每一次故障恢复都可能引入新的gap,通过对比事务Timestamp和gap集合,来判断数据的可见性,需要依靠后台垃圾回收线程真正的进行回滚和对gap的清理,如下图所示;

  • 可以看出,WBL本质并没有什么新颖,是一个Force加Undo的实现方式,其正确性保证如下:
  • Durability of Updates:Commit事务的数据刷盘后才进行Commit,因此Commit事务的数据一定在Recovery后存在
  • Failure Atomic:通过记录的Undo信息或多版本时的历史版本信息,在Recovery后依靠后台垃圾回收线程进行回滚。

总结

数据库故障恢复问题本质是要在容忍故障发生的情况下保证数据库的正确性,而这种正确性需要通过提供Durability of Updates和Failure Atomic来保证。其中Duribility of Update要保证Commit事务数据在恢复后存在,可以通过Force机制或者通过Redo信息回放来保证;对应的,Failure Atomic需要保证未Commit事务的修改再恢复后消失,可以通过No-Steal机制或者通过Undo信息回滚来保证。根据保证Durability和Atomic的不同方式,对本文提到的算法进行分类,如下:

  • Shadow Paging可以看做是采用了Force加No-Steal的方式,没有Log信息,在Commit时,通过原子的修改Directory元信息完成数据的持久化更新,但由于其对Page内并发的限制等问题,没有成为主流;
  • Logging的实现方式增加了Redo或Undo日志,记录恢复需要的信息,从而放松Force或No-Steal机制对刷盘顺序的限制,从而尽量用磁盘顺序写代替随机写获得较好的性能。ARIES算法是在这一方向上的集大成者,其对上层应用提供了丰富灵活的接口,采用了No-Force加Steal机制,将传统磁盘上的性能发挥到极致,从而成为传统磁盘数据故障恢复问题的标准解决方案;
  • 随着NVM设备的逐步出现,其接近内存的性能、同样优异的顺序访问和随机访问表现,以及基于字节的寻址方式,促使人们开始重新审视数据库故障恢复的实现。其核心思路在于充分利用NVM硬件特性,减少Log机制导致的写放大以及设计较复杂的问题;
  • MARS作为其中的佼佼者之一,在NVM上维护可以随机访问的Redo日志,同时采用Force加Steal的缓存策略,充分利用NVM优异的随机写性能和内部带宽。
  • WBL从另一个方向进行改造,保留Undo信息,采用No-Force加No-Steal的缓存策略,并通过Group Commit及延迟回滚等优化,减少日志信息,缩短恢复时间。

本文介绍了磁盘数据库一路走来的核心发展思路,但距离真正的实现还有巨大的距离和众多的设计细节,如对Logical Log或Physical Log的选择、并发Recovery、Fuzzy Checkpoing、Nested Top Actions等,之后会用单独的文章以InnoDB为例来深入探究其设计和实现细节。

参考


Read More

redis性能调优参考

Redis性能调优

TL;DR

单线程串行执行命令 角度入手

  • 确保没有让Redis执行耗时长的命令,注意O(N)命令以及数量量大的O(logN)命令

    • 比如keys,考虑用scan系列命令代替,迭代使用

    • 对一个field数未知的Hash数据执行HGETALL/HKEYS/HVALS命令,通常来说这些命令执行的很快,但如果这个Hash中的field数量极多,耗时就会成倍增长。
    • 使用SUNION对两个Set执行Union操作,或使用SORT对List/Set执行排序操作等时,都应该严加注意。

    针对这种要优化设计

    • 不要把List当做列表使用,仅当做队列来使用

    • 通过机制严格控制Hash、Set、Sorted Set的大小
    • 可能的话,将排序、并集、交集等操作放在客户端执行
  • 使用pipelining将连续执行的命令组合执行

  • 操作系统的Transparent huge pages功能必须关闭:

    echo never > /sys/kernel/mm/transparent_hugepage/enabled

  • 如果在虚拟机中运行Redis,可能天然就有虚拟机环境带来的固有延迟。可以通过./redis-cli –intrinsic-latency 100命令查看固有延迟。同时如果对Redis的性能有较高要求的话,应尽可能在物理机上直接部署Redis。

  • 检查数据持久化策略

  • 考虑引入读写分离机制

  • 检查慢日志,slowlog命令

配置项

slowlog-log-slower-than xxxms #执行时间慢于xxx毫秒的命令计入Slow Log slowlog-max-len xxx

网络引发的延迟

  • 尽可能使用长连接或连接池,避免频繁创建销毁连接
  • 客户端进行的批量数据操作,应使用Pipeline特性在一次交互中完成。
    • 但是也要注意pipeline可能遭遇的tcp拆包问题,别太大。
  • Redis的数据持久化工作本身就会带来延迟,需要根据数据的安全级别和性能要求制定合理的持久化策略:

  • AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响
    • AOF + fsync every second是比较好的折中方案,每秒fsync一次
    • AOF + fsync never会提供AOF持久化方案下的最优性能
  • 使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置
  • 每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟
  • Redis在fork子进程时需要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共需要拷贝24GB / 4kB * 8 = 48MB的数据。在使用单Xeon 2.27Ghz的物理机上,这一fork操作耗时216ms。
    • 可以通过INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗时(微秒)

Swap引发的延迟

当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟。Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生,应尽可能避免上述两种情况的出现。

/proc//swaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。

数据淘汰引发的延迟

当同一秒内有大量key过期时,也会引发Redis的延迟。在使用时应尽量将key的失效时间错开。

随机失效时间防止雪崩也是最佳实践了

引入读写分离机制, 或者冷热分离?

Redis的主从复制能力可以实现一主多从的多节点架构,在这一架构下,主节点接收所有写请求,并将数据同步给多个从节点。 在这一基础上,我们可以让从节点提供对实时性要求不高的读请求服务,以减小主节点的压力。

尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个或多个从节点上执行,避免这些长耗时命令影响其他请求的响应。

Read More

说说 time_wait

不只是time_wait, 说说tcp关闭状态

TCP CLOSE_WAIT

03130408-930b424bf5384c80b677b6a50f1c6edc

几个问题

  • 主动关闭进入FIN_WAIT_1状态后,被动方没有回ack,主动方回怎样?如果大量堆积FIN_WAIT_1会怎样?怎么避免?
  • 主动的一方进入FIN_WAIT_2

参考

  • 火丁笔记 这两篇博文非常棒。解释了大量FIN_WAIT1和大量TIME_WAIT的问题,就比如说很多解决TIME_WAIT的方案简单说下配置就完了,这几篇文章说了背后的问题,还带了rfc,专业。
    • https://huoding.com/2014/11/06/383
    • https://huoding.com/2013/12/31/316
  • https://benohead.com/tcp-about-fin_wait_2-time_wait-and-close_wait/

TCP CLOSE_WAIT

Read More

redis命令使用建议

redis命令使用建议

Key

Redis采用Key-Value型的基本数据结构,任何二进制序列都可以作为Redis的Key使用(例如普通的字符串或一张JPEG图片) 关于Key的一些注意事项:

  • 不要使用过长的Key。例如使用一个1024字节的key就不是一个好主意,不仅会消耗更多的内存,还会导致查找的效率降低
  • Key短到缺失了可读性也是不好的,例如”u1000flw”比起”user:1000:followers”来说,节省了寥寥的存储空间,却引发了可读性和可维护性上的麻烦
  • 最好使用统一的规范来设计Key,比如”object-type:id:attr”,以这一规范设计出的Key可能是”user:1000”或”comment:1234:reply-to”
  • Redis允许的最大Key长度是512MB(对Value的长度限制也是512MB)

String

String是Redis的基础数据类型,Redis没有Int、Float、Boolean等数据类型的概念,所有的基本类型在Redis中都以String体现。

与String相关的常用命令:

  • SET:为一个key设置value,可以配合EX/PX参数指定key的有效期,通过NX/XX参数针对key是否存在的情况进行区别操作,时间复杂度O(1)
  • GET:获取某个key对应的value,时间复杂度O(1)
  • GETSET:为一个key设置value,并返回该key的原value,时间复杂度O(1)
  • MSET:为多个key设置value,时间复杂度O(N)
  • MSETNX:同MSET,如果指定的key中有任意一个已存在,则不进行任何操作,时间复杂度O(N)
  • MGET:获取多个key对应的value,时间复杂度O(N)

上文提到过,Redis的基本数据类型只有String,但Redis可以把String作为整型或浮点型数字来使用,主要体现在INCR、DECR类的命令上:

  • INCR:将key对应的value值自增1,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • INCRBY:将key对应的value值自增指定的整型数值,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • DECR/DECRBY:同INCR/INCRBY,自增改为自减。

INCR/DECR系列命令要求操作的value类型为String,并可以转换为64位带符号的整型数字,否则会返回错误。 也就是说,进行INCR/DECR系列命令的value,必须在[-2^63 ~ 2^63 - 1]范围内。

前文提到过,Redis采用单线程模型,天然是线程安全的,这使得INCR/DECR命令可以非常便利的实现高并发场景下的精确控制。

例1:库存控制

在高并发场景下实现库存余量的精准校验,确保不出现超卖的情况。

设置库存总量:

SET inv:remain "100"

库存扣减+余量校验:

DECR inv:remain

当DECR命令返回值大于等于0时,说明库存余量校验通过,如果返回小于0的值,则说明库存已耗尽。

假设同时有300个并发请求进行库存扣减,Redis能够确保这300个请求分别得到99到-200的返回值,每个请求得到的返回值都是唯一的,绝对不会找出现两个请求得到一样的返回值的情况。

例2:自增序列生成

实现类似于RDBMS的Sequence功能,生成一系列唯一的序列号

设置序列起始值:

SET sequence "10000"

获取一个序列值:

INCR sequence

直接将返回值作为序列使用即可。

获取一批(如100个)序列值:

INCRBY sequence 100

假设返回值为N,那么[N - 99 ~ N]的数值都是可用的序列值。

当多个客户端同时向Redis申请自增序列时,Redis能够确保每个客户端得到的序列值或序列范围都是全局唯一的,绝对不会出现不同客户端得到了重复的序列值的情况。

List

Redis的List是链表型的数据结构,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的两端执行插入元素和弹出元素的操作。虽然List也支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应小心使用。

与List相关的常用命令:

  • LPUSH:向指定List的左侧(即头部)插入1个或多个元素,返回插入后的List长度。时间复杂度O(N),N为插入元素的数量
  • RPUSH:同LPUSH,向指定List的右侧(即尾部)插入1或多个元素
  • LPOP:从指定List的左侧(即头部)移除一个元素并返回,时间复杂度O(1)
  • RPOP:同LPOP,从指定List的右侧(即尾部)移除1个元素并返回
  • LPUSHX/RPUSHX:与LPUSH/RPUSH类似,区别在于,LPUSHX/RPUSHX操作的key如果不存在,则不会进行任何操作
  • LLEN:返回指定List的长度,时间复杂度O(1)
  • LRANGE:返回指定List中指定范围的元素(双端包含,即LRANGE key 0 10会返回11个元素),时间复杂度O(N)。应尽可能控制一次获取的元素数量,一次获取过大范围的List元素会导致延迟,同时对长度不可预知的List,避免使用LRANGE key 0 -1这样的完整遍历操作。

应谨慎使用的List相关命令:

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index数值是回环的,即-1代表List最后一个位置,-2代表List倒数第二个位置。时间复杂度O(N)
  • LSET:将指定List指定index上的元素设置为value,如果index越界则返回错误,时间复杂度O(N),如果操作的是头/尾部的元素,则时间复杂度为O(1)
  • LINSERT:向指定List中指定元素之前/之后插入一个新元素,并返回操作后的List长度。如果指定的元素不存在,返回-1。如果指定key不存在,不会进行任何操作,时间复杂度O(N)

由于Redis的List是链表结构的,上述的三个命令的算法效率较低,需要对List进行遍历,命令的耗时无法预估,在List长度大的情况下耗时会明显增加,应谨慎使用。

换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现类似ArrayList这样的列表的。如果你不是想要实现一个双端出入的队列,那么请尽量不要使用Redis的List数据结构。

为了更好支持队列的特性,Redis还提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能够实现类似于BlockingQueue的能力,即在List为空时,阻塞该连接,直到List中有对象可以出队时再返回。针对阻塞类的命令,此处不做详细探讨,请参考官方文档(https://redis.io/topics/data-types-intro) 中”Blocking operations on lists”一节。

Hash

Hash即哈希表,Redis的Hash和传统的哈希表一样,是一种field-value型的数据结构,可以理解成将HashMap搬入Redis。 Hash非常适合用于表现对象类型的数据,用Hash中的field对应对象的field即可。 Hash的优点包括:

  • 可以实现二元查找,如”查找ID为1000的用户的年龄”
  • 比起将整个对象序列化后作为String存储的方法,Hash能够有效地减少网络传输的消耗
  • 当使用Hash维护一个集合时,提供了比List效率高得多的随机访问命令

与Hash相关的常用命令:

  • HSET:将key对应的Hash中的field设置为value。如果该Hash不存在,会自动创建一个。时间复杂度O(1)
  • HGET:返回指定Hash中field字段的值,时间复杂度O(1)
  • HMSET/HMGET:同HSET和HGET,可以批量操作同一个key下的多个field,时间复杂度:O(N),N为一次操作的field数量
  • HSETNX:同HSET,但如field已经存在,HSETNX不会进行任何操作,时间复杂度O(1)
  • HEXISTS:判断指定Hash中field是否存在,存在返回1,不存在返回0,时间复杂度O(1)
  • HDEL:删除指定Hash中的field(1个或多个),时间复杂度:O(N),N为操作的field数量
  • HINCRBY:同INCRBY命令,对指定Hash中的一个field进行INCRBY,时间复杂度O(1)

应谨慎使用的Hash相关命令:

  • HGETALL:返回指定Hash中所有的field-value对。返回结果为数组,数组中field和value交替出现。时间复杂度O(N)
  • HKEYS/HVALS:返回指定Hash中所有的field/value,时间复杂度O(N)

上述三个命令都会对Hash进行完整遍历,Hash中的field数量与命令的耗时线性相关,对于尺寸不可预知的Hash,应严格避免使用上面三个命令,而改为使用HSCAN命令进行游标式的遍历,具体请见 https://redis.io/commands/scan

Set

Redis Set是无序的,不可重复的String集合。

与Set相关的常用命令:

  • SADD:向指定Set中添加1个或多个member,如果指定Set不存在,会自动创建一个。时间复杂度O(N),N为添加的member个数
  • SREM:从指定Set中移除1个或多个member,时间复杂度O(N),N为移除的member个数
  • SRANDMEMBER:从指定Set中随机返回1个或多个member,时间复杂度O(N),N为返回的member个数
  • SPOP:从指定Set中随机移除并返回count个member,时间复杂度O(N),N为移除的member个数
  • SCARD:返回指定Set中的member个数,时间复杂度O(1)
  • SISMEMBER:判断指定的value是否存在于指定Set中,时间复杂度O(1)
  • SMOVE:将指定member从一个Set移至另一个Set

慎用的Set相关命令:

  • SMEMBERS:返回指定Hash中所有的member,时间复杂度O(N)
  • SUNION/SUNIONSTORE:计算多个Set的并集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SINTER/SINTERSTORE:计算多个Set的交集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SDIFF/SDIFFSTORE:计算1个Set与1或多个Set的差集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数

上述几个命令涉及的计算量大,应谨慎使用,特别是在参与计算的Set尺寸不可知的情况下,应严格避免使用。可以考虑通过SSCAN命令遍历获取相关Set的全部member(具体请见 https://redis.io/commands/scan ),如果需要做并集/交集/差集计算,可以在客户端进行,或在不服务实时查询请求的Slave上进行。

Sorted Set

Redis Sorted Set是有序的、不可重复的String集合。Sorted Set中的每个元素都需要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。如果多个member拥有相同的score,则以字典序进行升序排序。

Sorted Set非常适合用于实现排名。

Sorted Set的主要命令:

  • ZADD:向指定Sorted Set中添加1个或多个member,时间复杂度O(Mlog(N)),M为添加的member数量,N为Sorted Set中的member数量
  • ZREM:从指定Sorted Set中删除1个或多个member,时间复杂度O(Mlog(N)),M为删除的member数量,N为Sorted Set中的member数量
  • ZCOUNT:返回指定Sorted Set中指定score范围内的member数量,时间复杂度:O(log(N))
  • ZCARD:返回指定Sorted Set中的member数量,时间复杂度O(1)
  • ZSCORE:返回指定Sorted Set中指定member的score,时间复杂度O(1)
  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK则返回按降序排序的排名。时间复杂度O(log(N))
  • ZINCRBY:同INCRBY,对指定Sorted Set中的指定member的score进行自增,时间复杂度O(log(N))

慎用的Sorted Set相关命令:

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名范围内的所有member,ZRANGE为按score升序排序,ZREVRANGE为按score降序排序,时间复杂度O(log(N)+M),M为本次返回的member数
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score范围内的所有member,返回结果以升序/降序排序,min和max可以指定为-inf和+inf,代表返回所有的member。时间复杂度O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名范围/指定score范围内的所有member。时间复杂度O(log(N)+M)

上述几个命令,应尽量避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set做一次性的完整遍历,特别是在Sorted Set的尺寸不可预知的情况下。可以通过ZSCAN命令来进行游标式的遍历(具体请见 https://redis.io/commands/scan ),或通过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以实现游标式的遍历。

Bitmap和HyperLogLog

Redis的这两种数据结构相较之前的并不常用,在本文中只做简要介绍,如想要详细了解这两种数据结构与其相关的命令,请参考官方文档https://redis.io/topics/data-types-intro 中的相关章节

Bitmap在Redis中不是一种实际的数据类型,而是一种将String作为Bitmap使用的方法。可以理解为将String转换为bit数组。使用Bitmap来存储true/false类型的简单数据极为节省空间。

HyperLogLogs是一种主要用于数量统计的数据结构,它和Set类似,维护一个不可重复的String集合,但是HyperLogLogs并不维护具体的member内容,只维护member的个数。也就是说,HyperLogLogs只能用于计算一个集合中不重复的元素数量,所以它比Set要节省很多内存空间。

其他常用命令

  • EXISTS:判断指定的key是否存在,返回1代表存在,0代表不存在,时间复杂度O(1)
  • DEL:删除指定的key及其对应的value,时间复杂度O(N),N为删除的key数量
  • EXPIRE/PEXPIRE:为一个key设置有效期,单位为秒或毫秒,时间复杂度O(1)
  • TTL/PTTL:返回一个key剩余的有效时间,单位为秒或毫秒,时间复杂度O(1)
  • RENAME/RENAMENX:将key重命名为newkey。使用RENAME时,如果newkey已经存在,其值会被覆盖;使用RENAMENX时,如果newkey已经存在,则不会进行任何操作,时间复杂度O(1)
  • TYPE:返回指定key的类型,string, list, set, zset, hash。时间复杂度O(1)
  • CONFIG GET:获得Redis某配置项的当前值,可以使用*通配符,时间复杂度O(1)
  • CONFIG SET:为Redis某个配置项设置新值,时间复杂度O(1)
  • CONFIG REWRITE:让Redis重新加载redis.conf中的配置

数据持久化

Redis提供了将数据定期自动持久化至硬盘的能力,包括RDB和AOF两种方案,两种方案分别有其长处和短板,可以配合起来同时运行,确保数据的稳定性。

必须使用数据持久化吗?

Redis的数据持久化机制是可以关闭的。如果你只把Redis作为缓存服务使用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过来的备份,那么可以关闭Redis的数据持久化机制。 但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:

  • RDB方式的持久化几乎不损耗Redis本身的性能,在进行RDB持久化时,Redis主进程唯一需要做的事情就是fork出一个子进程,所有持久化工作都由子进程完成
  • Redis无论因为什么原因crash掉之后,重启时能够自动恢复到上一次RDB快照中记录的数据。这省去了手工从其他数据源(如DB)同步数据的过程,而且要比其他任何的数据恢复方式都要快
  • 现在硬盘那么大,真的不缺那一点地方

RDB

采用RDB持久方式,Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。可以在配置文件中配置Redis进行快照保存的时机:

save [seconds] [changes]

意为在[seconds]秒内如果发生了[changes]次数据修改,则进行一次RDB快照保存,例如

save 60 100

会让Redis每60秒检查一次数据变更情况,如果发生了100次或以上的数据变更,则进行RDB快照保存。 可以配置多条save指令,让Redis执行多级的快照保存策略。 Redis默认开启RDB快照,默认的RDB策略如下:

save 900 1
save 300 10
save 60 10000

也可以通过BGSAVE命令手工触发RDB快照保存。

RDB的优点:

  • 对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
  • 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
  • 使用RDB文件进行数据恢复比使用AOF要快很多。

RDB的缺点:

  • 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。
  • 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。

AOF

采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。

AOF默认是关闭的,如要开启,进行如下配置:

appendonly yes

AOF提供了三种fsync配置,always/everysec/no,通过配置项[appendfsync]指定:

  • appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
  • appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由后台线程每秒fsync一次

随着AOF不断地记录写操作日志,必定会出现一些无用的日志,例如某个时间点执行了命令SET key1 “abc”,在之后某个时间点又执行了SET key1 “bcd”,那么第一条命令很显然是没有用的。大量的无用日志会让AOF文件过大,也会让数据恢复的时间过长。 所以Redis提供了AOF rewrite功能,可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。 AOF rewrite可以通过BGREWRITEAOF命令触发,也可以配置Redis定期自动进行:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

上面两行配置的含义是,Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增长了100%后,自动进行AOF rewrite。同时如果增长的大小没有达到64mb,则不会进行rewrite。

AOF的优点:

  • 最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。
  • AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。
  • AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。

AOF的缺点:

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 数据恢复速度比RDB慢

内存管理与数据淘汰机制

最大内存设置

默认情况下,在32位OS中,Redis最大使用3GB的内存,在64位OS中则没有限制。

在使用Redis时,应该对数据占用的最大空间有一个基本准确的预估,并为Redis设定最大使用的内存。否则在64位OS中Redis会无限制地占用内存(当物理内存被占满后会使用swap空间),容易引发各种各样的问题。

通过如下配置控制Redis使用的最大内存:

maxmemory 100mb

在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:

  • 根据配置的数据淘汰策略尝试淘汰数据,释放空间
  • 如果没有数据可以淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写请求返回错误,但读请求仍然可以正常执行

在为Redis设置maxmemory时,需要注意:

  • 如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间,如果maxmemory过于接近主机的可用内存,导致数据同步时内存不足。所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。

数据淘汰机制

Redis提供了5种数据淘汰策略:

  • volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
  • allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰
  • volatile-random:随机淘汰数据,只淘汰设定了有效期的key
  • allkeys-random:随机淘汰数据,所有的key都可以被淘汰
  • volatile-ttl:淘汰剩余有效期最短的key

最好为Redis指定一种有效的数据淘汰策略以配合maxmemory设置,避免在内存使用满后发生写入失败的情况。

一般来说,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,并且能够热加载的数据(比如缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),可以设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。

配置方法:

maxmemory-policy volatile-lru   #默认是noeviction,即不进行数据淘汰

Pipelining

Pipelining

Redis提供许多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,这些命令存在的意义是减少维护网络连接和传输数据所消耗的资源和时间。 例如连续使用5次SET命令设置5个不同的key,比起使用一次MSET命令设置5个不同的key,效果是一样的,但前者会消耗更多的RTT(Round Trip Time)时长,永远应优先使用后者。

然而,如果客户端要连续执行的多次操作无法通过Redis命令组合在一起,例如:

SET a "abc"
INCR b
HSET c name "hi"

此时便可以使用Redis提供的pipelining功能来实现在一次交互中执行多条命令。 使用pipelining时,只需要从客户端一次向Redis发送多条命令(以\r\n)分隔,Redis就会依次执行这些命令,并且把每个命令的返回按顺序组装在一起一次返回,比如:

$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

大部分的Redis客户端都对Pipelining提供支持,所以开发者通常并不需要自己手工拼装命令列表。

Pipelining的局限性

Pipelining只能用于执行连续且无相关性的命令,当某个命令的生成需要依赖于前一个命令的返回时,就无法使用Pipelining了。

通过Scripting功能,可以规避这一局限性

事务与Scripting

Pipelining能够让Redis在一次交互中处理多条命令,然而在一些场景下,我们可能需要在此基础上确保这一组命令是连续执行的。

比如获取当前累计的PV数并将其清0

> GET vCount
12384
> SET vCount 0
OK

如果在GET和SET命令之间插进来一个INCR vCount,就会使客户端拿到的vCount不准确。

Redis的事务可以确保复数命令执行时的原子性。也就是说Redis能够保证:一个事务中的一组命令是绝对连续执行的,在这些命令执行完成之前,绝对不会有来自于其他连接的其他命令插进去执行。

通过MULTI和EXEC命令来把这两个命令加入一个事务中:

> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK

Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。

可以使用DISCARD命令放弃当前的事务,将保存的命令队列清空。

需要注意的是,Redis事务不支持回滚: 如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误。 但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚。

通过事务实现CAS

Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。

假设要实现将某个商品的状态改为已售:

if(exec(HGET stock:1001 state) == "in stock")
    exec(HSET stock:1001 state "sold");

这一伪代码执行时,无法确保并发安全性,有可能多个客户端都获取到了”in stock”的状态,导致一个库存被售卖多次。

使用WATCH命令和事务可以解决这一问题:

exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
    exec(MULTI);
    exec(HSET stock:1001 state "sold");
    exec(EXEC);
}

WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。

Scripting

通过EVAL与EVALSHA命令,可以让Redis执行LUA脚本。这就类似于RDBMS的存储过程一样,可以把客户端与Redis之间密集的读/写交互放在服务端进行,避免过多的数据交互,提升性能。

Scripting功能是作为事务功能的替代者诞生的,事务提供的所有能力Scripting都可以做到。Redis官方推荐使用LUA Script来代替事务,前者的效率和便利性都超过了事务。

关于Scripting的具体使用,本文不做详细介绍,请参考官方文档 https://redis.io/commands/eval

Read More

redis io复用

首先把reactor和proactor流程图抄过来

反应器Reactor Reactor模式结构

Reactor

Reactor包含如下角色:

Handle 句柄;用来标识socket连接或是打开文件;
Synchronous Event Demultiplexer:同步事件多路分解器:由操作系统内核实现的一个函数;用于阻塞等待发生在句柄集合上的一个或多个事件;(如select/epoll;)
Event Handler:事件处理接口
Concrete Event HandlerA:实现应用程序所提供的特定事件处理逻辑;
Reactor:反应器,定义一个接口,实现以下功能: 
1)供应用程序注册和删除关注的事件句柄; 
2)运行事件循环; 
3)有就绪事件到来时,分发事件到之前注册的回调函数上处理;

“反应”器名字中”反应“的由来: “反应”即“倒置”,“控制逆转”,具体事件处理程序不调用反应器,而是由反应器分配一个具体事件处理程序,具体事件处理程序对某个指定的事件发生做出反应;这种控制逆转又称为“好莱坞法则”(不要调用我,让我来调用你) 业务流程及时序图

seq_Reactor

应用启动,将关注的事件handle注册到Reactor中;
调用Reactor,进入无限事件循环,等待注册的事件到来;
事件到来,select返回,Reactor将事件分发到之前注册的回调函数中处理;

主动器Proactor Proactor模式结构

Proactor

Proactor主动器模式包含如下角色

Handle 句柄;用来标识socket连接或是打开文件;
Asynchronous Operation Processor:异步操作处理器;负责执行异步操作,一般由操作系统内核实现;
Asynchronous Operation:异步操作
Completion Event Queue:完成事件队列;异步操作完成的结果放到队列中等待后续使用
Proactor:主动器;为应用程序进程提供事件循环;从完成事件队列中取出异步操作的结果,分发调用相应的后续处理逻辑;
Completion Handler:完成事件接口;一般是由回调函数组成的接口;
Concrete Completion Handler:完成事件处理逻辑;实现接口定义特定的应用处理逻辑;

业务流程及时序图

seq_Proactor

应用程序启动,调用异步操作处理器提供的异步操作接口函数,调用之后应用程序和异步操作处理就独立运行;应用程序可以调用新的异步操作,而其它操作可以并发进行;
应用程序启动Proactor主动器,进行无限的事件循环,等待完成事件到来;
异步操作处理器执行异步操作,完成后将结果放入到完成事件队列;
主动器从完成事件队列中取出结果,分发到相应的完成事件回调函数处理逻辑中;

对比两者的区别 主动和被动

以主动写为例: Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑; Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;

可以看出,Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作; Proactor直接调用异步读写操作,调用完后立刻返回; 实现

Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;

Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响) 优点

Reactor实现相对简单,对于耗时短的处理场景处理高效; 操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性; 事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁; 事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,

Proactor性能更高,能够处理耗时长的并发场景; 缺点

Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;

Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现; 适用场景

Reactor:同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;

Proactor:异步接收和同时处理多个服务请求的事件驱动程序

reference

  • redis与reactor模式。这个文章讲的不错。里面的论文值得读一下。http://www.dengshenyu.com/%E5%90%8E%E7%AB%AF%E6%8A%80%E6%9C%AF/2016/01/09/redis-reactor-pattern.html
    • http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
  • reactor和proactor https://blog.csdn.net/wanbf123/article/details/78062802

todo list

  • 读参考链接中的论文
Read More

LD_PRELOAD为何不能劫持printf

环境gcc linux

简而言之是gcc在某些时刻会优化掉printf,优化成puts


下面是牢骚。群友们针对printf咋被优化的进行了探讨,可能是弱符号,或者涉及到变参的复杂场景,以及printf可以使用寄存器作为参数。一顿天马行空

这个链接具有一定的参考性

特别摘抄过来,作为后续分析glibc的一个参考思路

printf的代码在哪里?

显然,Helloworld的源代码需要经过编译器编译,操作系统的加载才能正确执行。而编译器包含预编译、编译、汇编和链接四个步骤。

#include<stdio.h>
int main()
{
​    **printf("Hello World !\n");**
​    return 0;
}

首先,预编译器处理源代码中的宏,比如#include。预编译结束后,我们发现printf函数的声明。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -E -quiet
​ main.c -o main.i # 1 “main.c” # 1 “<命令行>" \# 1 "main.c" ... extern int printf (const char *__restrict __format, ...); ...

int main() { printf(“Hello World!\n”); return 0; }

然后编译器将高级语言程序转化为汇编代码。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet  \
​    main.i -o main.s
​    .file      "main.c"
​    .section   .rodata
.LC0:
​    .string    "Hello World!"
​    .text
​    .globl     main
​    .type      main, @function
main:
​    pushl      %ebp
​    movl       %esp,  %ebp
​    andl       $-16,  %esp
​    subl       $16,   %esp
​    movl       $.LC0, (%esp)
​    `call       puts`
​    movl       $0,    %eax
​    leave
​    ret
​    .size      main, .-main
...

我们发现printf函数调用被转化为call puts指令,而不是call printf指令,这好像有点出乎意料。不过不用担心,这是编译器对printf的一种优化。实践证明,对于printf的参数如果是以’\n’结束的纯字符串,printf会被优化为puts函数,而字符串的结尾’\n’符号被消除。除此之外,都会正常生成call printf指令。

如果我们仍希望通过printf调用”Hello World !\n”的话,只需要按照如下方式修改即可。不过这样做就不能在printf调用结束后立即看到打印字符串了,因为puts函数可以立即刷新输出缓冲区。我们仍然使用puts作为例子继续阐述。

    .section   .rodata
.LC0:
​    .string    "hello world!\n"
​    ...
​    call       printf
...

接下来,汇编器开始工作。将汇编文件转化为我们不能直接阅读的二进制格式——可重定位目标文件,这里我们需要gcc工具包的objdump命令查看它的二进制信息。可是我们发现call puts指令里保存了无效的符号地址。

$as -o main.o main.s
$objdump –d main.o
main.o:     文件格式 elf32-i386
Disassembly of section .text:
00000000 <main>:
   0:  55                     push   %ebp
   1:  89 e5                  mov    %esp,%ebp
   3:  83 e4 f0               and    $0xfffffff0,%esp
   6:  83 ec 10               sub    $0x10,%esp
   9:  c7 04 24 00 00 00 00   movl   $0x0,(%esp)
  10:  e8 fc ff ff ff         call   11 <main+0x11>
  15:  b8 00 00 00 00         mov    $0x0,%eax
  1a:  c9                     leave  
  1b:  c3                     ret

而链接器最终会将puts的符号地址修正。由于链接方式分为静态链接和动态链接两种,虽然链接方式不同,但是不影响最终代码对库函数的调用。我们这里关注printf函数背后的原理,因此使用更易说明问题的静态链接的方式阐述。

$/usr/lib/gcc/i686-linux-gnu/4.7/collect2                   \
​    -static -o main                                         \
​    /usr/lib/i386-linux-gnu/crt1.o                          \
​    /usr/lib/i386-linux-gnu/crti.o                          \
​    /usr/lib/gcc/i686-linux-gnu/4.7/crtbeginT.o             \
​    main.o                                                  \
​    --start-group                                           \
​    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc.a                \
​    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc_eh.a             
​    /usr/lib/i386-linux-gnu/libc.a                          \
​    --end-group                                             \
​    /usr/lib/gcc/i686-linux-gnu/4.7/crtend.o                \
​    /usr/lib/i386-linux-gnu/crtn.o

$objdump –sd main

Disassembly of section .text:

...

08048ea4 <main>:
 8048ea4:  55                     push   %ebp
 8048ea5:  89 e5                  mov    %esp,%ebp
 8048ea7:  83 e4 f0               and    $0xfffffff0,%esp
 8048eaa:  83 ec 10               sub    $0x10,%esp
 8048ead:  c7 04 24 e8 86 0c 08   movl   $0x80c86e8,(%esp)
 8048eb4:  e8 57 0a 00 00         call   8049910 <_IO_puts>
 8048eb9:  b8 00 00 00 00         mov    $0x0,%eax
 8048ebe:  c9                     leave  
 8048ebf:  c3                     ret
...

静态链接时,链接器将C语言的运行库(CRT)链接到可执行文件,其中crt1.o、crti.o、crtbeginT.o、crtend.o、crtn.o便是这五个核心的文件,它们按照上述命令显示的顺序分居在用户目标文件和库文件的两侧。由于我们使用了库函数puts,因此需要库文件libc.a,而libc.a与libgcc.a和libgcc_eh.a有相互依赖关系,因此需要使用-start-group和-end-group将它们包含起来。

链接后,call puts的地址被修正,但是反汇编显示的符号是_IO_puts而不是puts!难道我们找的文件不对吗?当然不是,我们使用readelf命令查看一下main的符号表。竟然发现puts和_IO_puts这两个符号的性质是等价的!objdump命令只是显示了全局的符号_IO_puts而已。

$readelf main –s
Symbol table '.symtab' contains 2307 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
...
  1345: 08049910   352 FUNC    WEAK   DEFAULT    6 puts
...
  1674: 08049910   352 FUNC    GLOBAL DEFAULT    6 _IO_puts
...

那么puts函数的定义真的是在libc.a里吗?我们需要对此确认。我们将libc.a解压缩,然后全局符号_IO_puts所在的二进制文件,输出结果为ioputs.o。然后查看该文件的符号表。发现ioputs.o定义了puts和_IO_puts符号,因此可以确定ioputs.o就是puts函数的代码文件,且在库文件libc.a内。

$ar -x /usr/lib/i386-linux-gnu/libc.a
$grep -rin "_IO_puts" *.o
​    $readelf -s ioputs.o
Symbol table '.symtab' contains 20 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
...
​    11: 00000000   352 FUNC    GLOBAL DEFAULT    1 _IO_puts
...
​    19: 00000000   352 FUNC    WEAK   DEFAULT    1 puts

二、printf的调用轨迹

我们知道对于”Hello World !\n”的printf调用被转化为puts函数,并且我们找到了puts的实现代码是在库文件libc.a内的,并且知道它是以二进制的形式存储在文件ioputs.o内的,那么我们如何寻找printf函数的调用轨迹呢?换句话说,printf函数是如何一步步执行,最终使用Linux的int 0x80软中断进行系统调用陷入内核的呢?

如果让我们向终端输出一段字符串信息,我们一般会使用系统调用write()。那么打印Helloworld的printf最终是这样做的吗?我们借助于gdb来追踪这个过程,不过我们需要在编译源文件的时候添加-g选项,支持调试时使用符号表。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet -g\

​ main.i -o main.s

然后使用gdb调试可执行文件。

$gdb ./main

(gdb)break main

(gdb)run

(gdb)stepi

在main函数内下断点,然后调试执行,接着不断的使用stepi指令执行代码,直到看到Hello World !输出为止。这也是为什么我们使用puts作为示例而不是使用printf的原因。

(gdb)

0xb7fff419 in __kernel_vsyscall ()

(gdb)

Hello World!

我们发现Hello World!打印位置的上一行代码的执行位置为0xb7fff419。我们查看此处的反汇编代码。

(gdb)disassemble
Dump of assembler code for function __kernel_vsyscall:
   0xb7fff414 <+0>:  push   %ecx
   0xb7fff415 <+1>:  push   %edx
   0xb7fff416 <+2>:  push   %ebp
   0xb7fff417 <+3>:  mov    %esp,%ebp
   0xb7fff419 <+5>:  sysenter
   0xb7fff41b <+7>:  nop
   0xb7fff41c <+8>:  nop
   0xb7fff41d <+9>:  nop
   0xb7fff41e <+10>: nop
   0xb7fff41f <+11>: nop
   0xb7fff420 <+12>: nop
   0xb7fff421 <+13>: nop
   0xb7fff422 <+14>: int    $0x80
=> 0xb7fff424 <+16>: pop    %ebp
   0xb7fff425 <+17>: pop    %edx
   0xb7fff426 <+18>: pop    %ecx
   0xb7fff427 <+19>: ret    
End of assembler dump.

我们惊奇的发现,地址0xb7fff419正是指向sysenter指令的位置!这里便是系统调用的入口。如果想了解这里为什么不是int 0x80指令,请参考文章《Linux 2.6 对新型 CPU 快速系统调用的支持》。或者参考Linus在邮件列表里的文章《Intel P6 vs P7 system call performance》

系统调用的位置已经是printf函数调用的末端了,我们只需要按照函数调用关系便能得到printf的调用轨迹了。

(gdb)backtrace
#0  0xb7fff424 in __kernel_vsyscall ()
#1  0x080588b2 in __write_nocancel ()
#2  0x0806fa11 in _IO_new_file_write ()
#3  0x0806f8ed in new_do_write ()
#4  0x080708dd in _IO_new_do_write ()
#5  0x08070aa5 in _IO_new_file_overflow ()
#6  0x08049a37 in puts ()
#7  0x08048eb9 in main () at main.c:4

我们发现系统调用前执行的函数是__write_nocancel,它执行了系统调用__write!

三、printf源码阅读

虽然我们找到了Hello World的printf调用轨迹,但是仍然无法看到函数的源码。跟踪反汇编代码不是个好主意,最好的方式是直接阅读glibc的源代码!我们可以从官网下载最新的glibc源代码(glibc-2.18)进行阅读分析,或者直接访问在线源码分析网站LXR。然后按照调用轨迹的的逆序查找函数的调用点。

1.puts 调用 _IO_new_file_xsputn

具体的符号转化关系为:_IO_puts => _IO_sputn => _IO_XSPUTN => __xsputn => _IO_file_xsputn => _IO_new_file_xsputn

$cat ./libio/ioputs.c
int
_IO_puts (str)
​     const char *str;
{
  int result = EOF;
  _IO_size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);

  if ((_IO_vtable_offset (_IO_stdout) != 0
​       || _IO_fwide (_IO_stdout, -1) == -1)
​      && **_IO_sputn (_IO_stdout, str, len)** == len
​      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
​    result = MIN (INT_MAX, len + 1);
  _IO_release_lock (_IO_stdout);
  return result;
}
#ifdef weak_alias
weak_alias (_IO_puts, puts)
#endif

这里注意weak_alias宏的含义,即将puts绑定到符号_IO_puts,并且puts符号为weak类型的。这也就解释了puts符号被解析为_IO_puts的真正原因。

2._IO_new_file_xsputn 调用 _IO_new_file_overflow

具体的符号转化关系为:_IO_new_file_xsputn => _IO_OVERFLOW => __overflow => _IO_new_file_overflow

$cat ./libio/fileops.c
_IO_size_t
_IO_new_file_xsputn (f, data, n)
​     _IO_FILE *f;
​     const void *data;
​     _IO_size_t n;
{
 ...
  if (to_do + must_flush > 0)
​    {
​      _IO_size_t block_size, do_write;
​      /* Next flush the (full) buffer. */
​      if (**_IO_OVERFLOW (f, EOF)** == EOF)
​    /* If nothing else has to be written or nothing has been written, we
​       must not signal the caller that the call was even partially
​       successful.  */
​    return (to_do == 0 || to_do == n) ? EOF : n - to_do;
...

3._IO_new_file_overflow 调用 _IO_new_do_write

具体的符号转化关系为:_IO_new_file_overflow =>_IO_do_write =>_IO_new_do_write

$cat ./libio/fileops.c
int
_IO_new_file_overflow (f, ch)
​      _IO_FILE *f;
​      int ch;
{
 ...
  if (INTUSE(**_IO_do_write**) (f, f->_IO_write_base,
  f->_IO_write_ptr - f->_IO_write_base) == EOF)
  return EOF;
  return (unsigned char) ch;
}

4. _IO_new_do_write 调用 new_do_write

具体的符号转化关系为:_IO_new_do_write => new_do_write

$cat ./libio/fileops.c
int
_IO_new_do_write (fp, data, to_do)
​     _IO_FILE *fp;
​     const char *data;
​     _IO_size_t to_do;
{
  return (to_do == 0
​      || (_IO_size_t) **new_do_write** (fp, data, to_do) == to_do) ? 0 : EOF;
}

5. new_do_write调用 _IO_new_file_write

具体的符号转化关系为:new_do_write =>_IO_SYSWRITE => __write() => write() => _IO_new_file_write

$cat ./libio/fileops.c
_IO_size_t
new_do_write (fp, data, to_do)
_IO_FILE *fp;
const char *data;
_IO_size_t to_do;
{
 ...
  count = **_IO_SYSWRITE** (fp, data, to_do);
  if (fp->_cur_column && count)
  fp->_cur_column = INTUSE(_IO_adjust_column) (fp->_cur_column - 1, data, count) + 1;
 ...
}

6. _IO_new_file_write调用 write_nocancel

具体的符号转化关系为:_IO_new_file_write=>write_not_cancel => write_nocancel

$cat ./libio/fileops.c
_IO_ssize_t
_IO_new_file_write (f, data, n)
_IO_FILE *f;
const void *data;
_IO_ssize_t n;
{
 _IO_ssize_t to_do = n;
  while (to_do > 0)
  {
​    _IO_ssize_t count = (__builtin_expect (f->_flags2& _IO_FLAGS2_NOTCANCEL, 0)? **write_not_cancel** (f->_fileno, data, to_do): write (f->_fileno, data, to_do));
...
}

7. write_nocancel 调用 linux-gate.so::__kernel_vsyscall

具体的符号转化关系为:write_nocancel => INLINE_SYSCALL => INTERNAL_SYSCALL =>__kernel_vsyscall

注意 linux-gate.so在磁盘上并不存在,它是内核镜像中的特定页,由内核编译生成。关于它的更多信息,可以参考文章《linux-gate.so技术细节》和《What is linux-gate.so.1?》

Read More

^