Go之分布式

在使用分布式并发原语时,除了需要考虑可用性和数据一致性,还需要考虑分布式设计带来的性能损耗问题。所以,在使用之前,你一定要做好性能的评估。

分布式互斥锁

基于redis的分布式互斥锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package tool

import (
    "context"
    "errors"
    "github.com/go-redis/redis/v8"
    "time"
)

// 分布式互斥锁

// 默认重试次数
var retryTimes = 20

// 默认重试频率
var retryInterval = time.Millisecond * 50

var ctx = context.Background()

type DcsLock struct {
    rdb           *redis.Client
    RetryTimes    int
    RetryInterval time.Duration
}

func (m *DcsLock) SetRdb(rdb *redis.Client) *DcsLock {
    m.rdb = rdb
    m.RetryTimes = retryTimes
    m.RetryInterval = retryInterval
    return m
}

func (m *DcsLock) SetRetryTimes(i int) *DcsLock {
    m.RetryTimes = i
    return m
}

func (m *DcsLock) SetRetryInterval(t time.Duration) *DcsLock {
    m.RetryInterval = t
    return m
}

func (m *DcsLock) Lock(cacheKey string, ttl time.Duration) error {
    resp := false
    for i := 0; i < m.RetryTimes; i++ {
        if m.SetNX(cacheKey, ttl) {
            resp = true
            break
        }
        time.Sleep(m.RetryInterval)
    }
    if !resp {
        return errors.New("抢锁失败,请稍后重试")
    }
    return nil
}

func (m *DcsLock) Unlock(cacheKey string) {
    m.DelSetNX(cacheKey)
}

func (m *DcsLock) SetNX(cacheKey string, ttl time.Duration) bool {
    if m.rdb.SetNX(ctx, cacheKey, 1, ttl).Val() {
        return true
    }
    return false
}

func (m *DcsLock) DelSetNX(cacheKey string) {
    m.rdb.Del(ctx, cacheKey)
}

基于etcd的分布式互斥锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
    "context"
    "go.etcd.io/etcd/client/v3"
    "go.etcd.io/etcd/client/v3/concurrency"
    "log"
    "math/rand"
    "strings"
    "time"
)

// etcd实现分布式互斥锁
var (
    addr     = "http://127.0.0.1:23791,http://127.0.0.1:23792,http://127.0.0.1:23793"
    lockName = "my-test-lock"
)

func main() {
    rand.Seed(time.Now().UnixNano())

    // etcd地址
    endpoints := strings.Split(addr, ",")

    // 生成一个etcd client
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    useLock(cli) // 测试锁
}

func useLock(cli *clientv3.Client) {
    // 为锁生成session「节点宕机对应 session 销毁,持有的锁会被释放」
    s1, err := concurrency.NewSession(cli)
    if err != nil {
        log.Fatal(err)
    }
    defer s1.Close()

    // 得到一个分布式锁
    locker := concurrency.NewMutex(s1, lockName)

    // 请求锁
    log.Println("acquiring lock")
    if err := locker.Lock(context.TODO()); err != nil {
        log.Fatal(err)
    }
    log.Println("acquired lock")

    // 等待一段时间
    time.Sleep(time.Duration(rand.Intn(30)+3) * time.Second)

    // 释放锁
    if err := locker.Unlock(context.TODO()); err != nil {
        log.Fatal(err)
    }

    log.Println("released lock")
}

分布式读写锁

基于etcd的分布式读写锁

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
package main

import (
    "bufio"
    "flag"
    "fmt"
    "go.etcd.io/etcd/client/v3"
    "go.etcd.io/etcd/client/v3/concurrency"
    "go.etcd.io/etcd/client/v3/experimental/recipes"
    "log"
    "math/rand"
    "os"
    "strings"
    "time"
)

// etcd实现分布式读写锁
var (
    rwAddr     = "http://127.0.0.1:23791,http://127.0.0.1:23792,http://127.0.0.1:23793"
    rwLockName = "my-test-lock"
)

