深入理解计算机系统 读书笔记


csapp 不多说了。早就该读完了


计算机系统漫游

  • 信息就是位 + 上下文

  • 程序被程序翻译成不同的格式

  • 了解编译系统

    • 优化系统性能
    • 看懂链接报错
    • 避免安全漏洞
  • 处理器读并解释存储在存储器中的指令

    • 典型的系统硬件组织

    image-20200328101740941

    总线bus 传word

    IO

    内存 DRAM

    cache SRAM

程序结构和执行

信息的表示和处理

信息存储

  • 寻址和字节顺序

    • 大端小端
    01 05 64 94 04 08 add %eax, 0x8049464
    
    • 整数和浮点的不同表达

      image-20200328113653117

    • 布尔袋鼠 ,布尔环,位运算

    • 无符号与二进制补码 -128 127

      • 无论哪种解码方式, 都是字节码的解释而已
    • 扩展一个数字的位表示

      • 零扩展
      • 符号扩展
  • 浮点

程序的机器级表示 基于IA32

  • 反汇编

  • 数据格式

    • 字word 16位 double word 32位

    • 三种GAS后缀,movb 字节 movwmovl双字

    • 操作指示符 operand

      • 立即数 immediate $0x1F
      • 寄存器 eax
      • 存储器引用 地址
    • 数据传送指令

      • mov
      movl $0x4050, %eax 立即数 ->寄存器
      movl %ebp, %esp 寄存器->寄存器
      movl (%edi,%ecx), %eax 内存->寄存器
      movl $-18, (%esp) 立即数->内存
      movl %eax, -12(%ebp) 寄存器->内存
      • push %ebp
      subl $4, %esp
      movl %ebp,(%esp)
      
      • pop
      movl (%esp), %eax
      addl $4, %esp
      
  • 算术和逻辑操作

    • 加载有效地址 lea aka load effective address 我还以为是leave

    • xor %eax ,%eax 赋0

      指令 效果 描述
      leal S, D D = &S 加载有效地址
      incl/decl/negl/notl D D=D@1 加一减一
      add/sub/imul/xor/or/and S,D    
  • 控制

    • 条件码
      • cmp test set
    • 跳转
      • jmp je jg jl ja jb
      • 跳转表
  • 过程

    • 栈帧

    • 转移控制

      • call leave ret

        ;leave 
        movl %ebp, %esp
        popl %ebp
        
    • 寄存器使用惯例

      • %ebp %esp 必须保存
      • %eax %edx %ecx调用者保存寄存器caller save %ebx %esi %edi 被调用者保存寄存器 callee save
    • 递归

  • 数组?

    • movl (%edx, %eax, 4), %eax

    • 指针运算 假设数组E的起始地址和索引i分别在%edx%ecx

      表达式 类型 汇编
      E int * xe movl %edx, %eax
      E[0] int M[xe] movl (%edx), %eax
      E[i] int M[xe + 4i] movl (%edx, %ecx, 4), %eax
      &E[2] int * xe+8 leal 8(%edx), %eax
      *(&E[i] + i) int M[xe+ 4i + 4i] movl (%edx, %ecx), %eax
      &E[i]-E int i movl %ecx, %eax
    • 数组循环

      • 减少imul
      • 优化递增变量
  • 异类数据结构

    • 编译时算好偏移
  • 理解指针

    • 越界
  • 浮点算术

    • 浮点寄存器
    • 栈表达式求值用到的寄存器
  • c中嵌入汇编

