在CSDN学Golang分布式中间件(redis)

一,redis整体结构,存储结构

Go语言中的Redis整体结构分为客户端和服务器端两部分,其中服务器端实现了Redis协议的存储引擎。

在服务器端,Redis主要采用哈希表(hash table)作为主要数据结构来存储键值对。哈希表由多个哈希桶(hash bucket)组成,每个哈希桶包含若干个节点(node),每个节点表示一个键值对。

在Redis中,每个节点都有指向下一个节点的指针,因此同一哈希桶内的所有节点可以通过链表连接起来。同时,每个节点还包括三个属性:指向键和值的指针、指向下一个节点的指针以及哈希函数计算出来的哈希值。

当需要查询或修改某个键值对时,Redis会根据该键计算出它在哈希表中所在的位置,并遍历相应哈希桶内的链表找到该节点。如果存在,则返回其对应的值;否则创建新节点,并将其添加到链表上。

除了常规的读写操作外,Redis还支持订阅/发布机制、事务处理、Lua脚本执行等高级功能。这些功能使得Redis不仅适用于缓存场景,在一些需要高性能、高可靠性、可扩展性和灵活度较高的场景也得到广泛应用。golang redis整体结构,存储结构解析不是单一的。

Go语言中的Redis存储结构主要包括以下几种:

  1. 字符串(String):存储最基本的键值对,可以存储字符串、整数或二进制数据。
  2. 哈希表(Hash):将多个键值对组合成一个大对象,适用于存储一些复杂的数据结构,比如用户信息、商品信息等。
  3. 列表(List):按照插入顺序排序的字符串列表,可以进行左右两端的插入和删除操作。
  4. 集合(Set):无序不重复的字符串集合,支持交集、并集、差集等操作。
  5. 有序集合(Sorted Set):与普通集合类似,但每个元素都有一个权重分数,并且按照分数从小到大排序。

在实际应用中,Redis还经常使用各种数据结构来实现一些高级功能。例如:

  1. 计数器(Counter):利用Redis原子性操作特性,在计数时避免竞争条件。
  2. BitMap:通过位运算实现布隆过滤器等功能。
  3. HyperLogLog:通过哈希函数实现去重计数等功能。

二,string命令及内部存储原理

Go语言中的字符串类型是不可变的,即一旦创建就不能被修改。字符串在内存中使用字节数组(byte slice)来存储UTF-8编码字符序列。

字符串类型提供了一些常用的方法,例如:

  • len(s):获取字符串s的长度;
  • s[i]:获取字符串s中索引为i的字符;
  • s + t:将字符串s和t拼接成一个新的字符串;
  • strings.HasPrefix(s, prefix)、strings.HasSuffix(s, suffix)、strings.Contains(s, substr)等方法用于判断字符串是否以某个前缀或后缀开始或结束,或者是否包含某个子串。

在底层实现上,Go语言中的字符串由两个字段组成:指向底层字节数组的指针和表示字符串长度的整数。因为Go语言中所有变量都是值传递,所以在函数参数传递时,如果传递一个较长的字符串,可能会导致性能问题。为了避免这种情况,可以使用切片(slice)作为函数参数。

