consul实现分布式锁
分布式一致性问题:
分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务
、分布式锁
等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行
分布式锁:
是在分布式系统之间同步访问共享资源的一种方式。
不同的系统或者同一个系统不同的主机共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰,从而保证数据的一致性,怎么保证数据的一致性,就用到了分布式锁
那么用锁来解决资源抢占时,又有哪些问题:
1、死锁
死锁是在并发编程中理论上都会出现的问题
什么是死锁:
抢占资源的各方,彼此都在等待对方释放资源,以便自己能获取系统资源,但是没有哪一方退出,这时候就死锁了
产生死锁的4个条件:
- 互斥条件
- 不可抢占条件
- 占用并申请条件
- 循环等待条件
解决方法:
解决死锁的问题只要解决了上面的4个条件之一即可。那怎么解决呢?
一般用 session + TTL 打破循环等待条件
当一个客户端尝试操作一把分布式锁的时候,我们必须校验其 session 是否为锁的拥有者,不是则无法进行操作。
当一个客户端已经持有一把分布式锁后,发生了掉线**,在超出了 TTL 时间后无法连接上,则回收其锁的拥有权。
2、惊群效应
什么是惊群效应?
简单说来,多线程/多进程等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群。可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择)。这种性能浪费现象就是惊群。
为了更好的理解何为惊群,举一个很简单的例子,当你往一群鸽子中间扔一粒谷子,所有的各自都被惊动前来抢夺这粒食物,但是最终注定只可能有一个鸽子满意的抢到食物,没有抢到的鸽子只好回去继续睡觉,等待下一粒谷子的到来。这里鸽子表示进程(线程),那粒谷子就是等待处理的事件
解决方法:
为了避免发生惊群效应, Nginx 和 ZooKeeper 分别使用了不同的方案解决,但是他们的核心解决思路都是一致的,
下面我们来看看 ZooKeeper 是怎么解决 惊群效应 的。
我们都知道,在 ZooKeeper 内部会使用临时目录节点的形式创建分布式锁,其中每个节点对应一个客户端的申请锁请求。
当客户端来请求该锁的时候, ZooKeeper 会生成一个
${lock-name}-${i}
的临时目录,此后每次请求该锁的时候,就会生成${lock-name}-${i+1}
的目录,如果此时在${lock-name}
中拥有最小的i
的客户端会获得该锁,而该客户端使用结束之后,就会删除掉自己的临时目录,并通知后续节点进行锁的获取。
没错,这个 i
是 ZooKeeper 解决惊群效应的利器,它被称为 顺序节点
Nginx怎么解决惊群
Nginx中处理epoll惊群问题的思路很简单,多个子进程有一个锁,谁拿到锁,谁才将accept的fd加入到epoll队列中,其他的子进程拿不到锁,也就不会将fd加入到epoll中,连接到来也就不会导致所有子进程的epoll被唤醒返回
3、脑裂(brain-split)
什么是脑裂?
脑裂主要是仲裁之间网络中断或不稳定导致
当集群中出现 脑裂 的时候,往往会出现多个 master 的情况,这样数据的一致性会无法得到保障,从而导致整个服务无法正常运行
解决方法:
- 可以将集群中的服务作为 P2P 节点,避免 Leader 与 Salve 的切换
- 向客户端发起重试,如果一段时间后依然无法连接上,再让下一个顺序客户端获取锁
consul怎么解决上面3个问题
Consul 是 Go 实现的一个轻量级 服务发现 、KV存储 的工具,它通过强一致性的KV存储实现了简易的 分布式锁 ,下面我们根据源码看下 Consul 是怎么解决以上分布式锁的难点的
// api/lock.go
// Lock 分布式锁数据结构
type Lock struct {
c *Client // 提供访问consul的API客户端
opts *LockOptions // 分布式锁的可选项
isHeld bool // 该锁当前是否已经被持有
sessionRenew chan struct{} // 通知锁持有者需要更新session
locksession string // 锁持有者的session
l sync.Mutex // 锁变量的互斥锁
}
// LockOptions 提供分布式锁的可选项参数
type LockOptions struct {
Key string // 锁的 Key,必填项,且必须有 KV 的写权限
Value []byte // 锁的内容,以下皆为选填项
Session string // 锁的session,用于判断锁是否被创建
SessionOpt *SessionEntry // 自定义创建session条目,用于创建session,避免惊群
SessionName string // 自定义锁的session名称,默认为 "Consul API Lock"
SessionTTL string // 自定义锁的TTL时间,默认为 "15s"
MonitorRetries int // 自定义监控的重试次数,避免脑裂问题
MonitorRetryTime time.Duration // 自定义监控的重试时长,避免脑裂问题
LockWaitTime time.Duration // 自定义锁的等待市场,避免死锁问题
LockTryOnce bool // 是否只重试一次,默认为false,则为无限重试
}
惊群
SessionOpt *SessionEntry // 自定义创建session条目,用于创建session,避免惊群
死锁
LockWaitTime time.Duration // 自定义锁的等待市场,避免死锁问题
脑裂
MonitorRetries int // 自定义监控的重试次数,避免脑裂问题
MonitorRetryTime time.Duration // 自定义监控的重试时长,避免脑裂问题
LockTryOnce bool // 是否只重试一次,默认为false,则为无限重试
从 LockOptions
中带有 session / TTL / monitor / wait 等字眼的成员变量可以看出,consul 已经考虑到解决我们上一节提到的三个难点,下面来看看实现代码中是如何使用的
先来看看生成可用的分布式锁的函数 LockOpts
:
// api/lock.go
// LockOpts 通过传入锁的参数,返回一个可用的锁
// 必须注意的是 opts.Key 必须在 KV 中有写权限
func (c *Client) LockOpts(opts *LockOptions) (*Lock, error) {
if opts.Key == "" {
return nil, fmt.Errorf("missing key")
}
if opts.SessionName == "" {
opts.SessionName = DefaultLockSessionName // "Consul API Lock"
}
if opts.SessionTTL == "" {
opts.SessionTTL = DefaultLockSessionTTL // "15s"
} else {
if _, err := time.ParseDuration(opts.SessionTTL); err != nil {
return nil, fmt.Errorf("invalid SessionTTL: %v", err)
}
}
if opts.MonitorRetryTime == 0 {
opts.MonitorRetryTime = DefaultMonitorRetryTime // 2 * time.Second
}
if opts.LockWaitTime == 0 {
opts.LockWaitTime = DefaultLockWaitTime // 15 * time.Second
}
l := &Lock{
c: c,
opts: opts,
}
return l, nil
}
我们可以在这个函数中可以注意到:
- 15s 的 SessionTTL 用于解决死锁、脑裂问题。
- 2s 的 MonitorRetryTime 是一个长期运行的协程用于监听当前锁持有者,用于解决脑裂问题。
- 15s 的 LockWaitTime 用于设置尝试获取锁的超时时间,用于解决死锁问题。
Lock
有3个可供其他包访问的函数,分别为 Lock
/ Unlock
/ Destroy
,下面按照顺序展开细说
Lock()函数
// Lock尝试获取一个可用的锁,可以通过一个非空的 stopCh 来提前终止获取
// 如果返回的锁发生异常,则返回一个被关闭的 chan struct ,应用程序必须要处理该情况
func (l *Lock) Lock(stopCh <-chan struct) (<-chan struct{}, error) {
// 先锁定本地互斥锁
l.l.Lock()
defer l.l.Unlock()
// 本地已经获取到分布式锁了
if l.isHeld {
return nil, ErrLockHeld
}
// 检查是否需要创建session
l.lockSession = l.opts.Session
if l.lockSession == "" {
s, err := l.createSession()
if err != nil {
return nil, fmt.Errorf("failed to create session: %v", err)
}
l.sessionRenew = make(chan struct{})
l.lockSession = s
session := l.c.Session()
go session.RenewPeriodic(l.opts.SessionTTL, s, nil, l.sessionRenew)
// 如果我们无法锁定该分布式锁,清除本地session
defer func() {
if !l.isHeld {
close(l.sessionRenew)
l.sessionRenew = nil
}
}()
// 准备向consul KV发送查询锁操作的参数
kv := l.c.KV()
qOpts := &QueryOptions{
WaitTime: l.opts.LockWaitTime,
}
start := time.Now()
attempts := 0
WAIT:
// 判断是否需要退出锁争夺的循环
select {
case <-stopCh:
return nil, nil
default:
}
// 处理只重试一次的逻辑
if l.opts.LockTryOnce && attempts > 0 { // 配置该锁只重试一次且已经重试至少一次了
elapsed := time.Since(start) // 获取当前时间偏移量
if elapsed > qOpts.WaitTime { // 当超过设置中的剩余等待时间
return nil, nil // 返回空结果
}
qOpts.WaitTime -= elapsed // 重设剩余等待时间
}
attempts++ // 已尝试次数自增1
// 阻塞查询该存在的分布式锁,直至无法获取成功
pair, meta, err := kv.Get(l.opts.Key, qOpts)
if err != nil {
return nil, fmt.Errorf("failed to read lock: %v", err)
}
}
}
Unlock()函数
// Unlock 尝试释放 consul 分布式锁,如果发生异常则返回 error
func (l *Lock) Unlock() error {
// 在释放锁之前必须先把 Lock 结构锁住
l.l.Lock()
defer l.l.Unlock()
// 确认我们依然持有该锁
if !isHeld {
return ErrLockNotHeld
}
// 提前先将锁的持有权释放
l.isHeld = false
// 清除刷新 session 通道
if l.sessionRenew != nil {
defer func() {
close(l.sessionRenew)
l.sessionRenew = nil
}()
}
// 获取当前 session 持有的锁信息
lockEnt := l.lockEntry(l.lockSession)
l.lockSession = ""
kv := l.c.KV()
_, _, err := kv.Release(lockEnt, nil) // 将持有的锁尝试释放
if err != nil {
return fmt.Errorf("failed to release lock: %v", err)
}
return nil
}
Destry()函数
// Destroy 尝试销毁分布式锁,虽然这不是必要的操作。
// 如果该锁正在被使用,则返回error
func (l *Lock) Destroy() error {
// 在释放锁之前必须先把 Lock 结构锁住
l.l.Lock()
defer l.l.Unlock()
// 确认我们依然持有该锁
if !isHeld {
return ErrLockNotHeld
}
// 获取锁
kv := l.c.KV()
pair, _, err := kv.Get(l.opts.Key, nil)
if err != nil {
return fmt.Errorf("failed to read lock: %v", err)
}
if pair == nil {
return nil
}
// 检查是否有可能状态冲突
if pair.Flags != LockFlagValue {
return ErrLockConflict
}
// 如果锁正在被持有,则返回异常
if pair.Session != "" {
return ErrLockUse
}
// 尝试通过 CAS 删除分布式锁
didRemove, _, err := kv.DeleteCAS(pair, nil)
if err != nil {
return fmt.Errorf("failed to remove lock: %v", err)
}
if !didRemove { // 如果没有删除成功,则返回异常
return ErrLockInUse
}
return nil
}
用golang实现的小demo
package main
import (
api "github.com/hashicorp/consul/api"
"github.com/satori/go.uuid"
"log"
)
func main() {
client, err := api.NewClient(&api.Config{
Address: "127.0.0.1:8500",
})
lockKey := "demo-lock-key"
lock, err := client.lockOpts(&api.LockOptions{
Key: lockKey,
Value: []byte("sender 1"),
SessionTTL: "10s",
SessionOpts: &spi.SessionEntry{
Checks: []string{"check1", "check2"},
Behavior: "release",
},
SessionName: uuid.Must(uuid.NewV4()),
})
if err != nil {
log.Fatalf("failed to created lock %v", err)
}
result, err := lock.Lock(nil)
if err != nil {
log.Fatalf("failed to accquired lock")
}
}
参考: https://www.jianshu.com/p/44307a394fe1,特别感谢
consul实现分布式锁的更多相关文章
- 玩转CONSUL(2)–分布式锁
1. 前言 分布式锁的场景,大家应该都有遇到过.比如对可靠性有较高要求的系统中,我们需要做主备切换.这时我们可以利用分布式锁,来做选主动作,抢到锁作为主,执行对应的任务,剩余的实例作为备份 redis ...
- 服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁
一.基于key/value实现 我们在构建分布式系统的时候,经常需要控制对共享资源的互斥访问.这个时候我们就涉及到分布式锁(也称为全局锁)的实现,基于目前的各种工具,我们已经有了大量的实现方式,比如: ...
- 基于Redis的分布式锁真的安全吗?
说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...
- Java分布式锁看这篇就够了
### 什么是锁? 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量. 而同步的本质是通过锁来实现的 ...
- 利用consul在spring boot中实现最简单的分布式锁
因为在项目实际过程中所采用的是微服务架构,考虑到承载量基本每个相同业务的服务都是多节点部署,所以针对某些资源的访问就不得不用到用到分布式锁了. 这里列举一个最简单的场景,假如有一个智能售货机,由于机器 ...
- Redis(七)分布式锁
前面学习了Redis的数据结构以及命令.Redis中的事务和Redis对Lua脚本的支持. 这一章就对Redis这些特性做一下实战性应用--基于Redis的分布式锁实现. Lock和Distribut ...
- Anno&Viper -分布式锁服务端怎么实现
1.Anno简介 Anno是一个微服务框架引擎.入门简单.安全.稳定.高可用.全平台可监控.依赖第三方框架少.底层通讯RPC(Remote Procedure Call)采用稳定可靠经过无数成功项目验 ...
- 使用redis构建可靠分布式锁
关于分布式锁的概念,具体实现方式,直接参阅下面两个帖子,这里就不多介绍了. 分布式锁的多种实现方式 分布式锁总结 对于分布式锁的几种实现方式的优劣,这里再列举下 1. 数据库实现方式 优点:易理解 缺 ...
- 分布式锁1 Java常用技术方案
前言: 由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题.所以自己结合实际工作中的一些经验和网上看到的一些资 ...
随机推荐
- Dynamics CRM项目实例之九:CRM 2015的产品中的捆绑销售
关注本人微信和易信公众号: 微软动态CRM专家罗勇,回复140或者20150112可方便获取本文,同时可以在第一时间得到我发布的最新的博文信息,follow me! 今天的博客主要是介绍Dy ...
- hadoop集群的搭建
hadoop集群的搭建 1.ubuntu 14.04更换成阿里云源 刚刚开始我选择了nat模式,所有可以连通网络,但是不能ping通,我就是想安装一下mysql,因为安装手动安装mysql太麻烦了,然 ...
- ElasticSearch head 插件安装
head 客户端可以很方便在上面创建索引,类型,文档,还有查询,使用它管理elasticsearch 提高效率. 在安装head 客户端之前必须安装node.js 环境,因为它是用node.js 编写 ...
- 数据结构java(一)数组链表
链表是数据结构中最基础的内容,链表在存储结构上分成两种:数组形式储存,链式存储. 相比c语言需要的结构体,在java中由于有了面向对象编程,将指针‘藏’了起来,不需要分配内存. 所以只需要创建一个对象 ...
- 无限极分类(adjacency list)的三种方式(迭代、递归、引用)
一般的分类树状结构有两种方式: 一种是adjacency list,也就是是id,parent id这中形式. 另一种是nested set,即左右值的形式. 左右值形式查询起来比较高效,无需递归等, ...
- 【视频】设计模式(Java)视频讲解
设计模式(JAVA) 视频网址: http://www.qghkt.com/ 设计模式(JAVA)视频地址: https://ke.qq.com/course/318643?tuin=a508ea62 ...
- nginx地址代理(2)
本章要讲的是服务器的代理: Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器, 一对一代理:就一个服务器监听 server { listen ...
- emacs单词首字母,单词,区域大小写转换
从光标开始,处理单词后半部分: 快捷键 说明 M-c (capitalize-word) 首字母改为大写 M-u (upcase-word) 全部改为大写 M-l (downcase-word) 全部 ...
- scp远程拷贝文件及文件夹
scp : 远程copy 命令 -r : 递归copy 从Linux Copy 到 Linux 从Linux Copy 到 Windows (当前目录使用. 就可以了) scp -r root@10. ...
- Surging微服务的注意事项
做个记录 1.Service的方法必须是异步方法 这个是同事发现的,非异步方法Swagger会用不了 2.仓储层不能用接口 这个是自己做的,根据同事的例子,本来好好的,想着在仓储层给加个接口,然后用接 ...