Dawn's Blogs

分享技术 记录成长

0%

消息队列通信模型

队列模式

消息⽣产者⽣产消息发送到队列queue中,然后消息消费者从queue中取出并且消费消息。 ⼀条消息被消费以后,queue中就没有了,不存在重复消费。

发布/订阅

消息⽣产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点⽅式不同,发布到topic的消息会被所有订阅者消费


发布订阅模式下,当发布者消息量很⼤时,显然单个订阅者的处理能⼒是不⾜的。实际上现实场景中是多个订阅者节点组成⼀个订阅组负载均衡消费topic消息即分组订阅,这样订阅者很容易实现消费能⼒线性扩展。可以看成是⼀个topic下有多个queue,每个queue是点对点的⽅式,queue之间是发布订阅⽅式。

Kafka

Kafka是一种高吞吐量的分布式发布订阅消息系统。

同时Kafka结合队列模型和发布订阅模型

  • 对于同一个消费者组Consumer Group,一个message只有被一个consumer消费
  • 同一个消息可以被广播到不同的Consumer Group中

架构介绍

Kafka架构

  • Producer:Producer即⽣产者,消息的产⽣者,是消息的⼊⼝。
  • kafka cluster:kafka集群,⼀台或多台服务器组成
    • Broker:Broker是指部署了Kafka实例的服务器节点。每个服务器上有⼀个或多个kafka的实例,我们姑且认为每个broker对应⼀台服务器。每个kafka集群内的broker都有⼀个不重复的编号。
    • Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。实际应⽤中通常是⼀个业务线建⼀个topic。
    • Partition:Topic的分区,每个topic可以有多个分区,分区的作⽤是做负载,提⾼kafka的吞吐量。同⼀个topic在不同的分区的数据是不重复的,partition的表现形式就是⼀个⼀个的⽂件夹。为了提高可靠性,提出分区的副本,分为Leader和Follower,生产和消费只针对Leader。
    • Replication:每⼀个分区都有多个副本,副本的作⽤是做备份。当主分区(Leader)故障的时候会选择⼀个备胎(Follower)上位,成为Leader。在kafka中默认副本的最⼤数量是10个,且副本的数量不能⼤于Broker的数量,follower和leader绝对是在不同的机器,同⼀机器对同⼀个分区也只可能存放⼀个副本(包括⾃⼰)。
  • Consumer:消费者,即消息的消费⽅,是消息的出⼝。
    • Consumer Group:我们可以将多个消费组组成⼀个消费者组,在kafka的设计中同⼀个分区的数据只能被消费者组中的某⼀个消费者消费同⼀个消费者组的消费者可以消费同⼀个topic的不同分区的数据,这也是为了提⾼kafka的吞吐量!
阅读全文 »

概述

context主要用来在goroutine之间传递上下文信息

Go1.7加入了一个新的标准库context,它定义了Context类型,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作。

可以使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。

为什么需要Context?

如何解决通知goroutine退出?

全局变量

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
var wg sync.WaitGroup

// 全局变量的方式
var exit bool

func worker() {
for {
fmt.Println("worker")
time.Sleep(time.Second)
if exit { // 查看全局变量,检测是否退出
break
}
}
wg.Done()
}

func main() {
wg.Add(1)
go worker()
// 如何优雅的实现结束子goroutine
time.Sleep(time.Second * 5) // 防止退出太快
exit = true // 修改全局变量
wg.Wait()
fmt.Println("over")
}

channel

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
var wg sync.WaitGroup

// channel方式通知goroutine退出

func worker(exitChan <-chan struct{}) {
label:
for {
select {
case <-exitChan:
break label
default:
fmt.Println("worker")
time.Sleep(time.Second)
}
}
wg.Done()
}

func main() {

exitChan := make(chan struct{}) // 定义退出管道

wg.Add(1)
go worker(exitChan)
time.Sleep(time.Second * 5) // 防止退出太快
exitChan <- struct{} // 通知协程退出
wg.Wait()
fmt.Println("over")
}

context - 官方方案

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
var wg sync.WaitGroup

// context方式通知协程退出

func worker(ctx context.Context) {
label:
for {
select {
case <-ctx.Done():
break label
default:
fmt.Println("worker")
time.Sleep(time.Second)
}
}
wg.Done()
}

func main() {

ctx, cancel := context.WithCancel(context.Background())

wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5) // 防止退出太快
cancel() // 调用cancel函数,告诉goroutine退出
wg.Wait()
fmt.Println("over")
}

当goroutine又启动一个goroutine时,只需要将ctx传入即可。通过context库,可以控制子孙协程的退出。

阅读全文 »

复制带随机指针的链表

复制带随机指针的链表

解题思路

我们用哈希表记录每一个节点对应新节点的创建情况。

解题代码

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
/**
* Definition for a Node.
* type Node struct {
* Val int
* Next *Node
* Random *Node
* }
*/

func copyRandomList(head *Node) *Node {
cachedNode := map[*Node]*Node{}
var deepCopy func(head *Node) *Node
deepCopy = func(head *Node) *Node {
if head == nil {
return nil
}

if node, ok := cachedNode[head]; ok {
// 已经创建了这个节点
return node
}

// 创建新的节点
newNode := &Node{ Val: head.Val }
// 记录在哈希表中
cachedNode[head] = newNode
newNode.Next = deepCopy(head.Next)
newNode.Random = deepCopy(head.Random)

return newNode
}

return deepCopy(head)
}
阅读全文 »

ORM

ORM(Object Relational Mapping,对象关系映射),作用是在关系型数据库和对象之间作一个映射。

1
2
3
数据表	 <-->  类
数据行 <--> 类的实例
字段 <--> 类的字段