func main() {
    rand.Seed(time.Now().UnixNano())

    // 解析etcd地址
    endpoints := strings.Split(rwAddr, ",")

    // 创建etcd的client
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }

    defer cli.Close()

    // 创建session
    s1, err := concurrency.NewSession(cli) // 节点宕机对应 session 销毁,持有的锁会被释放
    if err != nil {
        log.Fatal(err)
    }

    defer s1.Close()

    m1 := recipe.NewRWMutex(s1, rwLockName)

    // 从命令行读取命令
    consolescanner := bufio.NewScanner(os.Stdin)
    log.Println("请输入指令w/r:")
    for consolescanner.Scan() {
        action := consolescanner.Text()
        switch action {
        case "w": // 请求写锁
            testWriteLocker(m1)
        case "r": // 请求读锁
            testReadLocker(m1)
        default:
            fmt.Println("unknown action")
        }
        log.Println("请输入指令w/r:")
    }
}

func testWriteLocker(m1 *recipe.RWMutex) {
    // 请求写锁
    log.Println("acquiring write lock")
    if err := m1.Lock(); err != nil {
        log.Fatal(err)
    }

    log.Println("acquired write lock")

    // 等待一段时间
    time.Sleep(time.Duration(rand.Intn(10)+3) * time.Second)

    // 释放写锁
    if err := m1.Unlock(); err != nil {
        log.Fatal(err)
    }

    log.Println("released write lock")
}

func testReadLocker(m1 *recipe.RWMutex) {
    // 请求读锁
    log.Println("acquiring read lock")
    if err := m1.RLock(); err != nil {
        log.Fatal(err)
    }

    log.Println("acquired read lock")

    // 等待一段时间
    time.Sleep(time.Duration(rand.Intn(10)+3) * time.Second)

    // 释放写锁
    if err := m1.RUnlock(); err != nil {
        log.Fatal(err)
    }

    log.Println("released read lock")
}

分布式队列

基于etcd的分布式队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

package main

import (
    "bufio"
    "fmt"
    "go.etcd.io/etcd/client/v3"
    recipe "go.etcd.io/etcd/client/v3/experimental/recipes"
    "log"
    "os"
    "strings"
)

// etcd实现分布式队列
var (
    etcdAddrQ     = "http://127.0.0.1:23791,http://127.0.0.1:23792,http://127.0.0.1:23793"
    queueName = "my-test-queue"
)

func main() {
    // 解析etcd地址
    endpoints := strings.Split(etcdAddrQ, ",")

    // 创建etcd的client
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    // 创建/获取队列
    q := recipe.NewQueue(cli, queueName)

    // 从命令行读取命令
    fmt.Println("请输入指令:push/pop,多参数用空格间隔")
    consolescanner := bufio.NewScanner(os.Stdin)
    for consolescanner.Scan() {
        action := consolescanner.Text()
        items := strings.Split(action, " ")
        switch items[0] {
        case "push": // 加入队列
            if len(items) != 2 {
                fmt.Println("must set value to push")
                continue
            }
            q.Enqueue(items[1]) // 入队
        case "pop": // 从队列弹出
            v, err := q.Dequeue() // 出队
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println(v) // 输出出队的元素
        case "quit", "exit": //退出
            return
        default:
            fmt.Println("unknown action")
        }

        fmt.Println("请输入指令:push/pop,多参数用空格间隔")
    }
}

分布式栅栏

基于etcd的分布式栅栏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import (
    "bufio"
    "fmt"
    "go.etcd.io/etcd/client/v3"
    recipe "go.etcd.io/etcd/client/v3/experimental/recipes"
    "log"
    "os"
    "strings"
)

// 分布式栅栏
var (
    etcdAddrB   = "http://127.0.0.1:23791,http://127.0.0.1:23792,http://127.0.0.1:23793"
    barrierName = "my-test-queue"
)

func main() {

    // 解析etcd地址
    endpoints := strings.Split(etcdAddrB, ",")

    // 创建etcd的client
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    // 创建/获取栅栏
    b := recipe.NewBarrier(cli, barrierName)

    // 从命令行读取命令
    fmt.Println("请输入指令hold/release/wait:")
    consolescanner := bufio.NewScanner(os.Stdin)
    for consolescanner.Scan() {
        action := consolescanner.Text()
        items := strings.Split(action, " ")
        switch items[0] {
        case "hold": // 持有这个barrier
            if err := b.Hold(); err != nil {
                fmt.Printf("hold fail: %v\n", err)
            } else {
                fmt.Println("hold success")
            }
        case "release": // 释放这个barrier
            if err := b.Release(); err != nil {
                fmt.Printf("released fail: %v\n", err)
            } else {
                fmt.Println("released success")
            }
        case "wait": // 等待barrier被释放
            if err := b.Wait(); err != nil {
                fmt.Printf("wait fail: %v\n", err)
            } else {
                fmt.Println("after wait")
            }
        case "quit", "exit": // 退出
            return
        default:
            fmt.Println("unknown action")
        }
        fmt.Println("请输入指令hold/release/wait:")
    }
}

