go语言分布式id生成器及分布式锁源码分析
来源:亿速云
时间:2023-05-01 21:36:13 399浏览 收藏
一分耕耘,一分收获!既然打开了这篇文章《go语言分布式id生成器及分布式锁源码分析》,就坚持看下去吧!文中内容包含go语言、id等等知识点...希望你能在阅读本文后,能真真实实学到知识或者帮你解决心中的疑惑,也欢迎大佬或者新人朋友们多留言评论,多给建议!谢谢!
本文小编为大家详细介绍“go语言分布式id生成器及分布式锁源码分析”,内容详细,步骤清晰,细节处理妥当,希望这篇“go语言分布式id生成器及分布式锁源码分析”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
分布式 id 生成器
在分布式场景中,唯一 id 的生成算比较重要。
而通常在高并发场景中,需要类似 MySQL 自增 id 一样不断增长且又不会重复的 id,即 MySql 的主键 id。
比如,在电商 618 或者双 11 搞活动的时候,一般在 0 点 开始,会有千万到亿级的订单量写入,每秒大概需要处理 10 万加的订单。
而在将订单插入数据库之前,我们在业务上需要给订单一个唯一的 id,即利用 idMaker 生存唯一的订单号,再插入数据库内。如果生成的 id 是随机且没有含义的纯数字的话,在大订单量的情况下,对数据库进行增删改查时就不能起到提高效率的作用。所以 此 id 应该应该包含一些时间信息,机器信息等,这样即使后端的系统对消息进行了分库分表,也能够以时间顺序对这些消息进行排序了。
比较典型的就是推特的【雪花算法】了,在以上场景下可以算是最优解,原理如图:
首先确定的是,id 数值长度是 64 位,int64 类型,除去开头的符号位 unused ,其它可以分为四个部分:
41 位来表示收到请求时的时间戳,单位为毫秒
5 位表示数据中心的 id
5 位表求机器的实例 id
12 位为循环自增 id,到达 1111,1111,1111 后归就会 0
以上机制原理生成的 id,可以支持一台机器在一毫秒内能够产生 4096 条消息。也就是一秒共 409.6w 条消息。单单从值域上来讲是完全够用。
数据中心 id 加上实例 id 共有 10 位,每个数据中心可以部署 32 台实例,搭建 32 个数据中心,所以可以一共部署 1024 台实例。
而 41 位的时间戳(毫秒为单位)能够使用 69 年。
worker_id 如何分配
timestamp(时间戳),datacenter_id(数据中心),worker_id(机器 ID) 和 sequence_id(序号) 这四个字段中,timestamp 和 sequence_id 是由程序在运行期生成的。但 datacenter_id 和 worker_id 需要在部署阶段就要能够获取得到,并且一旦程序启动之后,就是不可更改的了,因为如果可以随意更改,可能会造成最终生成的 id 有冲突。
不过一般不同数据中心的机器,会提供对应的获取数据中心 id 的 API,因此 datacenter_id 我们可以在部署阶段轻松地获取到。而 worker_id 是我们逻辑上给机器分配的一个 id,比较简单的做法就是由能够提供这种自增 id 功能的工具来支持,比如 MySql:
mysql> insert into a (ip) values("10.115.4.66"); Query OK, 1 row affected (0.00 sec) mysql> select last_insert_id(); +------------------+ | last_insert_id() | +------------------+ | 2 | +------------------+ 1 row in set (0.00 sec)
从 MySql 中获取到 worker_id 之后,就把这个 worker_id 直接持久化到本地,以避免每次上线时都需要获取新的 worker_id。让单实例的 worker_id 可以始终保持不变。
但是,使用 MySQL 的话,相当于给 id 生成服务增加了一个外部依赖。当然依赖越多,服务的运维成本就会增加。
考虑到集群中即使有单个 id 生成服务的实例挂了,也就是损失一段时间的一部分 id,所以我们也可以更简单暴力一些,把 worker_id 直接写在 worker 的配置中,上线时,由部署脚本完成 worker_id 字段替换即可。
开源示例:标准雪花算法
github.com/bwmarrin/snowflake
是一个相对轻量级的 snowflake 的 Go 实现。其文档对各位使用的定义如下图所示:
此库和标准的 snowflake 实现方式全完一致,使用也比较简单,直接上示例代码:
package main import ( "fmt" "github.com/bwmarrin/snowflake" ) func main() { node, err := snowflake.NewNode(1) if err != nil { println(err.Error()) os.Exit(1) } for i := 0; i分布式锁
单机程序并发或并行修改全局共享变量时,需要对修改行为加锁。因为如果不加锁,多个协程序就会对该变量竞争,然后得到的结果就会不准确,或者说得到的结果不是我们所预期的,比如下面的例子:
package main func main() { var wg sync.WaitGroup var count = 0 for i := 1; i多次运行结果不同:
➜ go run main.go
884
➜ go run main.go
957
➜ go run main.go
923预期的结果是:999
进程内加锁
而如果想要得到正确(预期)的结果,要把计数器的操作代码部分加上锁:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup var lock sync.Mutex var count = 0 for i := 1; i这样能够得到正确结果:
➜ go run main.go
999尝试加锁 tryLock
在某些场景,我们往往只希望一个任务有单一的执行者,而不像计数器一样,所有的 Goroutine 都成功执行。后续的 Goroutine 在抢锁失败后,需要放弃执行,这时候就需要用到尝试加锁,即实现 trylock。
尝试加锁,在加锁成功后执行后续流程,失败时不可以阻塞,而是直接返回加锁的结果。
在 Go 语言中可以用大小为 1 的 Channel 来模拟 trylock:
package main import ( "fmt" "sync" ) type MyLock struct { lockCh chan struct{} } func NewLock() MyLock { var myLock MyLock myLock = MyLock{ lockCh:make(chan struct{}, 1), } myLock.lockCh每个 Goruntine 只有获取到锁(成功执行了 Lock)才会继续执行后续代码,然后在 Unlock()时可以保证 Lock 结构体里的 Channel 一定是空的,所以不会阻塞也不会失败。
在单机系统中,tryLock 并不是一个好选择,因为大量的 Goruntine 抢锁会无意义地占用 cpu 资源,这就是活锁,所有不建议使用这种锁。
基于 Redis 的 setnx 分布式锁
在分布式场景中,也需要“抢占”的逻辑,可以用 Redis 的 setnx 实现:
package main import ( "github.com/go-redis/redis" "sync" "time" ) func setnx() { client := redis.NewClient(&redis.Options{}) var lockKey = "counter_lock" var counterKey = "counter" // lock resp := client.SetNX(lockKey, 1, time.Second*6) lockStatus, err := resp.Result() if err != nil || !lockStatus { println("lock failed") return } // counter++ getResp := client.Get(counterKey) cntValue, err := getResp.Int64() if err == nil || err == redis.Nil { cntValue++ resp := client.Set(counterKey, cntValue, 0) _, err := resp.Result() if err != nil { println(err) } } println("current counter is ", cntValue) // unlock delResp := client.Del(lockKey) unlockStatus, err := delResp.Result() if err == nil && unlockStatus > 0 { println("unlock success") } else { println("unlock failed", err) } } func main() { var wg sync.WaitGroup for i := 0; i运行结果:
➜ go run main.go
lock failed
lock failed
lock failed
lock failed
lock failed
current counter is 34
lock failed
unlock success通过上面的代码和执行结果可以看到,远程调用 setnx 运行流程上和单机的 troLock 非常相似,如果获取锁失败,那么相关的任务逻辑就不会继续向后执行。
setnx 很适合高并发场景下用来争抢一些“唯一”的资源。比如,商城秒杀的商品,在某个时间点,多个买家会对其进行下单并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不同设备的时间不能保证使用的是统一的时间,也就不能保证时序。
所以,我们需要依赖于这些请求到达 redis 节点的顺序来做正确的抢锁操作。
如果用户的网络环境比较差,是有可能抢不到的。
基于 ZooKeeper 分布式锁
基于 ZooKeeper 的锁与基于 Redis 的锁有点类似,不同之处在于 Lock 成功之前会一直阻塞,这与单机场景中的 mutex.Lock 很相似。
package main import ( "github.com/go-zookeeper/zk" "time" ) func main() { c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) if err != nil { panic(err) } l := zk.NewLock(c, "/lock", zk.WorldACL(zk.PermAll)) err = l.Lock() if err != nil { panic(err) } println("lock success, do your business logic") time.Sleep(time.Second * 10) // 模拟业务处理 l.Unlock() println("unlock success, finish business logic") }其原理也是基于临时 Sequence 节点和 watch API,例如我们这里使用的是
/lock
节点。Lock 会在该节点下的节点列表中插入自己的值,只要节点下的子节点发生变化,就会通知所有 watch 该节点的程序。这时候程序会检查当前节点下最小的子节点的 id 是否与自己的一致。如果一致,说明加锁成功了。
这种分布式的阻塞锁比较适合分布式任务调度场景,但不适合高频次持锁时间短的抢锁场景。
一般基于强一致协议的锁适用于粗粒度的加锁操作。这里的粗粒度指锁占用时间较长。我们在使用时也应思考在自己的业务场景中使用是否合适。
读到这里,这篇“go语言分布式id生成器及分布式锁源码分析”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注golang学习网行业资讯频道。
到这里,我们也就讲完了《go语言分布式id生成器及分布式锁源码分析》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于golang的知识点!
-
185 收藏
-
460 收藏
-
430 收藏
-
450 收藏
-
320 收藏
-
233 收藏
-
322 收藏
-
181 收藏
-
316 收藏
-
244 收藏
-
300 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习