处理器体系结构

  • 定义一个简单的指令集,就叫它Y86吧

    • %eax, %ecx $edx %esi $edi %esp %ebp
    • 条件码 ZF SF OF
    • 程序计数器PC
    • 存储器,内存,数组代替
    • 汇编设计
      • mov按照mov的类别设计 irmovl(立即数 →寄存器) rrmovl(寄存器→寄存器) rmmovl(寄存器→内存) mrmovl (内存→寄存器)
      • opl addd sub and xor
      • jxx
      • call ,ret
      • pushl, popl
  • 逻辑电路

    • 布尔逻辑
    • 存储器和时钟控制
    • 集合关系
  • Y86的顺序实现

    • 取指,fetch 从PC中拿到地址,从地址抽出指令指示符 icode ifun

    • 解码decode 从寄存器里读

    • 执行 execute 算术逻辑单元(ALU)执行指令指明的动作(ifun)或者移动栈指针或者跳转

    • 访问内存memory

    • 写回寄存器

    • 更新PC

      阶段 opl rA, rB subl %edx, %edx
      取指 icode:ifun <- M[PC]
      rA:rB <- M[PC+1]
      valP <- PC+2
      icode:ifuicode:ifun <- M[0x00c] = 6:1
      rA:rB <- M[0x00d] = 2:3
      valP <- 0x00c+2 = 0x00e
      解码 valA <- R[rA]
      valB <- R[rB]
      valA <- R[%edx] = 9
      valB <- R[%ebx] = 21
      执行 valE <- valB op valA
      Set CC
      valE <- 21 - 9 = 12
      ZF <- 0, SF <- 0 , OF <- 0
      访存    
      写回 R[rB] <- valE R[rB] <- valE = 12
      更新PC PC <- valP PC <- valP=0x00e

    其他流程类似,不过是获取值通过ALU实现

    image-20200402171439048

image-20200402171808118

一种实现image-20200402172202210

image-20200402172236476

  • 时序

    image-20200402172644242

    后面在设计电路了。理解PC

  • 流水线的通用原理

    • 时钟驱动组合逻辑
    • 流水线过深,收益反而下降
    • 指令控制
    • 指令重排
    • 预测PC
      • 分支预测
    • hazard
      • data hazard
        • 读写阶段不同造成寄存器错误
        • 条件码
        • PC
        • 存储器写回
      • control hazard
      • 解决方案
        • stall nop nop 吞吐降低
        • 转发

第四章真是太他妈复杂了。讲cpu 指令集 流水线怎么设计

优化程序性能

  • 优化编译器的能力和局限

    *xp += *yp; *xp+=&yp;*xp += 2* *yp 在xp=yp时优化效果不同,因为编译器必须假定指针使用不同的寄存器memory aliasing

    • 表示程序性能,CPE
  • 消除循环

    • 循环不变量优化,经典问题了。
    • 减少调用
    • 消除不必要的寄存器引用
      • 循环中访问指针可能不会被优化,导致多次访问指针。
    • 降低循环开销
  • 转换到指针代码

    • 通过指针改善数组效率?
  • 提高并行度

    • 循环展开
      • 这种优化的性能提升有上限。为什么?
  • 分支预测和预判错误处罚

  • 理解内存性能

    • save & load
    • 设计。数据结构和算法影响性能
    • 消除连续函数调用(浪费堆栈)以及计算放到循环外,不必要的寄存器引用以及引入临时变量保存中间结果
    • 循环展开,迭代分割,数组换指针
  • 确认和消除性能瓶颈

    • profiling -pg

内存层次结构

  • 存储技术

    • DRAM 实现内存
    • SRAM 实现cache 弥补cpu内存之间的的差距,带来局部性
  • 局部性

    • 利用局部性
  • 内存层次结构

    • cache 读成cash,卧槽,我一直读错了??
      • cache的命中,缓存策略,置换策略LRU等等。这里的概念讲的很少。有专门的书讲这个
  • cache

    • cache set, cache line
  • direct-mapped cache
    • 字选择,
    • 行替换,击中直接替换当前行
      • 这里细节比较复杂,还好有图说明。可以找出pdf来看。这里不解释了。看不懂。后面还有书来讲这个,到时候仔细看
  • set associative cache组相联
    • 行替换,LRU/LFU
  • fully associative cache
    • 做TLB
    • 怎么写
      • 写回write back 需要标志位dirty bit记录cache block是否被修改过
      • 直写 write through
    • 写不命中
      • 写分配or写不分配
    • L1 icache dcache L2 university cache
    • 性能指标
      • 命中率/不命中率
      • 命中时间
      • 不命中惩罚 miss penalty
      • 影响因素
        • cache越大越容易命中,但是增加命中时间
        • 块儿越大命中率高一些,提高空间局部性,但是占用大,行数少,降低时间局部性
        • 相连度,高相联度带来慢的速度,增加命中时间,但是高相联度冲突不命中影响低
          • L1直接映射,L2组项联,L3直接映射
        • 写策略,直写快但是浪费,写回带宽高但是复杂
          • 下层cache多用写回
    • cache-friendly
    • cache 对性能的影响

