物流企业网站模板,长春好的做网站公司排名,免费设计app软件,文化传播网站模板什么是分布式锁
分布式锁#xff08;多服务共享锁#xff09;在分布式的部署环境下#xff0c;通过锁机制来让多客户端互斥的对共享资源进行访问/操作。
为什么需要分布式锁
在单体应用服务里#xff0c;不同的客户端操作同一个资源#xff0c;我们可以通过操作系统提供…什么是分布式锁
分布式锁多服务共享锁在分布式的部署环境下通过锁机制来让多客户端互斥的对共享资源进行访问/操作。
为什么需要分布式锁
在单体应用服务里不同的客户端操作同一个资源我们可以通过操作系统提供的互斥锁/信号量等等来提供互斥的能力保证操作资源的只有一个客户端。
在分布式的情况里就需要第三方组件来保证对统一资源的操作的互斥。
下单中两个人下单一个人下单请求走订单服务A机器另一个人下单请求走订单服务B这样用单体的思维处理就可能不是很合适需要借用第三方组件配合来实现分布式锁 分布式锁可以用redis, zookeeper etcd等等来实现下面我们简单说说....
redis分布式锁
简单例子
set key value ex/px nx 或setnx
以setnx 为例可以使用 setnx key value 来进行 加锁 ( setnx 主要 是nx加了语义如果存在就不操作不存在就添加)多个客户端确保只有一个加锁成功去操作统一资源
127.0.0.1:6379[1] setnx lockObj 1 // 加锁
(integer) 1
127.0.0.1:6379[1] setnx lockObj 1 // 存在了就不能再加锁
(integer) 0
127.0.0.1:6379[1] get lockObj
1
127.0.0.1:6379[1] del lockObj // 释放锁
(integer) 1
127.0.0.1:6379[1] get lockObj
(nil)
127.0.0.1:6379[1] setnx lockObj 1
(integer) 1 上面就是简单的加锁的例子仔细思考下分布式锁使用的使用我们需要考虑哪些问题
存在的问题
简单的总结了下我们在使用redis分布式锁的时候需要考虑如下情况
1- 死锁问题
2- 续锁生命周期
3- 操作原子性
4- 锁的归属权
5- redis 集群锁的状态一致性
死锁问题
如果我们业务代码出现bug或服务器出现问题没有及时释放锁那么其他的客户端就永远获取不到这个加锁的资格。
这个时候我们就可以加上对应的处理逻辑
golang 加上defer 加锁锁逻辑 python try-Except-finally 用finally 释放锁。并且在加锁的时候加上过期时间根据业务进行合适的加
续锁生命周期
上面我们可以解决锁的释放问题但是我们的业务处理时间不一定百分百能知道处理的时间这个时候如果锁过期了但是资源操作没有做完那么就会出现问题。
在java的 Redisson 有个watch 续命机制 golang 的话可以 借鉴 Rllock 开启一个守护进程监听定时续命一定要提前续命不要等到到时间再续
操作原子性
在我们进行加锁 加过期时间的时候这两个操作不能分两步操作。因为如果setnx加锁成功了这时候失败了那么这个锁就永远被占用了。
根据这个问题我们可以使用lua脚本或者使用第三方模块是可以同时进行这两个步骤的
if redis.call(setnx, KEYS[1], ARGV[1]) 1 then return redis.call(PEXPIRE,KEYS[1],ARGV[2]) else return 0 end
锁的归属权问题
现在有几个场景
1- 客户端1 拿到锁处理业务 没处理完已经过期了这时候客户端2 拿到锁在处理结果客户端1 处理完就释放锁了
2- 一个业务不同画像的人处理的业务不同这时候我们就需要根据不同画像人进行分配 “锁”
我们在实际开发的时候有时候需要了解业务场景 有时候需要给锁加一个所属权的令牌。可以在setnx key时key设定特殊化的数值
redis 集群锁的状态一致性
在redis采用集群主从如果master加锁失败了这时候服务宕机了slave还同步这个 key 那么这个时候就会有客户端加锁成功
redis作者对于这个问题提出了解答REDLOCK
zookeeper 实现分布式锁
简单例子
[zk: localhost:2181(CONNECTED) 62] create /lock
Created /lock
[zk: localhost:2181(CONNECTED) 63] create -s -e /lock/req // 创建临时节点
Created /lock/req0000000000
[zk: localhost:2181(CONNECTED) 64] create -s -e /lock/req
Created /lock/req0000000001
[zk: localhost:2181(CONNECTED) 65] create -s -e /lock/req
Created /lock/req0000000002
[zk: localhost:2181(CONNECTED) 67] ls /lock // 节点下的临时节点
[req0000000000, req0000000001, req0000000002]
[zk: localhost:2181(CONNECTED) 68] delete /lock/req0000000000 // 释放第一锁golang的例子
package mainimport (fmtgithub.com/samuel/go-zookeeper/zksorttime
)func sortChildren(children []string) {sort.Slice(children, func(i, j int) bool {return children[i] children[j]})
}
func main() {go func() {conn, _, err : zk.Connect([]string{xx.xx.xx.xx:2181}, time.Second*5)if err ! nil {fmt.Println(Connect:, err.Error())return}defer conn.Close()lockPath : /locksObjlockName : lock// 创建锁的根节点_, err conn.Create(lockPath, []byte{}, int32(0), zk.WorldACL(zk.PermAll))if err ! nil err ! zk.ErrNodeExists {fmt.Println(Create:, err.Error())return}// 获取锁lockNodePath, err : conn.CreateProtectedEphemeralSequential(lockPath/lockName-, []byte{}, zk.WorldACL(zk.PermAll))if err ! nil {fmt.Println(CreateProtectedEphemeralSequential:, err.Error())return}// doworkfor {children, _, err : conn.Children(lockPath)if err ! nil {fmt.Println(Children:, err.Error())return}// 对子节点按照序列号进行排序sortChildren(children)// 检查自己创建的节点是否是第一个节点if lockNodePath lockPath/children[0] {// 获取到了锁fmt.Println(Acquired lock)break}// 监听前一个节点的删除事件exists, _, watch, err : conn.ExistsW(lockPath / children[0])if err ! nil {fmt.Println(ExistsW: , err.Error())break}if !exists {// 前一个节点已删除再次检查自己创建的节点是否是第一个节点children, _, err conn.Children(lockPath)if err ! nil {fmt.Println(Children: , err.Error())break}sortChildren(children)if lockNodePath lockPath/children[0] {// 获取到了锁fmt.Println(Acquired lock)break}}// 等待前一个节点的删除事件-watch}// 执行需要保护的代码fmt.Println(start-1)time.Sleep(3 * time.Second)fmt.Println(11111111)// 释放锁删除自己创建的节点err conn.Delete(lockNodePath, -1)if err ! nil {fmt.Println(Delete: , err.Error())return}fmt.Println(Released lock)}()go func() {conn, _, err : zk.Connect([]string{xx.xx.xx.xx:2181}, time.Second*5)if err ! nil {fmt.Println(Connect:, err.Error())return}defer conn.Close()lockPath : /locksObjlockName : lock// 创建锁的根节点_, err conn.Create(lockPath, []byte{}, int32(0), zk.WorldACL(zk.PermAll))if err ! nil err ! zk.ErrNodeExists {fmt.Println(Create:, err.Error())return}// 获取锁lockNodePath, err : conn.CreateProtectedEphemeralSequential(lockPath/lockName-, []byte{}, zk.WorldACL(zk.PermAll))if err ! nil {fmt.Println(CreateProtectedEphemeralSequential:, err.Error())return}for {children, _, err : conn.Children(lockPath)if err ! nil {fmt.Println(Children:, err.Error())return}// 对子节点按照序列号进行排序sortChildren(children)// 检查自己创建的节点是否是第一个节点if lockNodePath lockPath/children[0] {// 获取到了锁fmt.Println(Acquired lock)break}// 监听前一个节点的删除事件exists, _, watch, err : conn.ExistsW(lockPath / children[0])if err ! nil {fmt.Println(ExistsW: , err.Error())break}if !exists {// 前一个节点已删除再次检查自己创建的节点是否是第一个节点children, _, err conn.Children(lockPath)if err ! nil {fmt.Println(Children: , err.Error())break}sortChildren(children)if lockNodePath lockPath/children[0] {// 获取到了锁fmt.Println(Acquired lock)break}}// 等待前一个节点的删除事件-watch}// 执行需要保护的代码fmt.Println(start-2)time.Sleep(5 * time.Second)fmt.Println(22222222)// 释放锁删除自己创建的节点err conn.Delete(lockNodePath, -1)if err ! nil {fmt.Println(Delete: , err.Error())return}fmt.Println(Released lock)}()time.Sleep(10 * time.Second)
}
zookeeper 怎么实现 分布式锁的 zookeeper 会建立一个长链接监听锁对象节点的状态和事件 ETCD实现 分布式锁
简单实现
package mainimport (contextfmtclientv3 go.etcd.io/etcd/client/v3time
)func main() {go func() {config : clientv3.Config{Endpoints: []string{xx.xx.xx.xx:2379},DialTimeout: 5 * time.Second,}// 获取客户端连接client, err : clientv3.New(config)if err ! nil {fmt.Println(err)return}// 上锁// 用于申请租约lease : clientv3.NewLease(client)// 申请一个10s的租约leaseGrantResp, err : lease.Grant(context.TODO(), 10) //10sif err ! nil {fmt.Println(err)return}// 拿到租约的idleaseID : leaseGrantResp.IDctx, cancelFunc : context.WithCancel(context.TODO())// 停止defer cancelFunc()// 确保函数退出后租约会失效defer lease.Revoke(context.TODO(), leaseID)// 自动续租keepRespChan, err : lease.KeepAlive(ctx, leaseID)if err ! nil {fmt.Println(err)return}// 处理续租应答的协程go func() {select {case keepResp : -keepRespChan:if keepRespChan nil {fmt.Println(lease has expired)break} else {// 每秒会续租一次fmt.Println(收到自动续租应答, keepResp.ID)}}}()// if key 不存在then设置它else抢锁失败kv : clientv3.NewKV(client)// 创建事务txn : kv.Txn(context.TODO())// 如果key不存在txn.If(clientv3.Compare(clientv3.CreateRevision(/lockObj/lock/job), , 0)).Then(clientv3.OpPut(/lockObj/lock/job, , clientv3.WithLease(leaseID))).Else(clientv3.OpGet(/lockObj/lock/job)) //如果key存在// 提交事务txnResp, err : txn.Commit()if err ! nil {fmt.Println(err)return}// 判断是否抢到了锁if !txnResp.Succeeded {fmt.Println(锁被占用了, string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value))return}// 处理业务fmt.Println(work)time.Sleep(5 * time.Second)fmt.Println(END)}()time.Sleep(20 * time.Second)
}实现原理
etcd 支持以下功能正是依赖这些功能来实现分布式锁的
Lease机制即租约机制(TTL,Time To Live)etcd可以为存储的kv对设置租约当租约到期kv将失效删除同时也支持续约keepaliveRevision机制每个key带有一个Revision属性值etcd每进行一次事务对应的全局Revision值都会1因此每个key对应的Revision属性值都是全局唯一的。通过比较Revision的大小就可以知道进行写操作的顺序在实现分布式锁时多个程序同时抢锁根据Revision值大小依次获得锁避免“惊群效应”实现公平锁Prefix机制也称为目录机制可以根据前缀获得该目录下所有的key及其对应的属性值watch机制watch支持watch某个固定的key或者一个前缀目录当watch的key发生变化客户端将收到通知
执行流程
步骤 1: 准备
客户端连接 Etcd以 /lock/mylock 为前缀创建全局唯一的 key假设第一个客户端对应的 key/lock/mylock/UUID1第二个为 key/lock/mylock/UUID2客户端分别为自己的 key 创建租约 - Lease租约的长度根据业务耗时确定假设为 15s
步骤 2: 创建定时任务作为租约的“心跳”
当一个客户端持有锁期间其它客户端只能等待为了避免等待期间租约失效客户端需创建一个定时任务作为“心跳”进行续约。此外如果持有锁期间客户端崩溃心跳停止key 将因租约到期而被删除从而锁释放避免死锁。
步骤 3: 客户端将自己全局唯一的 key 写入 Etcd
进行 put 操作将步骤 1 中创建的 key 绑定租约写入 Etcd根据 Etcd 的 Revision 机制假设两个客户端 put 操作返回的 Revision 分别为 1、2客户端需记录 Revision 用以接下来判断自己是否获得锁。
步骤 4: 客户端判断是否获得锁
客户端以前缀 /lock/mylock 读取 keyValue 列表keyValue 中带有 key 对应的 Revision判断自己 key 的 Revision 是否为当前列表中最小的如果是则认为获得锁否则监听列表中前一个 Revision 比自己小的 key 的删除事件一旦监听到删除事件或者因租约失效而删除的事件则自己获得锁。
步骤 5: 执行业务
获得锁后操作共享资源执行业务代码。
步骤 6: 释放锁
完成业务流程后删除对应的key释放锁。
扩展
马丁·克莱普曼 对 分布式锁以及对redlock的看法
分布式锁的目的是确保在可能尝试执行同一工作的多个节点中只有一个节点实际执行该操作至少一次只有一个。
主要有两个功能
1- 效率使用锁可以避免不必要地重复相同的工作多执行一次也无妨只要最终正确就行
2- 正确性锁定可以防止并发进程互相干扰并扰乱系统状态。如果锁定失败并且两个节点同时处理同一数据则会导致文件损坏、数据丢失、永久不一致。 马丁认为锁在分布式系统使用会碰到以下三类问题
1- 网络延迟您可以保证数据包始终在某个保证的最大延迟内到达
2- GC问题 导致锁无法续期等等问题
3- 时钟飘移依赖于时钟的就容易出现问题 马丁认为redlock 强依赖于时钟节点之间时钟不对会使锁不可靠
假设系统有五个 Redis 节点A、B、C、D 和 E和两个客户端1 和 2。如果其中一个 Redis 节点上的时钟向前跳动会发生什么情况
客户端 1 获取节点 A、B、C 上的锁。由于网络问题无法访问 D 和 E。节点C上的时钟向前跳跃导致锁过期。客户端2获取节点C、D、E上的锁。由于网络问题无法访问A和B。客户 1 和 2 现在都相信他们持有锁。
如果 C 在将锁持久保存到磁盘之前崩溃并立即重新启动则可能会发生类似的问题。因此Redlock 文档建议延迟重新启动崩溃的节点至少要延迟最长寿命锁的生存时间。但这种重新启动延迟再次依赖于对时间的相当准确的测量并且如果时钟跳跃就会失败。 马丁提出了 fencing token 方案 客户端 1 获取租约并获得令牌 33但随后它进入长时间暂停状态并且租约到期。客户端 2 获取租约获取令牌 34数字始终增加然后将其写入发送到存储服务包括 34 的令牌。稍后客户端 1 恢复正常并将其写入发送到存储服务包括其令牌值 33。但是存储服务器记得它已经处理了具有更高令牌编号 (34) 的写入因此它拒绝具有令牌 33 的请求。
总结
分布式锁不是百分百安全我们要根据实际使用情况来考虑锁的使用解决效率问题还是正确行问题在使用分布式锁的时候我们需要考虑锁的续期锁归属集群数据一致性操作原子性GC时钟飘逸网络延迟等等的问题。在cap 理论里 redis保证了ap, zk和etcd保证cp ,所以实际使用中根据业务的情况选择redis/zk/etcd之一来实现分布式锁。