「如何设计」:redis
分布式环境下,锁定全局惟一公共资源 表现为:bash
第一步是上锁的资源目标,是锁定全局惟一公共资源,只有是全局惟一的资源才存在多个线程或服务竞争的状况,互斥性表现为一个资源的隔离级别串行化,若是对照单机事务ACID的隔离型来讲,互斥性的事务隔离级别是SERLALIZABLE,属于最高的隔离级别。restful
(事务隔离级别:DEFAULT,READ_UNCOMMITTED,READ_COMMITED,REPEATABLE_READ,SERLALIZABLE)网络
例子:分布式
1. 防止用户重复下单 共享资源进行上锁的对象 : 【用户id】
2. 订单生成后发送MQ给消费者进行积分的添加 寻找上锁的对象 :【订单id】
3. 用户已经建立订单,准备对订单进行支付,同时商家在对这个订单进行改价 寻找上锁对象 : 【订单id】
复制代码
redis单线程串行处理自然就是解决串行化问题,用来解决分布式锁是再适合不过。微服务
实现方式:
setnx key value Expire_time
获取到锁 返回 1 , 获取失败 返回 0post
存在问题:性能
redis 只能在setnx指定一个锁的超时时间,假设初始设定锁的时间是10秒钟,可是业务获取到锁跑了20秒钟,在10秒钟以后,若是又有一个业务能够获取到相同的一把锁,这个时候可能就存在两个相同的业务都获取获得锁的问题,而且两个业务处在并行阶段。也就是第一个获取锁的业务没法对自身的锁进行续租。spa
redis 的client与server端并无维持心跳的机制,若是在链接出现问题,client会获得一个超时的回馈。
redis的集群实际上在CAP模式中是处在与AP的模型,保证可用性。在主从复制中“主”有数据,但可能“从”尚未数据,这个时候,一旦主挂掉或者网络抖动等各类缘由,可能会切换到“从”节点,这个时候有可能会致使两个业务线程同时的获取到两把锁。
上述的问题其实并非redis的缺陷,只是redis采用了AP模型,它自己没法确保咱们对一致性的要求。redis官方推荐redlock算法来保证,问题是redlock至少须要三个redis主从实例来实现,维护成本比较高,至关于redlock使用三个redis集群实现了本身的另外一套一致性算法,比较繁琐,在业界也使用得比较少。
能不能使用redis做为分布式锁,这个自己就不是redis的问题,仍是取决于业务场景,咱们先要本身确认咱们的场景是适合 AP 仍是 CP , 若是在社交发帖等场景下,咱们并无很是强的事务一致性问题,redis提供给咱们高性能的AP模型是很是适合的,但若是是交易类型,对数据一致性很是敏感的场景,咱们可能要寻在一种更加适合的 CP 模型
redis可能做为高可用的分布式锁并不合适,咱们须要确立高可用分布式锁的设计目标
- | redis | zookeeper | etcd |
---|---|---|---|
一致性算法 | 无 | zab | raft |
CAP | AP | CP | CP |
高可用 | 主从 | N+1 | N+1 |
实现 | setnx | create临时有序节点 | restful |
刚刚也分析过,redis其实没法确保数据的一致性,先来看zookeeper是否合适做为咱们须要的分布式锁,首先zk的模式是CP模型,也就是说,当zk锁提供给咱们进行访问的时候,在zk集群中能确保这把锁在zk的每个节点都存在。
(这个其实是zk的leader经过二阶段提交写请求来保证的,这个也是zk的集群规模大了的一个瓶颈点)
说zk的锁问题以前先看看zookeeper中几个特性,这几个特性构建了zk的一把分布式锁 特性:
有序节点
当在一个父目录下如 /lock 下建立 有序节点,节点会按照严格的前后顺序建立出自节点 lock000001,lock000002,lock0000003,以此类推,有序节点能严格保证各个自节点按照排序命名生成。
临时节点
客户端创建了一个临时节点,在客户端的会话结束或会话超时,zookepper会自动删除该解ID那。
事件监听
在读取数据时,咱们能够对节点设置监听,当节点的数据发生变化(1 节点建立 2 节点删除 3 节点数据变成 4 自节点变成)时,zookeeper会通知客户端。
结合这几个特色,来看下zk是怎么组合分布式锁。
zk官方提供的客户端并不支持分布式锁的直接实现,咱们须要本身写代码去利用zk的这几个特性去进行实现。
客户端建立了临时有序节点并创建了事件监听,就可让业务线程与zk维持心跳,这个心跳也就是这把锁的租期。当客户端的业务线程完成了执行就把节点进行删除,也就释放了这把锁,不过中间也可能存在问题
客户端挂掉
由于注册的是临时节点,客户端挂掉,zk会进行感知,也就会把这个临时节点删除,锁也就随着释放
业务线程假死
业务线程并无消息,而是一个假死状态,(例如死循环,死锁,超长gc),这个时候锁会被一直霸占不能释放,这个问题须要从两个方面进行解决。
第一个是自己业务代码的问题,为什么会出现死循环,死锁等问题。
第二个是对锁的异常监控问题,这个其实也是微服务治理的一个方面。
刚刚说了zk锁的维持是靠zk和客户端的心跳进行维持,若是客户端出现了长时间的GC会出现什么情况
etcd实现分布式锁比zk要简单不少,就是使用key value的方式进行写入,在集群中,若是存在key的话就不能写入,也就意味着不能获取到锁,若是集群中,能够写入key,就意味着获取获得锁。
etc到使用了raft保证了集群的一致性,也就是在外界看来,只要etcd集群中某一台机器存在了锁,全部的机器也就存在了锁,这个跟zk同样属于强一致性,而且数据是能够进行持久化,默认数据一更新就持久化。
etcd 并不存在一个心跳的机制,因此跟redis同样获取锁的时候就要对其进行expire的指定,这个时候就存在一个锁的租期问题。
租期问题有几种思路能够去解决,这里讨论其中一种:
在获取到锁的业务线程,能够开启一个子线程去维护和轮训这把锁的有效时间,并定时的对这把锁进行续租
假设业务线程获取到一把锁,锁的expire时间为10s,业务线程会开启一个子线程经过轮训的方式每2秒钟去把这把锁进行续租,每次都将锁的expire还原到10s,当业务线程执行完业务时,会把这把锁进行删除,事件完毕。
这种思路同样会存在问题:
首先得了解清楚咱们使用分布式锁的场景,为什么使用分布式锁,用它来帮咱们解决什么问题,先聊场景后聊分布式锁的技术选型。
不管是redis,zk,etcd其实在各个场景下或多或少都存在一些问题,例如redis的AP模型会限制不少使用场景,但它却拥有了几者中最高的性能,zookeeper的分布式锁要比redis可靠不少,但他繁琐的实现机制致使了它的性能不如redis,并且zk会随着集群的扩大而性能更加降低。etcd 看似是一种折中的方案,不过像锁的租期续约都要本身去实现。
简单来讲,先了解业务场景,后进行技术选型。