在系统上运行程序

链接

链接基本上都看过了。简单抽取新点子。不详细记录了

  • 静态链接

    • 符号解析
    • 重定位
  • 符号表

    • bss 记成better save space
    • name mangling,直译确实很糟糕。毁坏。。这个毁坏不是destroy,是把布压碎的那种毁坏。意译好一点或者别翻译了
    • 全局符号处理,强弱符号。符号覆盖(hook)
    • 符号依赖,注意库在后面。放在前面就被忽略

异常控制流

  • 异常处理

    • 异常表

      类别 原因 异步? 返回行为
      中断 IO设备的信号 异步 总是返回到下一条指令
      陷阱 syscall 有意的异常 同步 总是返回到下一条指令
      故障 缺页错误 潜在可恢复的错误 同步 可能返回到当前指令
      终止 硬件错误 不可恢复的错误 同步 不会返回
      异常号 描述 异常类型
      0 除法错误 故障
      13 一般保护故障 故障
      14 缺页 故障
      18 机器检查 终止
      32~127 操作系统定义的异常 中断或陷阱
      128 系统调用 陷阱
      129~255 操作系统定义的异常 中断或陷阱
  • 进程,多任务,抢占。时间片
    • 用户模式和内核博士
      • /proc 访问内核数据 /proc/pid/maps老朋友 了
    • 上下文切换
      • 中断污染cache
  • 系统调用和错误处理,错误会返回-1且标记errno,记得处理
  • 进程控制
    • 创建/终止,获取pid等等
      • fork细节
        • 创建一次返回两次
        • 并发执行
        • 相同但是独立的地址空间,基于COW
        • 文件共享,注意关闭
    • 回收子进程
    • 加载运行程序
  • 信号
    • 信号处理问题
      • 信号被阻塞,直接返回
      • 待处理的信号不会排队等待
      • 系统调用可以被中断
        • 有的系统被中断不重新运行
      • sigaction和setjmp处理信号。
号码 名字 默认行为 相应事件
1 SIGHUP 终止 终端挂起
2 SIGINT 终止 键盘中断
3 SIGQUIT 终止 键盘退出
4 SIGILL 终止 非法之灵
5 SIGTRAP 终止并dumping core 跟踪陷阱
6 SIGABRT 终止并dumping core abort函数信号
7 SIGBUS 终止 bus err
8 SIGPFE 终止并dumping core 浮点异常
9 SIGKILL 终止 杀死程序
10 SIGUSR1 终止 用户自定义
11 SIGSEGV 终止并dumping core 段错误
12 SIGUSR2 终止 用户自定义
13 SIGPIPE 终止 向一个没有读的pipe里写,broken pipe
14 SIGALRM 终止 alarm函数信号
15 SIGTERM 终止 软件中指
16 SIGSTKFLT 终止 栈故障
17 SIGCHLD 忽略 子进程暂停or终止
18 SIGCONT 忽略 继续进程如果被暂停或终止
19 SIGSTOP 停止,直到下一个SIGCONT 不来自终端的暂停
20 SIGTSTP 停止,直到下一个SIGCONT 来自终端的暂停
21 SIGTTIN 停止,直到下一个SIGCONT 后台从终端读
22 SIGTTOU 停止,直到下一个SIGCONT 后台向终端写
23 SIGURG 忽略 socket紧急
24 SIGXCPU 终止 CPU时间限制超出
25 SIGXFSZS 终止 文件大小限制超出
26 SIGVTALRM 终止 虚拟定时器期满
27 SIGPROF 终止 剖析定时器期满
28 SIGWINCH 忽略 窗口大小变化
29 SIGIO 终止 某个描述符上可执行IO操作
30 SIGPWR 终止 电源故障
  • 操作进程工具
    • strace 分析调用
    • ps看进程 top看资源
    • /proc

测量程序执行时间

主要介绍了采集数据的方法 counter,多次测量,基于gettimeofday精确时间等。非常粗略

