hhhzua 发布的文章

slice的结构:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  • array 属性指向一个数组, 占用8个字节,Go中数组是固定长度的
  • len 属性则是当前slice的元素数量,占用8个字节
  • cap 属性是cap的容量,占用8个字节,也就是当前这个切片最大可容纳的元素数量

slice的初始化:

  1. var a []int

    
    使用 `var` 关键字直接申明
    
  2. a := []int{1,2,3}

    
    使用字面量直接`:=` 
    
  3. a := make([]int, 1)

    
    使用make
    
  4. a := b[1:2]

    
    从其他切片中截取
    
    

slice的浅拷贝与深拷贝:

  • 浅拷贝就是只拷贝当前数据的地址,新旧对象指向同一个内存地址
  • 深拷贝就是拷贝数据本身,完全复制一份,新旧对象不共享内存

将slice对象赋给另一个slice,默认是浅拷贝,以下两种方式可以实现slice的深拷贝:

  • copy
  • 遍历元素然后append

slice的扩容发生在append增加元素的时候,有3种机制:

  • 当append之后的cap > 原来的cap的2倍,则新cap直接是append之后的cap
  • 当append之后的cap 不大于 原来的cap的2倍,但是>1024时,将原来的cap按1.25倍扩大,直到新的cap > 原来cap
  • 当append之后的cap 不大于 原来的cap的2倍,且不大于1024时,新的cap为原先cap的2倍

slice是否线程安全的?答案:不是。为什么呢?

因为slice结构未使用锁机制,在多线程并发读写的时候,结果可能是不符合预期的

RocketMQ支持事务消息来提供分布式事务功能,事务消息分为两个部分:

  1. 事务的发送和提交,即两阶段提交
  2. 事务状态回查

事务消息
其中,事务的发送和提交的步骤如下:

  1. Producer发送一个half消息给MQ Sever,half消息是对消费者不可见的,用来检测MQ Sever是否可用
  2. MQ Sever对该half消息返回一个响应表示half消息接收成功
  3. Producer确认Sever可用之后,发起本地事务执行
  4. 本地事务执行成功或者失败之后,Producer提交一个二次确认消息
  5. MQ Sever收到二次确认消息后,如果是这个消息是commit,将half消息状态修改为Consumer可见,如果消息是rollback,则将half消息删除

而在第4步因为某些异常导致执行失败了,则half消息将无法得到确认,所以RocketMQ还提供了一个回查机制:

  1. MQ Sever在half消息等待一定时间未确认后,会对Producer发起回查请求
  2. 此时Producer再去检查本地事务状态,然后重新提交commit或者rollback二次确认

事务消息回查时,事务可能由3种状态:

  1. 提交事务
  2. 回滚事务
  3. 未知事务:未确定状态,需要再次回查

回查是有限制的,默认只能回查不超过15次,超过15次还不能确认消息状态的则默认当作回滚

事务消息的 缺点:

  1. 不支持延时消息和批量消息
  2. 回查机制可能导致消息堆积

redis6.0之前是单线程模型,6.0之后是多线程模型

为什么6.0之前采用单线程模型:

  1. redis本身是一个内存数据库,除了持久化,其他操作基于内存,所以处理速度非常快
  2. 大多数场景下,都不是IO密集型的,而是IO密集型的,对于redis来说,瓶颈主要在于网络IO

同时,单线程也带来以下优点:

  1. 避免了多线程调度的上下文切换的开销
  2. 避免了底层的数据同步的锁的开销

6.0之后的多线程:

为什么要引入多线程?

单线程模型的redis已经可以满足10w左右QPS,但是随着互联网的发展,需要满足更大的QPS

而网络IO的read/write系统调用占用了redis执行期间大部分的CPU时间,所以redis使用多线程充分利用多核CPU来减少socket读写阻塞

和单线程的区别:不再是单线程的事件循环,而是多个IO线程各自维护一个事件循环

整体模型上是由Main线程负责接收连接,在分发给IO线程去解析请求命令,具体命令还是Main线程执行,执行完后再使用IO线程回写响应给客户端。

也就是具体命令执行还是单线程,只是socket读写是多线程,一定程度上提高了性能

另外,Redis4.0引入了异步多线程任务来解决比较耗时命令的执行,比如大key删除,持久化

defer 的实现原理是,将defer函数直接插入到函数末尾,将defer函数在当前函数内展开并直接调用