GROM入门

详细内容查看GORM文档

连接数据库

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
package main

import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

type UserInfo struct {
ID uint
Name string
Gender string
Hobby string
}

func main() {
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}

// 创建表 自动迁移(把结构体与数据表进行对应)
db.AutoMigrate(&UserInfo{})

// 插入数据
u1 := UserInfo{1, "Dawn", "男", "Study"}
db.Create(&u1)

// 查询
var res UserInfo
db.First(&res)
fmt.Println(res)

// 更新
db.Model(&res).Update("hobby", "Games")

// 删除
db.Delete(&res)
}
阅读全文 »

回文数

回文数

解题思路

  1. 将数字转化为字符串,再判断是否是回文字符串
  2. 反转一半数字,如果该数字是回文,其后半部分反转后应该与原始数字的前半部分相同。
    • 如何知道反转数字的位数已经达到原始数字位数的一半?当原始数字小于或等于反转后的数字时,就意味着我们已经处理了一半位数的数字了。

反转一半数字

阅读全文 »

Gin中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

定义中间件

Gin的中间件必须是一个gin.handlerFunc类型type HandlerFunc func(*Context)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TimeCost是一个统计请求耗时的中间件
func TimeCost(c *gin.Context) {
start := time.Now()
// 调用该请求的剩余处理程序(中间件)
c.Next()
// 不调用该请求的剩余处理程序
// c.Abort()
// 计算耗时
cost := time.Since(start)
fmt.Printf("Time cost = %v\n", cost)
}

// 可以将中间件定义为闭包的形式,HadlerFunc以函数值返回
func otherMiddleware() gin.HadlerFunc {
// 做一些校验/准备/查询数据库工作
// ....
return func(c *gin.Context) {
/*
一些中间件的处理逻辑
*/
}
}
阅读全文 »

文件上传

单文件

使用FormFile方法可以获取到POST请求中上传的文件:

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
func main() {
r := gin.Default()
r.LoadHTMLFiles("upload.html")

r.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", nil)
})

r.POST("/upload", func(c *gin.Context) {
// 从POST请求中读取文件
f, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
} else {
// 把文件保存到本地
dst := fmt.Sprintf("./%s", f.Filename)
c.SaveUploadedFile(f, dst)
c.JSON(http.StatusOK, gin.H{
"status": "OK",
})
}
})

r.Run(":9090")
}

多文件

请求头参数设置 Content-Type: multipart/form-data,多文件上传请求为:

1
2
3
4
curl -X POST http://localhost:8080/upload \
-F "upload[]=@/Users/appleboy/test1.zip" \
-F "upload[]=@/Users/appleboy/test2.zip" \
-H "Content-Type: multipart/form-data"

首先解析 multipart forms,然后获取文件列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
router := gin.Default()
// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["upload[]"]

for _, file := range files {
log.Println(file.Filename)

// 上传文件至指定目录
c.SaveUploadedFile(file, dst)
}
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
})
router.Run(":8080")
}

重定向

HTTP重定向

使用Redirect方法即可进行HTTP重定向:

1
2
3
r.GET("/index", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com")
})

通过 POST 方法进行 HTTP 重定向(这里可以使用 StatusMovedPermanently 301 状态码、StatusFound 302 状态码和 StatusSeeOther 303 状态码)。

1
2
3
r.POST("/test", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/foo")
})

303 状态码明确表示客户端应当采用 GET 方法获取资源,这点与 302 状态码有区别。

当 301、302、303 响应状态码返回时,几乎所有的浏览器都会把 POST 改成 GET,并删除请求报文内的主体,之后请求会自动再次发送。301、302 标准是禁止将 POST 方法改变成 GET 方法的,但实际使用时大家都会这么做。

阅读全文 »

Gin渲染

HTML渲染

首先编写模板文件index.tmpl

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>

Gin中使用LoadHTMLGlob()或者LoadHTMLFiles()进行模板渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
r := gin.Default()
// 模板解析
r.LoadHTMLFiles("templates/index.tmpl")

r.GET("/index", func(c *gin.Context) {
// 模板渲染
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Dawn ZH",
})
})
// 运行
r.Run(":9090")
}

阅读全文 »

概述

Gin是一个由Go语言编写的高性能Web框架。

简单案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func sayHello(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
}

func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行sayHello
r.GET("/hello", sayHello)
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}

RESTful

REST,即Representational State Transfer,表现层状态转化。RESTful架构:

  • 每一个URL代表一种资源
  • 客户端与服务器之间,传递这种资源的某种表现层(把资源具体呈现出来的形式,叫做它的表现层)
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现表现层状态转化:
    • GET用于获取资源
    • POST用于新建资源
    • PUT用于更新资源
    • DELETE用于删除资源

Gin框架支持开发RESTful API的开发:

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
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// RESTFul风格
r.GET("/book", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"method": "GET",
})
})
r.POST("/book", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"method": "POST",
})
})
r.PUT("/book", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"method": "PUT",
})
})
r.DELETE("/book", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"method": "DELETE",
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}

概述

多版本并发控制(Multi-Version Concurrency Control,MVCC),通过数据的多个版本管理来实现数据库的并发控制。

在MySQL中,可重复读是默认的隔离级别。可重复读隔离级别下,不仅解决了脏读不可重复读问题,因为使用了MVCC,所以还一定程度上解决了幻读的问题。

快照读

快照读又叫一致性读,读取的是快照数据不加锁的简单的SELECT语句,都属于快照读

之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

当前读

当前读读取的是记录的最新版本的数据,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁加锁的SELECT语句,以及对数据进行增删改都会进行当前读

阅读全文 »