虚拟内存

  • 虚拟寻址 MMU做转换

  • VM 虚拟地址数组 映射VP

    • VP三个状态,未分配,缓存,未缓存
  • DRAM比SRAM慢十倍,相比缓存不命中要昂贵很多

    • 全相联,任何虚拟页VP可以放在任何物理页PP中
    • 替换策略很重要,替换错虚拟页代价(处罚)非常高
    • 总是写回
  • 页表

    • 在上面的场景下,VP都在分配磁盘,可能缓存在内存中,(在内存中也可能有对应的物理页也有可能没有),管理各种VP,需要页表

      • 子概念,页表项。注意下图的包含关系 image-20200409144609754
    • 页命中,上图中击中DRAM页,就会分配物理页,也就是命中

    • 不命中,就是缺页,page fault,假如上图中击中vp3,就会牺牲一个vp,上面场景,没被访问的vp4倍淘汰,更新vp3

    • 局部性,良好工作机不会产生磁盘流量,以及内存颠簸thrashing,频繁换入换出

  • VM 管理功能

    • 多进程不同页表,物理页可共享
    • 简化链接,简化共享代码段,简化内存分配,简化加载
  • VM作为内存保护工具

    • 页表,页表项标志位控制,以及段错误保护
  • 地址翻译

    image-20200409152413978

    • 结合cache和VM内容较细,不讨论
    • 利用TLB(translation lookaside buffer,不是透明大页)加速地址翻译,缓存一波vpe,代替走l1 cache

      image-20200409152832264image-20200409152844756

  • 多级页表

  • 内存映射

  • 动态内存分配,以及碎片

    • 分配器设计
    • 伙伴系统
  • 垃圾收集

    • 标记清除法
      • 有向可达图
  • 常见内存错误

    • 写错误地址,读未初始化内存,栈溢出,指针引用错误,误解指针运算,引用不存在的对象, 内存泄漏等等

程序间的交互与通信

这部分内容和一些书重复。简单列举一些没注意过的/重要的点

系统级IO

  • 一切都是文件
  • size_t ssize_t系统函数多用ssize_t, 错误处理返回值是-1
  • read write的正确用法,这段代码很常见了
ssize_t readn(inf fd, void *usrbuf, size_t n){
    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;
    while (nleft > 0) {
        if ((nread = read(fd, bufp, nleft)) < 0) {
            if (errno = EINTR)
                nread = 0;
            else
                return -1;
        } else if(nread == 0)
            break;
        nleft -= nread;
        bufp += nread;
    }
    return (n - nleft);
}

ssize_t writen(inf fd, void *usrbuf, size_t n){
    size_t nleft = n;
    ssize_t nwritten;
    char *bufp = usrbuf;
    while (nleft > 0) {
        if ((nwritten = write(fd, bufp, nleft)) < 0) {
            if (errno = EINTR)
                nwritten = 0;
            else
                return -1;
        } else if(nread == 0)
            break;
        nleft -= nwritten;
        bufp += nwritten;
    }
    return n;
}
  • 读取元数据
  • 共享文件
    • 描述符表 文件表 vnode表
    • 子进程集成fd,注意关闭
  • IO重定向 dup2

网络编程

  • cs模型
  • 网络api
  • socket api
    • socket拿到fd
    • connect阻塞 对应tcp哪个阶段?
    • bind server端绑定
    • listen,server使用,等着
    • accept,server端真正的接受了client。accept返回前在tcp哪个阶段?
  • web服务器以及http事务
    • 状态码
    • get post 等api

并发编程

  • 基于进程的并发模型
    • 进程间IPC
    • 处理子进程
      • 处理SIGCHILD,在handler里waitpid
      • fork之前的fd需要关闭两遍
  • 基于IO多路复用的并发模型
    • select
      • while select, 也不高效
      • 事件驱动,写好handler,缺点是比较分散比较复杂
  • 基于线程的并发模型
    • 创建线程,detach干活。浪费
    • 共享变量?需要线程之间同步,信号量,锁等等
    • 基于线程的IO多路复用
  • 并发性问题
    • 线程安全
      • 共享变量保护
      • 不可重入函数
      • 静态变量指针?
    • 竞争
    • 死锁

ref


Read More

julia笔记

由于julia和python/ruby/perl非常像,没什么可以整理的,所以这里只做记录备忘

环境搭建

安装julia 进入repl 按]进入安装包模式

add IJulia
Read More

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

^