defer特性:

  1. 延迟特性:defer后的函数不会立即执行,而是延迟到函数结束后执行
  2. 参数预计算:defer后的函数的参数会立即求值,并固定下来,不会等到函数执行的时候再将参数传递到defer中

    比如:

    import "fmt"
    
    func main() {
        a := 1
        defer func(a int) {
            fmt.Println(a)
        }(a +1)
        a = 100
    }

    会在执行到该defer的时候,计算a+1=2,然后把2当作参数固定下来,最后在函数返回前,执行defer函数内的语句,所以输出的是:2

  3. 执行顺序:按先进后出的顺序执行defer函数

    比如:

    import "fmt"
    
    func main() {
        defer func() {
            fmt.Println(111)
        }()
        defer func() {
            fmt.Println(222)
        }()
        defer func() {
            fmt.Println(333)
        }()
        defer func() {
            fmt.Println(444)
        }()
    }

    输出为:

    444
    333
    222
    111
  4. 返回值陷阱:

    return包含了下面几步:将返回值保存在栈上-》执行defer函数-〉函数返回

    比如:

    func deferReturn1() (r int) {
        defer func() {
            g = 200
        }()
        return g
    }
    func deferReturn2() (r int) {
        r = g
        defer func() {
            r = 200
        }()
        return r
    }
    
    func TestDeferReturn(t *testing.T) {
        t.Log(deferReturn1())
        t.Log(deferReturn2())
    }
    
    //输出
    index_test.go:65: 100
    index_test.go:66: 200

    deferReturn1中,先计算了返回值并保存在栈上,此时r=100,再执行defer将g=200,实际返回的r则还是100

    deferReturn2中,先将r=g,计算了返回值保存在栈上,此时r=100,再执行defer将r=200,最终返回的r为200

其他特点:

  1. panic之后的defer不会被执行
  2. panic没有recover时,抛出的panic到当前的grouting最上层函数时,最上层函数直接终止
  3. 当panic被捕获之后,当前gorouting上层函数正常执行
  4. 在panic之前定义的defer,会在panic之前执行

    func TestDefer(t *testing.T) {
        defer func() {
            fmt.Println("c")
        }()
        panic("b")
        fmt.Println("TestDefer")
    }
    
    //输出
    c
    --- FAIL: TestDefer (0.00s)
    panic: b [recovered]
        panic: b

Redis的三种集群模式

  1. 主从模式
  2. 哨兵模式
  3. Cluster模式

主从模式

Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制根据是否全量复制,分为全量复制和增量复制。

全量复制:

在slave启动的初始化阶段,slave需要将master的全量数据都复制一份,全量复制步骤:

  1. slave连接master,发送SYNC命令
  2. master接收到SYNC命令后,开始执行bgsave生成rdb文件,并使用缓冲区接受所有写命令
  3. master执行完bgsave之后,向slave发送快照,并继续将写命令记录在缓冲区
  4. slave接收到快照之后,载入收到的数据
  5. master发送完快照之后,向slave发送缓冲区的写命令
  6. slave载入快照之后,重放写命令

增量复制:

在slave初始化之后,将master上的写命令同步到slave,保证slave和master数据一致

采用主从复制架构时,必须在master开启持久化,否则可能在master当机重启时,因为master的数据为空导致复制之后slave的数据也丢失

主从复制的特点:

  1. 采用异步复制
  2. 一主可以多从
  3. 从服务器可以接受其他从服务器的连接
  4. 主从复制对于master、slave都是非阻塞的

主从复制的优点:

实现了读写分离,提高了可用性,解决了单点故障

缺点是当master节点宕机时,需要手动进行切换主机,进行故障转移,不容易扩容

哨兵模式

哨兵(Sentinel)是Redis中的一个组件,主要有以下功能:

  • 集群监听:监听master、slave是否正常工作
  • 消息通知:如果某个slave发生了故障,哨兵负责通知给管理员
  • 故障转移:一个master挂掉了,会自动转移到slave上
  • 配置中心:当故障发生了之后,通知client新的master地址

哨兵用于实现redis的高可用,本身也是分布式的,作为一个哨兵集群与运行

哨兵的核心知识:

  • 至少需要3个哨兵,来保证哨兵集群的健壮性
  • 哨兵+主从的架构,主要用于保证redis集群的高可用,不保证不会丢失数据

哨兵主要用来监控Redis集群,来保证Redis的高可用,但是还有一个缺点,就是不易扩容

Cluster模式

Redis Cluster是一种服务端芬片技术,采用了槽(slot)的概念,实现了分布式存储

分片原理:

redis将集群划分为16384个槽,存取数据的时候,对key进行hash来决定放在那个槽上,而redis集群的每一个槽都存放了所有槽的路由信息,当定位到槽后,可以直接从配置中获取到所处的节点,再去对应节点获取数据,扩容时只要将槽迁移到新的节点上,再更新槽的路由信息即可

Cluster模式下,每个节点需要开放两个端口,一个用于客户端请求命令,一个用于进行节点之间的通信,用于故障检测、配置更新、故障转移

优点是:

  • 分布式架构便于动态扩容
  • 节点不需要存放所有数据,节省资源
  • 具备哨兵模式的故障监控和转移能力
  • 无proxy,客户端直接访问redis服务

但缺点是运维更复杂,迁移数据需要人工操作,只能使用0号库,不能使用SELECT