分布式事务

todo

Session和Cookie

session与cookie属于一种会话控制技术,常用在身份识别,登录验证,数据传输等。

cookie,是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过 HTTP 协议将本地 cookie 内容发送给服务器,从而完成验证,或继续上一步操作。

依赖注入

go.uber.org/dig

依赖注入,抽象接口,依赖于抽象接口,而不依赖于具体的对象,实现接口的相互依赖。(可解决同级包的循环引用问题)

引用包:go.uber.org/dig

工具类方法汇总

循环定时功能

业务场景:如某个提醒功能,可以设定重复定时,设定周一某个时间点循环提醒。

重复定时截图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 计算出 weekdays 里距离 compareTime 最近的一个未来时间点
// weekdays: [1,2,3,4,5,6,7]周一、周二...周日, houMinute: 时分,compareTime:对比时间点,一般设置为time.Now()
func LatestTimeByWeekday(weekdays []int, hourMinute string, compareTime time.Time) (time.Time, error) {
    if len(weekdays) <= 0 {
        return time.Time{}, errors.New("weekdays参数不能为空")
    }

    // 对比时间点为周几,默认周日设置为7
    weekday := int(compareTime.Weekday())
    if weekday == 0 {
        weekday = 7
    }

    sort.Slice(weekdays, func(i, j int) bool {
        return weekdays[i] < weekdays[j]
    })

    getTime := func(addDay int) (time.Time, error) {
        date := compareTime.AddDate(0, 0, addDay).Format("2006-01-02 ") + hourMinute + ":00"
        return time.ParseInLocation("2006-01-02 15:04:05", date, time.Local)
    }

    var tmpTimeArr []time.Time
    for _, v := range weekdays {
        if weekday == v {
            // 检查当天时间点符不符合条件
            tmpTime, err := getTime(0)
            if err != nil {
                return time.Time{}, err
            }

            // 当天时间点不符合条件,直接取下一周的时间
            if tmpTime.Before(compareTime) || tmpTime.Equal(compareTime) {
                tmpTime, err := getTime(7)
                if err != nil {
                    return time.Time{}, err
                }
                tmpTimeArr = append(tmpTimeArr, tmpTime)
            } else {
                return tmpTime, nil
            }
        } else if weekday < v {
            tmpTime, err := getTime(v - weekday)
            if err != nil {
                return time.Time{}, err
            }
            return tmpTime, nil
        } else {
            tmpTime, err := getTime(7 - weekday + v)
            if err != nil {
                return time.Time{}, err
            }
            tmpTimeArr = append(tmpTimeArr, tmpTime)
        }
    }

    if len(tmpTimeArr) > 0 {
        return tmpTimeArr[0], nil
    }

    return time.Time{}, errors.New("异常错误")
}

Go 性能分析工具

一、PProf 简介

pprof 是用于可视化和分析性能分析数据的工具。

Golang是一个非常注重性能的语言,因此语言的内置库里就自带了性能分析库pprof。

性能分析和采集在计算机性能调试领域使用的术语是profile,或者有时候也会使用profiling代表性能分析这个行为。所以pprof名字里的prof来源于对单词profile的缩写,profile这个单词的原意是画像,那么针对程序性能的画像就是应用使用 CPU 和内存等等这些资源的情况。

简单的excel导入导出

excel导入导出代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package tool

import (
    "errors"
    "github.com/360EntSecGroup-Skylar/excelize/v2"
    "mime/multipart"
    "strconv"
)

const (
    maxCharCount     = 26       // maxCharCount 最多26个字符A-Z
    defaultSheetName = "Sheet1" // 默认Sheet名称
)

type Excel struct {
    SheetName string          // sheetName,默认为Sheet1
    Headers   []string        // 表头
    Rows      [][]interface{} // 表数据
}

