分布式锁的实现详情
做惯了讲究响应速度的微小化web服务,当有人给我讲分布式锁时深刻怀疑说这个名词的哥们要么准备给我挖坑,要么自己把架构玩脱了已经掉进了坑里。这个东西虽然常见,但是稍有不慎就会掉坑里出不来。
系统做的越多现在越来越害怕那种千钧一发的系统,动辄每秒单例服务响应web业务请求百万上下,这样的实现功力确实佩服,就是不知道服务宕机时的瞬时数据在恢复时能不能达到同样的速度了。从概率上来讲,服务器越多,发生问题的概率越小,再加上自己常人天资,最希望的就是一台服务器只处理一个请求-------当然这种开发富贵病也得治,不过那种炫技式的服务设计架构,除非是穷人家需要榨干服务器所有的算力,否则还是花点钱,多买几台服务器均摊开的好。可以照着秒发百万设计,但是最好贴着每秒负载上千的去玩。
扯远了。使用分布式锁就为了保证服务间数据一致性,那么这个锁怎么进行分布式?
一
把锁设置在各个服务点上就扯淡了,如果不讲究响应时间并且不担心系统复杂度,可以这么玩。首先在收到服务请求时发给所有同服务点发出广播,要求占用资源点,要求其他服务点暂停对这个资源点的请求。如果资源数据占用粒度比较小,那么还好,其他服务点可以采用队列堆积对着被占用的资源数据其中锁定的那一部分的请求,比如某个指定账户的积分。如果没有粒度区分,那就悲剧了,需要把所有资源的请求都堆积起来。请求完成,再发一次通知,并且还需要从堆积的请求里按照一定规则挑出新的请求服务点以避免死锁。如果不幸占用资源的服务挂掉了,还要再加一层根据时间条件的自旋锁机制,估计这样实现的话工程师都要哭了。所以,就别扯淡了。
二
把锁设置在数据资源服务本身上。比如直接利用关系型数据库的原子性事务或者redis自身的锁机制,这种是常见的利用方式,关系型数据库的响应速度可能会慢点,redis的响应速度相对好些。不过这种锁,尤其是关系型数据库有锁粒度过大的问题,通常是表级别锁,对并发性影响大,不适用于那种分分钟百万上下的。redis提供了一个分布式锁的算法https://redis.io/topics/distlock。
三
请求队列。将所有请求都放置到 一个队列中先进先出,串行。这个方法虽然省事,但是不适用高响应系统大数据量系统,不然就是千钧一发,系统一旦宕机,恢复数据时估计想打人。当然,为了提高响应速度可以依据一定规则对资源数据进行划分,将请求队列切分为多个,但是需要保证队列间不能有请求资源交叉。
四
2和3的方式相结合,资源切分,请求归队,队列再次切分,利用redis作为资源存储,启用redis的锁算法,并且redis按照服务器进行数据存储划分而不是在一台服务器进行分片。2的架构在请求数未突破redis的瓶颈时还好,一旦突破则性能会陡峭下降。所以此方案单次响应时间相比较2会长一些,但是支持的吞吐量不会完全受限于redis的性能瓶颈并且可以通过队列服务增加来牺牲部分响应速度来降低性能下降速度,为redis划分负荷增加处理时间。当然,架构的层级划分也挺吓人,价格也挺好看的................
五
1到4都是可切分的,但是有些数据是不太容易切分的。容许请求失败的比如秒杀类的业务可以做切分,在子资源点数据不足时直接拒绝请求就可以了。但是面向成千上万用户发送积分并且积分有所预算的就不成了。当然,这个也可以切分,但是有后遗症,如果被请求的子节点数据不足,那么这个请求是不能认为失败的,需要将请求路由到资源充足的节点上,那么就需要在队列请求分发前进行一次预占用处理,保证请求真正发出时目标资源点的数据是能够满足的,如果不能还需要进行查找再进行路由。还有一种比较坑的情况,就是各个子节点的资源都不足,但是总量是满足,那么还需要继续再添加一层数据占用处理保障,对于突破子节点资源量的请求进行一套单独的处理,并且还需要选择预占用的节点进行预占用处理。
总结:
伴随着一种种情况,似乎CAP原则大显神威,但是情况没有绝对,从企业的角度出发,好的用户体验是一方面,但是数据的正确性则更加重要一些。可以通过对业务进行细致分析尝试将单次请求的资源需要量进行一些限制,反常则是妖,大多数的时候用户的需求还是常见,能够被系统一种较小的代价去实现出来。特定用户高频并且对资源大量请求的情况大多数来自与爬虫或者机器人,人类的信息单位处理能力还是有限。当然,一些特定领域的信息处理则要另当别论。