另外需要注意的是,在Go语言中可以使用反引号(`)括起来的原始字符串(raw string),它不会对转义字符进行处理。例如:

s := `hello\nworld`

这里的s将被赋值为"hello\nworld",而不是"hello\nworld"。

总之,在Go语言中使用字符串非常方便和实用,并采用了高效的UTF-8编码存储方式。同时需要注意字符串不可变性质,以及在函数参数传递时可能会导致性能问题。

三,string对象存储应用

Go语言中的字符串类型是不可变的,即一旦创建了一个字符串变量,就不能再对其进行修改。因此,在处理字符串时需要注意一些存储和应用的问题。

  1. 字符串对象存储在哪里?

在Go语言中,字符串对象是存储在堆上的。当我们声明一个字符串变量时,实际上是创建了一个指向堆上字符串对象的指针。这个指针所占用的内存比较小(通常为8字节),而实际存储字符串内容的内存则会动态分配并在需要时释放。

  1. 字符串拼接会产生多少临时对象?

在使用加号(+)或者fmt.Sprintf()函数拼接多个短小的字符串时,会产生很多临时对象,影响性能。为了避免这个问题,可以使用bytes.Buffer或strings.Builder等类型来进行高效地字符串拼接。例如:

var buffer bytes.Buffer
for _, str := range strs {
    buffer.WriteString(str)
}
result := buffer.String()
  1. 如何判断两个字符串是否相等?

由于Go语言中的字符串类型是不可变的,因此可以使用“==”运算符来判断两个字符串是否相等。需要注意,在比较两个大型的长字符串时可能会影响性能。

  1. 如何处理含有非ASCII字符集合(Unicode)的文本?

在处理含有非ASCII字符集合(Unicode)的文本时,需要注意字符编码的问题。Go语言中使用UTF-8作为默认的字符编码方式,可以通过golang.org/x/text/encoding等包来进行转换和处理。例如:

import (
    "golang.org/x/text/encoding"
    "golang.org/x/text/encoding/japanese"
)

// 将字符串s从ShiftJIS编码转换为UTF-8编码
func ConvertShiftJISToUTF8(s string) (string, error) {
    decoder := japanese.ShiftJIS.NewDecoder()
    result, err := decoder.String(s)
    if err != nil {
        return "", err
    }
    return result, nil
}
  1. 如何避免字符串被修改?

由于字符串类型是不可变的,因此如果我们需要在函数内部修改一个字符串对象,则需要创建一个新的字符串对象,并将修改后的内容复制到新对象中。这个操作可能会产生一定的性能开销,因此应该根据具体情况来选择合适的方法。

在处理字符串时需要注意存储和应用方面的问题。在实际开发中,可以使用一些优秀的库(比如strings、strconv等标准库)来处理各种常见情况下的字符串操作需求,并根据具体情况进行优化和改进。

四,string累加器应用

在Go语言中,由于字符串是不可变类型,所以每次对字符串进行拼接都会创建一个新的字符串对象。这样会导致频繁的内存分配和垃圾回收,从而影响程序性能。为了避免这个问题,可以使用字符串累加器(string builder)来高效地拼接字符串。

Go标准库中提供了bytes.Buffer和strings.Builder两种字符串累加器类型。这两种类型的实现方式很类似,但前者适用于任意字节序列(包括二进制数据),后者则仅适用于Unicode字符序列。

下面是使用strings.Builder来进行字符串拼接的示例代码:

package main

import (
    "fmt"
    "strings"
)

func main() {
    strs := []string{"Hello", ",", "world", "!"}
    
    var builder strings.Builder
    for _, str := range strs {
        builder.WriteString(str)
    }
    
    result := builder.String()
    fmt.Println(result) // 输出:Hello,world!
}

在上述代码中,我们首先声明了一个字符串数组strs,并初始化为一组需要拼接的子串。然后声明一个strings.Builder类型的变量builder,并通过WriteString方法将所有子串按顺序添加到builder中。最后调用builder.String()方法获取最终结果。

需要注意的是,在每次追加子串时,strings.Builder类型都会自动调整内部缓冲区大小,以保证拼接操作的效率和稳定性。因此,使用strings.Builder进行字符串拼接可以避免频繁的内存分配和垃圾回收,从而提高程序性能。

五,string分布式锁

在Go语言中,可以使用分布式锁来解决多个进程或节点同时对同一资源进行修改的问题。常见的分布式锁实现方式包括基于ZooKeeper、Redis等分布式存储系统的实现。

以下是使用Redis实现分布式锁的示例代码:

package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "time"
)

var redisClient *redis.Client

func init() {
    // 初始化 Redis 客户端
    redisClient = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })
}

// 获取分布式锁
func acquireLock(lockKey string, timeout time.Duration) (bool, error) {
    for {
        // 在 Redis 中设置一个带有过期时间的键值对作为锁
        success, err := redisClient.SetNX(lockKey, time.Now().Unix(), timeout).Result()
        
        if err != nil {
            return false, err
        }
        
        if success {
            return true, nil
        }
        
        time.Sleep(100 * time.Millisecond)
    }
}

// 释放分布式锁
func releaseLock(lockKey string) error {
    _, err := redisClient.Del(lockKey).Result()
    
    return err
}

func main() {
    lockKey := "my_lock_key"
    
    // 尝试获取分布式锁,超时时间为10秒钟
    locked, err := acquireLock(lockKey, 10*time.Second)
    
    if err != nil {
        fmt.Println("Failed to acquire lock:", err)
        return
    }
    
    if !locked {
        fmt.Println("Failed to acquire lock")
        return
    }
    
    defer releaseLock(lockKey)
    
    // 在这里执行需要加锁的代码
    
    fmt.Println("Got the lock!")
}

在上述代码中,我们使用go-redis库来连接Redis数据库,并实现了acquireLock和releaseLock两个函数来获取和释放分布式锁。其中,acquireLock函数会在Redis中设置一个带有过期时间的键值对作为锁,并通过循环判断当前是否能够获取到该锁;而releaseLock函数则是通过删除该键值对来释放锁。

需要注意的是,在使用分布式锁时,应当避免死锁和多次解除同一把锁的情况。此外,在设置过期时间时应根据业务需要合理设置,以兼顾程序性能和数据一致性。

六,string位运算应用

在Go语言中,可以使用位运算对字符串进行一些特定的操作。下面介绍两个常见的应用场景:

  1. 判断字符串是否包含某个字符

我们可以将每个字符转换为ASCII码,并使用位运算来判断是否存在某个字符。

func hasChar(s string, c byte) bool {
    for i := 0; i < len(s); i++ {
        if s[i] == c {
            return true
        }
    }
    return false
}

// 使用位运算判断字符串s中是否包含字符c
func hasCharBitwise(s string, c byte) bool {
    var bitmap uint32 = 0
    
    for i := 0; i < len(s); i++ {
        // 将字符对应的bit位置为1
        bitmap |= (1 << (s[i] - 'a'))
    }
    
    // 判断字符c对应的bit位置是否为1
    return ((bitmap >> (c - 'a')) & 1) == 1
}

在上述代码中,我们定义了hasChar函数和hasCharBitwise函数来分别实现判断字符串是否包含某个字符的功能。其中,hasChar函数通过遍历字符串每一个字符并与目标字符进行比较来判断;而hasCharBitwise函数则是使用位运算方式,先将每个字符对应的bit位置为1,再根据目标字符得到相应的bit位置并判断其值是否为1。

  1. 字符串去重

我们可以利用哈希表(map)或者布隆过滤器(Bloom Filter)来对字符串进行去重操作。下面是利用布隆过滤器实现字符串去重的示例代码:

type BloomFilter struct {
    bits   []uint64 // 存储bit位
    hashFn [4]func([]byte) uint32 // 哈希函数
}

// 初始化布隆过滤器
func NewBloomFilter(n int) *BloomFilter {
    return &BloomFilter{
        bits: make([]uint64, (n+63)/64),
        hashFn: [4]func([]byte) uint32{
            func(data []byte) uint32 { return murmur3.Sum32(data, 0) },
            func(data []byte) uint32 { return murmur3.Sum32(data, 1) },
            func(data []byte) uint32 { return murmur3.Sum32(data, 2) },
            func(data []byte) uint32 { return murmur3.Sum32(data, 3) },
        },
    }
}

// 添加一个字符串到布隆过滤器中
func (bf *BloomFilter) Add(s string) {
    data := []byte(s)
    
    for i := range bf.hashFn {
        bitIndex := bf.hashFn[i](data) % uint32(len(bf.bits)*64)
        
        bf.bits[bitIndex/64] |= (1 << (bitIndex % 64))
    }
}

// 判断一个字符串是否在布隆过滤器中已存在
func (bf *BloomFilter) Contains(s string) bool {
    data := []byte(s)
    
    for i := range bf.hashFn {
        bitIndex := bf.hashFn[i](data) % uint32(len(bf.bits)*64)
        
        if (bf.bits[bitIndex/64] & (1 << (bitIndex % 64))) == 0 {
            return false
        }
    }
    
    return true
}

func main() {
    bf := NewBloomFilter(1000000) // 布隆过滤器中的元素数量为100万
    
    s := []string{"hello", "world", "go", "language", "go"}
    
    for _, str := range s {
        if !bf.Contains(str) { // 如果该字符串不在布隆过滤器中,则添加到其中
            bf.Add(str)
        }
    }
    
    fmt.Println("Unique strings:")
    
    for _, str := range s {
        if bf.Contains(str) { // 判断字符串是否在布隆过滤器中已存在
            fmt.Println(str)
        }
    }
}

在上述代码中,我们使用了murmur3哈希算法作为布隆过滤器的哈希函数,实现了Add和Contains两个方法来添加和判断字符串是否已存在。需要注意的是,在创建布隆过滤器时需要根据预期元素数量进行合理设置,以兼顾空间和误判率的问题。

七,list命令及内部存储原理

在Go语言中,list是双向链表(doubly linked list)的实现。它可以用来实现各种数据结构,如队列、栈等。list提供了以下常用的命令:

  1. New():创建一个空的双向链表。
  2. PushFront(v interface{}):在链表头部插入元素v。
  3. PushBack(v interface{}):在链表尾部插入元素v。
  4. InsertBefore(v interface{}, mark *Element):在mark前面插入元素v。
  5. InsertAfter(v interface{}, mark *Element):在mark后面插入元素v。
  6. Remove(e *Element):从链表中移除元素e。
  7. Len() int:返回链表长度。

下面是一个示例代码,演示了如何使用list来实现队列和栈:

package main

import (
    "container/list"
    "fmt"
)

func main() {
    q := list.New()
    
    // 队列操作
    q.PushBack(1)
    q.PushBack(2)
    q.PushBack(3)
    
    for q.Len() > 0 {
        e := q.Front()
        fmt.Println(e.Value.(int)) // 输出队首元素
        
        q.Remove(e) // 将队首元素出队
    }
    
    s := list.New()
    
    // 栈操作
    s.PushFront(1)
    s.PushFront(2)
    s.PushFront(3)
    
    for s.Len() > 0 {
        e := s.Front()
        fmt.Println(e.Value.(int)) // 输出栈顶元素
        
        s.Remove(e) // 将栈顶元素出栈
    }
}

在上述代码中,我们使用了list来实现队列和栈。通过PushBack、PushFront、InsertBefore、InsertAfter等方法,我们可以很方便地在链表头部或尾部插入元素,也可以在指定的位置插入元素。同时,Remove方法可以将链表中的任意一个元素移除。

下面讲解一下list的内部存储原理。

在Go语言中,双向链表是由Element类型表示的节点构成的。每个节点包含三个字段:Value(该节点对应的值)、next(指向下一个节点)和prev(指向前一个节点)。List类型则包含两个字段:root(指向链表头部和尾部的哨兵节点)和len(链表长度)。

当创建一个新的空链表时,会生成两个哨兵节点,并使它们相互连接:

// New returns an initialized list.
func New() *List {
    return new(List).Init()
}

func (l *List) Init() *List {
    l.root.next = &l.root
    l.root.prev = &l.root
    l.len = 0
    
    return l
}

其中,root.next和root.prev都指向自身,这样就形成了一个空链表。当调用PushFront或PushBack方法时,会创建一个新的Element,并将其插入到相应位置:

// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List) PushFront(v interface{}) *Element {
    return l.insertValue(v, &l.root)
}

// insertValue is a convenience wrapper that calls insert(e, value, at).
func (l *List) insertValue(v interface{}, at *Element) *Element {
    return l.insert(&Element{Value: v}, at)
}

func (l *List) insert(e, at *Element) *Element {
    n := at.next
    at.next = e
    e.prev = at
    e.next = n
    n.prev = e
    
    l.len++
    
    return e
}

在这个过程中,需要注意的是链表长度len会在每次插入或移除元素时进行更新。例如,在PushFront方法中,我们创建一个新的Element,并将其插入到root和root.next之间。同时,需要将原本指向root.next的节点prev指针重新指向新的节点e。

list是Go语言提供的一种双向链表实现。它可以用来实现各种数据结构,如队列、栈等。在内部存储上,list由一些节点(包含值、前后指针)组成,并通过哨兵节点相互连接形成一个双向链表。

八,list栈应用

在Go语言中,list可以用来实现栈。通过PushFront方法将元素插入到链表头部,再通过Remove方法将元素从链表中移除,就可以实现栈的操作。

下面是一个使用list实现栈的示例代码:

package main

import (
    "container/list"
    "fmt"
)

func main() {
    s := list.New()
    
    // 将元素1、2、3依次压入栈
    s.PushFront(1)
    s.PushFront(2)
    s.PushFront(3)
    
    // 输出栈顶元素,并将其出栈
    e := s.Front()
    fmt.Println(e.Value.(int)) // 输出3
    
    s.Remove(e) // 将3出栈
    
    // 再次输出栈顶元素,并将其出栈
    e = s.Front()
    fmt.Println(e.Value.(int)) // 输出2
    
    s.Remove(e) // 将2出栈
    
    // 最后输出剩余的栈顶元素
    e = s.Front()
    
  	fmt.Println(e.Value.(int)) // 输出1
}

在上述代码中,我们创建了一个空链表s作为栈。然后通过PushFront方法依次将元素1、2、3压入栈中。接着,我们使用Front方法获取当前的栈顶元素(也就是最后一个插入的元素),并通过Remove方法将其从链表中移除。这样就完成了对该元素的出栈操作。

需要注意的是,在使用list实现栈时,需要在插入元素和移除元素时分别使用PushFront和Remove方法。同时,由于list中存储的是interface{}类型的值,因此需要在取出元素时进行类型断言。

九,list队列,异步队列,阻塞队列

在Go语言中,list也可以用来实现队列。通过PushBack方法将元素插入到链表尾部,再通过Front和Remove方法分别获取队首元素并将其从链表中移除,就可以实现队列的操作。

下面是一个使用list实现队列的示例代码:

package main

import (
    "container/list"
    "fmt"
)

func main() {
    q := list.New()
    
    // 将元素1、2、3依次加入队列
    q.PushBack(1)
    q.PushBack(2)
    q.PushBack(3)
    
    // 输出队首元素,并将其出队
    e := q.Front()
    fmt.Println(e.Value.(int)) // 输出1
    
    q.Remove(e) // 将1出队
    
    // 再次输出队首元素,并将其出队
    e = q.Front()
    fmt.Println(e.Value.(int)) // 输出2
    
  	q.Remove(e) // 将2出队
    
  	// 最后输出剩余的队首元素
  	e = q.Front()
  	fmt.Println(e.Value.(int)) // 输出3
}

异步队列指的是,在向已满的队列添加新元素时不会阻塞,而是立即返回。在Go语言中,可以通过使用带有缓冲通道来实现异步队列。

下面是一个使用带有缓冲通道实现异步队列的示例代码:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 3)

	// 将元素1、2、3依次加入队列
	ch <- 1
	ch <- 2
	ch <- 3

	// 异步向已满的队列添加新元素,不会阻塞
	go func() {
		ch <- 4
	}()

	// 输出队列中的所有元素
	for i := 0; i < cap(ch); i++ {
		fmt.Println(<-ch)
	}
}

在上述代码中,我们创建了一个容量为3的带缓冲通道ch。通过向该通道发送三个整数,我们将它们加入到了队列中。接着,我们启动了一个协程,在不等待的情况下向这个已满的队列中添加一个新元素4。最后,我们使用for循环遍历并输出队列中的所有元素。

阻塞队列指的是,在向已满或为空的队列添加或移除元素时,操作会被阻塞直到有足够的空间或者有可用元素。在Go语言中,可以通过使用无缓冲通道来实现阻塞队列。

下面是一个使用无缓冲通道实现阻塞队列的示例代码:

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int)

    // 向空队列中添加元素会被阻塞,直到有其他协程从该通道取走一个值为止
    go func() {
        ch <- 1
    }()

    // 从空队列中移除元素也会被阻塞,直到有其他协程向该通道添加一个值为止
    fmt.Println(<-ch)
}

在上述代码中,我们创建了一个无缓冲通道ch。在启动的协程中,我们试图向这个空队列中添加一个整数1。由于这个队列为空,操作会被阻塞直到main函数的另外一部分从该通道取走一个值。最后,我们输出了取出的值1。

十,list固定窗口记录

在Go语言中,可以使用list来实现一个固定窗口记录的功能。这种数据结构可以用于需要限制某个时间段内操作数量的场景。

下面是一个使用list实现固定窗口记录的示例代码:

package main

import (
	"container/list"
	"fmt"
	"time"
)

func main() {
	var winSize int = 5 // 窗口大小为5秒
	var maxCount int = 3 // 在每个窗口内最多允许执行3次操作

	l := list.New()
	counts := make([]int, winSize) // 存储每个窗口内的操作计数

	for i := 0; i < 10; i++ { // 模拟执行10次操作
		now := time.Now().Unix()

		if l.Len() >= winSize { // 如果队列长度超过了窗口大小,则移除最旧的元素
			e := l.Front()
			timestamp := e.Value.(int64)
			index := timestamp % int64(winSize)
			counts[index]--
			l.Remove(e)
		}

		l.PushBack(now) // 将当前时间戳加入队列尾部
		index := now % int64(winSize)
		counts[index]++

		if counts[index] > maxCount { // 如果当前窗口内的操作次数超过了阈值,则拒绝该操作
			fmt.Println("Operation rejected: too frequent")
		} else {
			fmt.Println("Operation accepted")
		}

        time.Sleep(time.Second) // 模拟执行一次操作需要1秒钟
	}
}

在上述代码中,我们定义了一个窗口大小为5秒,每个窗口内最多允许执行3次操作。我们使用list来存储每个窗口内的时间戳,并使用一个长度为5的数组counts来记录每个窗口内的操作计数。在模拟执行10次操作的循环中,我们首先检查队列是否已经满了,如果是,则移除最旧的元素。接着将当前时间戳加入队列尾部,并根据当前时间戳计算出所属窗口的索引。然后将该索引对应位置上的计数器加1,并判断该计数器是否超过阈值。如果超过了阈值,则拒绝该操作;否则接受该操作并输出信息。

需要注意的是,在实际应用中,固定窗口记录可能需要考虑更复杂的场景和策略,如滑动窗口、指数衰减等等。以上代码仅提供了一种简单实现方式作为参考。

十一,hash命令及内部存储原理

在Go语言中,hash命令主要有两个:map和struct。其中,map是一种哈希表实现的数据结构,用于存储键值对;而struct则可以通过tag指定字段的名称、类型等信息,并将其转换为一个类似哈希表的结构体。

下面我们主要介绍一下map这个哈希表实现的数据结构。

Map内部存储原理

在Go语言中,map是一种基于哈希表实现的数据结构。它使用了散列函数来把key映射到相应的bucket中。每个bucket又包含了若干个链表节点,用于存储具有相同hash值(即落入同一个bucket)的key-value对。

具体来说,一个map由以下几个组成部分:

  • 桶(bucket)数组:用于存放链表头指针。
  • 链表节点:用于存放真正的key-value对以及指向下一个节点的指针。
  • 元素数量:记录当前map中已经存储了多少个key-value对。
  • 负载因子(load factor):表示桶数组被占用的比例。当负载因子超过某个阈值时,会自动触发扩容操作。

当我们向map中插入一个新元素时,Go语言会先计算出该元素对应的hash值,并根据hash值找到对应的桶。然后遍历该桶中的链表,如果找到了key相同的节点,则更新其value;否则创建一个新的链表节点,并插入到链表头部。

当元素数量超过一定阈值时,map会自动进行扩容操作。扩容的具体流程如下:

  • 分配一个新的桶数组。
  • 遍历旧桶数组中的每个桶,将其中所有元素重新计算hash并插入到新桶数组中对应的位置上。
  • 释放旧桶数组及其所有节点。

需要注意的是,在扩容期间,由于存在同时访问旧、新两个桶数组的情况,因此可能会导致读写冲突或者死锁等问题。为了避免这种情况发生,Go语言在扩容期间会使用growing状态来标识当前map正在进行扩容操作,并通过读写分离等机制来确保数据一致性。在Go语言中,map是一种高效、易用、安全的哈希表实现数据结构,在很多场景下都可以作为首选数据结构使用。

十二,hash对象存储应用

在Go语言中,我们可以使用map和struct这两种哈希对象来存储数据。

1. 使用map存储键值对

map是一种基于哈希表实现的键值对集合。我们可以使用make函数创建一个空的map,也可以直接使用字面量创建一个非空的map。例如:

// 创建一个空的map
m := make(map[string]int)

// 创建一个非空的map
m := map[string]int{"apple": 2, "orange": 3}

向map中添加元素可以通过下标索引操作来完成。例如:

// 向m中添加元素
m["banana"] = 5

查找map中是否存在某个key可以使用_, ok := m[key]语法糖来判断。例如:

if _, ok := m["banana"]; ok {
    fmt.Println("m[\"banana\"] exists")
} else {
    fmt.Println("m[\"banana\"] does not exist")
}

遍历map时,我们需要注意到它是无序的,并且遍历顺序可能会发生变化。因此,在需要有序遍历时,我们需要手动将key按照指定顺序排序后再进行遍历。

2. 使用struct存储结构体对象

在Go语言中,struct也可以看做是一种哈希对象,因为它具有类似哈希表的结构:每个字段都有名称和类型信息,并且可以通过名字索引访问到相应的值。

我们可以通过定义一个struct类型来创建一个结构体对象。例如:

type Person struct {
    Name string
    Age  int
}

// 创建一个Person对象
p := Person{Name: "Tom", Age: 30}

我们可以使用点号操作符来访问结构体的字段值。例如:

fmt.Println(p.Name) // 输出:Tom

在需要对结构体进行序列化和反序列化时,我们可以使用encoding/json包提供的Marshal和Unmarshal函数。例如:

// 将p序列化为JSON字符串
b, _ := json.Marshal(p)
fmt.Println(string(b)) // 输出:{"Name":"Tom","Age":30}

// 将JSON字符串反序列化为Person对象
var p2 Person
json.Unmarshal(b, &p2)
fmt.Println(p2) // 输出:{Tom 30}

在Go语言中,map和struct都是非常实用的哈希对象存储数据的方式,它们分别适用于不同的场景和需求。当我们需要存储键值对时,应该优先考虑使用map;当我们需要表示一组具有相同属性和行为的对象时,应该优先考虑使用struct。

十三,set命令及内部存储原理

在Go语言中,没有内置的Set类型。但是,我们可以使用map或slice等数据结构来实现Set。

使用map实现Set

使用map实现Set的基本思路是,将元素作为key存储在map中,并给这些key一个固定的value值(例如true),表示它们存在于Set中。

下面是一个使用map实现Set的示例:

type Set map[string]bool

func (s Set) Add(str string) {
    s[str] = true
}

func (s Set) Remove(str string) {
    delete(s, str)
}

func (s Set) Contains(str string) bool {
    _, ok := s[str]
    return ok
}

func main() {
    s := make(Set)
    s.Add("apple")
    s.Add("banana")
    s.Add("orange")

    fmt.Println(s.Contains("apple")) // 输出:true

    s.Remove("banana")

    fmt.Println(s.Contains("banana")) // 输出:false
}

使用slice实现Set

使用slice实现Set的基本思路是,将元素按照指定顺序存储在slice中,并通过遍历slice来进行查找、添加和删除操作。

下面是一个使用slice实现Set的示例:

type Set []string

func (s *Set) Add(str string) {
    for _, v := range *s {
        if v == str {
            return
        }
    }
    *s = append(*s, str)
}

func (s *Set) Remove(str string) bool {
    for i, v := range *s {
        if v == str {
            (*s)[i] = (*s)[len(*s)-1]
            *s = (*s)[:len(*s)-1]
            return true
        }
    }
    return false
}

func (s Set) Contains(str string) bool {
    for _, v := range s {
        if v == str {
            return true
        }
    }
    return false
}

func main() {
    var s Set
    s.Add("apple")
    s.Add("banana")
    s.Add("orange")

    fmt.Println(s.Contains("apple")) // 输出:true

    s.Remove("banana")

    fmt.Println(s.Contains("banana")) // 输出:false
}

内部存储原理

无论是使用map还是slice实现Set,其内部存储结构都类似于哈希表。具体来说,使用map实现Set时,元素作为key存储在map中;使用slice实现Set时,元素按照指定顺序存储在slice中。在查询、添加和删除操作时,我们都需要通过遍历或者哈希算法来查找相应的元素位置。

在Go语言中,虽然没有内置的Set类型,但是我们可以使用map或slice等数据结构来自己实现Set,并且这些数据结构的内部存储原理类似于哈希表。

十四,set唯一无序应用

Set是一种集合类型,它可以存储一组唯一、无序的元素。在Go语言中,虽然没有内置的Set类型,但是我们可以使用map或slice等数据结构来实现Set。

Set最常见的应用场景是去重。例如,在从数据库中读取数据时,可能会出现重复记录的情况,而我们需要对这些记录进行去重操作。此时,使用Set就非常方便了。

下面是一个使用Set进行去重操作的示例:

func RemoveDuplicates(nums []int) []int {
    set := make(map[int]bool)
    result := []int{}

    for _, num := range nums {
        if !set[num] {
            set[num] = true
            result = append(result, num)
        }
    }

    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 1, 2, 3}
    result := RemoveDuplicates(nums)

    fmt.Println(result) // 输出:[1 2 3 4 5]
}

上述代码中,我们定义了一个名为RemoveDuplicates的函数,该函数接收一个整型数组nums作为参数,并返回一个不含重复元素的新数组result。在函数体内部,我们使用map来实现Set,并遍历nums数组中的每个元素。如果该元素不存在于Set中,则将其添加到result数组末尾,并将其加入Set中;否则忽略该元素。最后返回result数组即可。

除了去重之外,还有很多其他应用场景也可以使用Set来实现,例如:

  • 统计某个元素在一个数组或列表中出现的次数
  • 判断两个数组或列表是否有相同的元素
  • 找出两个数组或列表之间的交集、并集和差集等操作

Set是一种非常有用的数据结构,在实际开发中可以帮助我们简化代码、提高效率。

十五,set关系应用

Set关系应用指的是在两个或多个集合之间进行操作,例如求并集、交集、差集等。在Go语言中,我们可以使用map或slice等数据结构来实现Set,并利用这些数据结构来实现集合之间的关系运算。

下面是一些常见的Set关系应用:

  1. 求并集

求两个集合A和B的并集,可以将A和B中所有元素放入一个新的Set中即可。

func Union(setA, setB map[int]bool) map[int]bool {
    union := make(map[int]bool)
    
    for k := range setA {
        union[k] = true
    }
    
    for k := range setB {
        union[k] = true
    }
    
    return union
}
  1. 求交集

求两个集合A和B的交集,需要遍历其中一个Set,然后判断该元素是否同时存在于另一个Set中。

func Intersection(setA, setB map[int]bool) map[int]bool {
    intersection := make(map[int]bool)

    for k := range setA {
        if _, ok := setB[k]; ok {
            intersection[k] = true
        }
    }

    return intersection
}
  1. 求差集

求两个集合A和B的差集,需要遍历其中一个Set,并检查该元素是否不存在于另一个Set中。

func Difference(setA, setB map[int]bool) map[int]bool {
    difference := make(map[int]bool)

    for k := range setA {
        if _, ok := setB[k]; !ok {
            difference[k] = true
        }
    }

    return difference
}
  1. 判断子集

判断一个集合A是否为另一个集合B的子集,需要遍历A中的所有元素,并检查它们是否都存在于B中。

func IsSubset(setA, setB map[int]bool) bool {
    for k := range setA {
        if _, ok := setB[k]; !ok {
            return false
        }
    }

    return true
}

上述代码中,我们定义了四个函数来实现Set关系运算。这些函数接收两个map类型的参数,分别表示要操作的两个集合。每个函数返回一个新的map,其中包含所求的结果。例如,在执行Union(setA, setB)时,会将setA和setB中所有元素放入一个新的map(即并集)并返回该map。

在Go语言中使用Set来进行关系运算非常方便和高效。只需要通过简单的代码就可以实现各种常见Set操作,提高开发效率。

十六,zset命令及内部存储原理

在Go语言中,ZSet指的是有序集合,它是一种特殊的Set,其中每个元素都带有一个分数。根据分数大小,ZSet可以进行范围查找、排名和排序等操作。

Redis作为一个流行的内存数据库,也支持ZSet数据类型,并提供了一些常用的命令来对ZSet进行操作。

以下是一些常见的ZSet命令:

  1. ZADD

向ZSet中添加元素,并指定其分数。

ZADD key score member [score member ...]

例如:

> ZADD myset 1 "one"
(integer) 1
> ZADD myset 2 "two"
(integer) 1
> ZADD myset 3 "three" 4 "four" 5 "five"
(integer) 3
  1. ZRANGE

按照排名(从小到大)获取指定范围内的成员。

ZRANGE key start stop [WITHSCORES]

例如:

> ZRANGE myset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
7) "four"
8) "4"
9) "five"
10) "5"
  1. ZREVRANGE

按照排名(从大到小)获取指定范围内的成员。

ZREVRANGE key start stop [WITHSCORES]

例如:

> ZREVRANGE myset 0 -1 WITHSCORES
1) "five"
2) "5"
3) "four"
4) "4"
5) "three"
6) "3"
7) "two"
8) "2"
9) "one"
10) "1"
  1. ZINCRBY

将指定成员的分数增加increment。

ZINCRBY key increment member

例如:

> ZINCRBY myset 2 two
"4"
> ZRANGE myset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "three"
4) "3"
5) "four"
6) "4"
7) "two"
8) "4" 
9)"five”
10)“5”

除了以上命令外,还有许多其他常见的ZSet命令,例如ZREM、ZRANK、ZREVRANK等。

在Redis中,ZSet是通过跳跃表(Skip List)来实现内部存储的。跳跃表是一种随机化数据结构,它以对数时间复杂度(O(log n))来维护一个元素集合。Redis中的跳跃表由多层链表组成,每一层链表都是原链表的一个子集,并按照某个概率进行裁剪和连接。这样可以使得查询效率非常高,并且插入和删除操作也比较容易实现。

在Go语言中使用ZSet可以方便地实现各种有序集合操作,而Redis中的跳跃表则提供了高效的内部存储机制。如果需要使用有序集合,可以考虑使用ZSet和Redis来实现。

十七,zset排行榜

在Golang中,可以通过使用Redis的ZSet数据类型来实现排行榜功能。ZSet是一个有序集合,其中每个元素都带有一个分数,在查询时按照分数从小到大排序。这正好符合排行榜的需求。

下面是一个简单的实现步骤:

  1. 使用Redis的ZADD命令将成员添加到ZSet中,并指定其分数为对应的得分。
  2. 使用Redis的ZRANGE命令按照排名(从小到大)获取前N名成员和对应的得分。
  3. 将获取到的成员和得分格式化输出即可。

以下是一段示例代码:

package main

import (
	"fmt"
	"github.com/go-redis/redis"
)

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	defer client.Close()

	// 添加成员及对应得分
	client.ZAdd("rank", &redis.Z{Score: 90, Member: "Tom"},
		&redis.Z{Score: 80, Member: "Jerry"},
		&redis.Z{Score: 70, Member: "Jack"},
	)

	rankList, err := client.ZRevRangeWithScores("rank", 0, -1).Result()
	if err != nil {
		panic(err)
	}

	fmt.Printf("%-10s %-10s\n", "Name", "Score")
	for i, rank := range rankList {
	    // 只输出前三名
	    if i >= 3 {
	        break
	    }
		fmt.Printf("%-10s %-10v\n", rank.Member, rank.Score)
	}
}

输出结果:

Name       Score     
Tom        90        
Jerry      80        
Jack       70

上述代码中,我们使用了go-redis库来连接Redis,并通过ZAdd命令将成员添加到ZSet中。然后,使用ZRANGE命令按照排名从大到小获取所有成员和得分,并遍历前三名成员输出其名称和得分。

这只是一个简单的示例,实际应用中可能需要更复杂的业务逻辑和查询条件。但基本思路是相同的,即通过Redis的ZSet数据类型来维护排行榜数据,并在需要时查询、过滤和排序。

十八,zset延迟队列

在Golang中,可以使用Redis的ZSet数据类型来实现延迟队列。ZSet是一个有序集合,每个元素都带有一个分数,在查询时按照分数从小到大排序。因此,我们可以将消息的触发时间作为分数存储在ZSet中,当消息到达触发时间时,再从ZSet中删除该消息并执行相应操作。

以下是一段示例代码:

package main

import (
	"fmt"
	"time"

	"github.com/go-redis/redis"
)

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	defer client.Close()

	msg := "hello world"
	delayTime := 10 // 延迟10秒执行

	err := client.ZAdd("delay_queue", &redis.Z{Score: float64(time.Now().Unix() + int64(delayTime)), Member: msg}).Err()
	if err != nil {
	    panic(err)
    }

	for {
        currentTime := time.Now().Unix()
        msgs, err := client.ZRangeByScore("delay_queue", &redis.ZRangeBy{
            Min:    "0",
            Max:    fmt.Sprintf("%d", currentTime),
            Offset: 0,
            Count:  1,
        }).Result()
        
        if len(msgs) == 0 || err != nil {
            continue
        }
        
        for _, m := range msgs {
            fmt.Println("execute message:", m)
            
            // TODO 执行对应操作
            
            // 删除已经处理过的消息
            _, _ = client.ZRem("delay_queue", m).Result()
        }
        
        time.Sleep(time.Second)
    }
}

在上述代码中,我们使用了go-redis库连接Redis,并通过ZAdd命令将消息添加到ZSet中。其中,分数为当前时间加上延迟时间。然后,通过ZRangeByScore命令获取所有分数小于等于当前时间的消息,并遍历执行相应操作。最后,删除已经处理过的消息。

需要注意的是,在实际生产环境中,可能需要考虑多个进程同时从队列中取出消息的情况。为了避免竞争条件和重复执行问题,可以使用Redis的WATCH、MULTI和EXEC命令来保证原子性操作。

十九,zset时间窗口限流

在Golang中,可以使用Redis的ZSet数据类型实现时间窗口限流。具体实现思路是:将每个请求的时间戳作为分数存储在ZSet中,并设置一个过期时间来控制窗口大小。当有新请求到达时,先将当前时间戳作为分数加入ZSet中,然后使用ZREMRANGEBYSCORE命令删除所有分数小于当前时间减去窗口大小的元素,最后通过ZCARD命令获取集合大小与限流阈值进行比较即可。

以下是一段示例代码:

package main

import (
	"fmt"
	"time"

	"github.com/go-redis/redis"
)

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	defer client.Close()

	limit := 10          // 时间窗口内允许通过的请求数
	windowSize := 60     // 窗口大小,单位秒
	key := "rate_limit"  // Redis键名

	for {
        currentTime := time.Now().Unix()
        
        err := client.ZAdd(key, &redis.Z{Score: float64(currentTime), Member: currentTime}).Err()
        if err != nil {
            panic(err)
        }
        
        _, _ = client.Expire(key, time.Duration(windowSize)*time.Second).Result()
        
        size, err := client.ZCard(key).Result()
        if err != nil {
            panic(err)
        }
        
        if size > int64(limit) {
            fmt.Println("exceed rate limit")
            
            // TODO 返回错误信息
            
            break
        }
        
        time.Sleep(time.Millisecond * 100)
    }
}

在上述代码中,我们使用go-redis库连接Redis,并通过ZAdd命令将请求的时间戳作为分数存储在ZSet中。然后,使用Expire命令设置过期时间为窗口大小,并通过ZCard命令获取集合大小与限流阈值进行比较。需要注意的是,在实际生产环境中,可能需要考虑多个进程同时访问Redis的情况。为了避免竞争条件和重复执行问题,可以使用Redis的WATCH、MULTI和EXEC命令来保证原子性操作。

二十,redigo操作

redigo是Go语言的Redis客户端,可以用来操作Redis数据库。以下是一些常见的redigo操作示例:

  1. 连接Redis数据库
import "github.com/gomodule/redigo/redis"

func main() {
    // 创建连接池
    pool := &redis.Pool{
        MaxIdle:     10,
        MaxActive:   1000,
        IdleTimeout: 240 * time.Second,
        Dial: func() (redis.Conn, error) {
            return redis.Dial("tcp", "localhost:6379")
        },
    }
    defer pool.Close()

    // 获取连接
    conn := pool.Get()
    defer conn.Close()

    // 执行命令
    _, err := conn.Do("SET", "key", "value")
}
  1. 设置和获取键值对
// 设置键值对
_, err = conn.Do("SET", "key", "value")

// 获取键值对
value, err := redis.String(conn.Do("GET", "key"))
if err != nil {
    fmt.Println("error getting value from redis:", err)
} else {
    fmt.Println(value)
}
  1. 列表操作
// 将元素添加到列表尾部
_, err = conn.Do("RPUSH", "list_key", "item")

// 获取列表长度
length, err := redis.Int(conn.Do("LLEN", "list_key"))

// 获取指定范围内的元素
items, err := redis.Strings(conn.Do("LRANGE", "list_key", 0, -1))
  1. 集合操作
// 添加元素到集合中
_, err = conn.Do("SADD", "set_key", "value1", "value2")

// 获取集合元素个数
count, err := redis.Int(conn.Do("SCARD", "set_key"))

// 判断元素是否在集合中
exist, err := redis.Bool(conn.Do("SISMEMBER", "set_key", "value1"))
  1. 哈希表操作
// 设置哈希表字段值
_, err = conn.Do("HSET", "hash_key", "field1", "value1")

// 获取哈希表字段值
value, err := redis.String(conn.Do("HGET", "hash_key", "field1"))

// 获取所有哈希表字段和值
items, err := redis.Strings(conn.Do("HGETALL", "hash_key"))
  1. 事务操作
conn.Send("MULTI")
conn.Send("SET", key1, value1)
conn.Send("SET", key2, value2)
reply, err := conn.Do("EXEC")
if reply == nil || err != nil {
    fmt.Println(err)
} else {
    fmt.Println(reply)
}
  1. 发布/订阅操作
pubsubConn := redis.PubSubConn{Conn: conn}
pubsubConn.Subscribe(channel)

for {
    switch v := pubsubConn.Receive().(type) {
    case redis.Message:
        fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
    case redis.Subscription:
        fmt.Printf("%s: %d\n", v.Channel, v.Count)
    case error:
        return v
    }
}

以上是一些常见的redigo操作示例。

二十一,redigo管道和事务

在Redis中,管道和事务都是用来批量处理命令的方法。redigo提供了方便的API来支持这些操作。

  1. 管道

使用管道可以将多个Redis命令一次性发送到服务器执行,并将所有结果一次性接收返回。这比分别发送每个命令并等待每个响应更快。下面是一个简单的示例:

// 获取连接
conn := pool.Get()
defer conn.Close()

// 创建管道
pipeline := redis.NewPipeline(conn)

// 发送多个命令
pipeline.Send("SET", "key1", "value1")
pipeline.Send("SET", "key2", "value2")

// 执行管道,并获取所有结果
results, err := pipeline.Exec()
if err != nil {
    // 处理错误
} else {
    // 处理结果
}

上面的代码首先获取一个连接,然后创建一个管道对象,向它添加两个命令(在本例中是设置键值对),最后执行管道并接收所有结果。

  1. 事务

Redis事务使得可以原子地执行多个操作。这意味着,要么所有操作都被成功地执行(提交),要么不会有任何操作被执行(回滚)。下面是一个简单的示例:

// 获取连接
conn := pool.Get()
defer conn.Close()

// 开始事务
tx, err := conn.Do("MULTI")
if err != nil {
    // 处理错误
}

// 添加多个操作到事务中
tx.Send("SET", "key1", "value1")
tx.Send("SET", "key2", "value2")

// 提交事务
_, err = tx.Do("EXEC")
if err != nil {
    // 处理错误
} else {
    // 处理结果
}

上面的代码首先获取一个连接,然后开始一个事务,向它添加两个操作(在本例中是设置键值对),最后提交事务并处理结果。如果有任何操作失败,整个事务将回滚。

需要注意的是,通过redigo执行的所有命令都必须在同一管道或事务中执行。因此,在使用管道和事务时,请确保不会同时使用其他命令。

二十二,redigo订阅分布,stream

Redigo是Redis的一个Golang客户端,支持订阅/发布和Stream数据类型。

  1. 订阅/发布

在Redis中,可以使用SUBSCRIBE命令订阅通道,并使用PUBLISH命令向通道发送消息。redigo为这些操作提供了方便的API。

首先,需要创建一个连接到Redis服务器的连接池:

pool := &redis.Pool{
    MaxIdle:     3,
    IdleTimeout: 240 * time.Second,
    Dial: func() (redis.Conn, error) {
        c, err := redis.Dial("tcp", ":6379")
        if err != nil {
            return nil, err
        }
        return c, err
    },
}
defer pool.Close()

接下来,可以使用以下代码订阅通道:

conn := pool.Get()
defer conn.Close()

psc := redis.PubSubConn{Conn: conn}

// 订阅通道
psc.Subscribe("channel")

// 处理消息
for {
    switch v := psc.Receive().(type) {
    case redis.Message:
        fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
    case redis.Subscription:
        fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
    case error:
        // 处理错误
        return
    }
}

上面的代码通过创建PubSubConn对象来进行订阅。然后,在无限循环中等待接收消息。当有新消息到达时,它将被打印出来。如果订阅发生错误,则循环将退出。

发送消息的方法与普通命令一样简单:

conn := pool.Get()
defer conn.Close()

_, err := conn.Do("PUBLISH", "channel", "hello world")
if err != nil {
    // 处理错误
}
  1. Stream

Stream是Redis 5.0引入的数据类型,可以用于实现高性能的流处理应用程序。redigo为Stream提供了方便的API。

首先,需要创建一个连接到Redis服务器的连接池:

pool := &redis.Pool{
    MaxIdle:     3,
    IdleTimeout: 240 * time.Second,
    Dial: func() (redis.Conn, error) {
        c, err := redis.Dial("tcp", ":6379")
        if err != nil {
            return nil, err
        }
        return c, err
    },
}
defer pool.Close()

接下来,可以使用以下代码向Stream添加条目:

conn := pool.Get()
defer conn.Close()

// 添加条目到Stream中
_, err := conn.Do("XADD", "mystream", "*", "message", "hello world")
if err != nil {
    // 处理错误
}

上面的代码通过执行XADD命令将一条包含“hello world”消息的新条目添加到名为“mystream”的Stream中。

可以使用以下代码从流中读取最后几个元素:

conn := pool.Get()
defer conn.Close()

// 从流中读取最后两个元素
res, err := redis.Values(conn.Do("XREVRANGE", "mystream", "+", "-", "COUNT", 2))
if err != nil {
    // 处理错误
}

// 解析结果
for len(res) > 0 {
    var id string
    var values []interface{}
    res, _ = redis.Scan(res, &id, &values)

    fmt.Printf("%s: %v\n", id, values)
}

上面的代码通过执行XREVRANGE命令从名为“mystream”的Stream中读取最后两个元素。然后,使用redis.Scan解析结果,并将它们打印出来。

redigo提供了方便的API来处理订阅/发布和Stream数据类型。这些功能使得它成为构建分布式系统和流处理应用程序的强大工具。

最后,创作不易,希望给大家我点赞收藏,谢谢!

你可能感兴趣的:(Golang,自学编程,golang,分布式,中间件)