func (m *Excel) SetDefaultSheetName() {
    if m.SheetName == "" {
        m.SheetName = defaultSheetName
    }
}

// 从文件流读取 Excel Sheet1 的所有数据
func (Excel) ReadExcelFromStream(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    ff, err := excelize.OpenReader(src)
    if err != nil {
        if err.Error() == "zip: not a valid zip file" {
            return rows, errors.New("无效文件,注意不支持加密的EXCEL文件")
        }
        return rows, errors.New(err.Error())
    }

    // 获取 Sheet1 上所有单元格
    rows, err = ff.GetRows(ff.GetSheetName(0))
    if err != nil {
        return rows, errors.New(err.Error())
    }

    return rows, nil
}

// 导出excel到浏览器
func (m Excel) ExportToWeb() ([]byte, error) {
    f, err := m.exportExcel()
    if err != nil {
        return nil, err
    }
    buffer, err := f.WriteToBuffer()
    if err != nil {
        return nil, err
    }
    return buffer.Bytes(), nil
}

// 导出excel到本地服务器
// path 文件导出路径,建议使用绝对路径
// filename 文件名,需带上后缀
func (m Excel) ExportToPath(path, filename string) (string, error) {
    var filePath string
    f, err := m.exportExcel()
    if err != nil {
        return filePath, err
    }
    filePath = path + "/" + filename
    if err := f.SaveAs(filePath); err != nil {
        return filePath, err
    }
    return filePath, nil
}

// exportExcel 导出Excel文件
// sheetName 工作表名称
// headers 列名切片, 表头
// rows 数据切片,是一个二维数组
func (m Excel) exportExcel() (*excelize.File, error) {
    m.SetDefaultSheetName()

    f := excelize.NewFile()
    sheetIndex := f.NewSheet(m.SheetName)
    maxColumnRowNameLen := 1 + len(strconv.Itoa(len(m.Rows)))
    columnCount := len(m.Headers)
    if columnCount > maxCharCount {
        maxColumnRowNameLen++
    } else if columnCount > maxCharCount*maxCharCount {
        maxColumnRowNameLen += 2
    }
    columnNames := make([][]byte, 0, columnCount)
    for i, header := range m.Headers {
        columnName := m.getColumnName(i, maxColumnRowNameLen)
        columnNames = append(columnNames, columnName)
        // 初始化excel表头,这里的index从1开始要注意
        curColumnName := m.getColumnRowName(columnName, 1)
        err := f.SetCellValue(m.SheetName, curColumnName, header)
        if err != nil {
            return nil, err
        }
    }
    for rowIndex, row := range m.Rows {
        for columnIndex, columnName := range columnNames {
            // 从第二行开始
            err := f.SetCellValue(m.SheetName, m.getColumnRowName(columnName, rowIndex+2), row[columnIndex])
            if err != nil {
                return nil, err
            }
        }
    }
    f.SetActiveSheet(sheetIndex)
    return f, nil
}

// getColumnName 生成列名
// Excel的列名规则是从A-Z往后排;超过Z以后用两个字母表示,比如AA,AB,AC;两个字母不够以后用三个字母表示,比如AAA,AAB,AAC
// 这里做数字到列名的映射:0 -> A, 1 -> B, 2 -> C
// maxColumnRowNameLen 表示名称框的最大长度,假设数据是10行,1000列,则最后一个名称框是J1000(如果有表头,则是J1001),是4位
// 这里根据 maxColumnRowNameLen 生成切片,后面生成名称框的时候可以复用这个切片,而无需扩容
func (m Excel) getColumnName(column, maxColumnRowNameLen int) []byte {
    const A = 'A'
    if column < maxCharCount {
        // 第一次就分配好切片的容量
        slice := make([]byte, 0, maxColumnRowNameLen)
        return append(slice, byte(A+column))
    } else {
        // 递归生成类似AA,AB,AAA,AAB这种形式的列名
        return append(m.getColumnName(column/maxCharCount-1, maxColumnRowNameLen), byte(A+column%maxCharCount))
    }
}

// getColumnRowName 生成名称框
// Excel的名称框是用A1,A2,B1,B2来表示的,这里需要传入前一步生成的列名切片,然后直接加上行索引来生成名称框,就无需每次分配内存
func (Excel) getColumnRowName(columnName []byte, rowIndex int) (columnRowName string) {
    l := len(columnName)
    columnName = strconv.AppendInt(columnName, int64(rowIndex), 10)
    columnRowName = string(columnName)
    // 将列名恢复回去
    columnName = columnName[:l]
    return
}

WEB端调用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package controller

import (
    "bytes"
    "github.com/labstack/echo/v4"
    "xxx/lib/tool"
    "net/http"
)

type xxxController struct{}

var XxxController = new(xxxController)

func init() {
    Router.GET("/export", AuthCorpController.Export).Name = "导出excel"
}

func (xxxController) Export(c echo.Context) error {
    var (
        excelHeaders = []string{"姓名", "性别", "年龄"}
        excelRows    = [][]interface{}{
            {"张三", "女", 18},
            {"李四", "男", 19},
        }
    )
    b, _ := tool.Excel{Headers: excelHeaders, Rows: excelRows}.ExportToWeb()
    c.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename=students.xlsx")
    return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(b))
}

csv导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package tool

import (
    "bufio"
    "bytes"
    "encoding/csv"
    "errors"
    "golang.org/x/text/encoding/simplifiedchinese"
    "golang.org/x/text/transform"
    "io"
    "mime/multipart"
    "strconv"
    "strings"
)

type Csv struct{}

// 从文件流读取 csv 的所有数据
func (m Csv) ReadCsvFromStream(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    // 初始化一个 csv reader,并通过这个 reader 从 csv 文件读取数据
    reader := csv.NewReader(transform.NewReader(bufio.NewReader(src), simplifiedchinese.GBK.NewDecoder()))

    // 设置返回记录中每行数据期望的字段数,-1 表示返回所有字段
    reader.FieldsPerRecord = -1
    // 通过 readAll 方法返回 csv 文件中的所有内容
    rows, err = reader.ReadAll()
    if err != nil {
        return rows, err
    }

    return rows, nil
}

// 从文件流读取不规范 csv 的所有数据
func (m Csv) ReadIllegalCsvFromSteam(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    // 直接复制文件内容
    var buf bytes.Buffer
    if _, err = io.Copy(&buf, transform.NewReader(bufio.NewReader(src), simplifiedchinese.GBK.NewDecoder())); err != nil {
        return rows, err
    }
    contents := buf.String()
    slice := strings.Split(contents, "\n")

    for _, v := range slice {
        records := strings.Split(strings.ReplaceAll(v, "=\"", "\""), ",") // 去掉不合法的等号
        for kk, vv := range records {
            records[kk] = strings.Trim(vv, "\"") // 去除前后的双引号
        }
        rows = append(rows, records)
    }
    return rows, nil
}

线上事故记录

一、数据库宕机

  1. 事故时间:2020-08-25

  2. 事故描述:

    1. 大量慢查询
    2. 阿里云数据库RDS,一主三从一备胎主库(共 5 个数据库),主从读写分离失效,大量请求涌入主库,主库承受不住请求压力,主库宕机,备胎主库也失效。
    3. 主从延迟严重,从库失效,全部查询堆在主库,这样也会导致主库宕机。(比如删除大量数据、更新大量数据的时候)
  3. 事故错误日志:

多系统对接-签名校验案例(GO)

接口通讯规范

  • 签名规则:

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 签名有效期暂定为 600 秒
    var platform = "baidu"     // 对接平台,正式测试环境保持一致
    var secret = "test_123456"   // 测试环境密钥,生产环境密钥私聊
    var requestTime = 1640163102 // 秒级时间戳
    var loginStaffId = 123 // 请求员工ID,便于权限控制
    
    // 生成签名
    sign := Md5(fmt.Sprintf("%d-%s-%s-%d", requestTime, platform, secret, loginStaffId))
    
  • 数据规范:

企业微信自建应用代开发

企业微信开发汇总

关于企业微信的所有,如开源企业微信sdk

企业微信开发自建应用代开发篇

企业微信开发第三方应用开发篇

企业微信开发自建内部应用开发篇

自建应用代开发上线流程

应用代开发上线流程,只看此文就够了(上)

应用代开发上线流程,只看此文就够了(中)

应用代开发上线流程,只看此文就够了(下)

疑问

  1. 自建应用代开发,服务商怎么获取授权企业的会话存档接口?

稳定排序和不稳定排序

首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。