分布式锁探讨
一、分布式锁背景
a、什么是锁?
从使用场景定义:当存在多个线程可以同时改变某个变量时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。
锁的实现方式有多种,只要能满足所有线程都能看得到这个锁标记即可。
java中常见的锁:
synchronized
reentrantlock
reentrantreadwritelock
b、什么是分布式?
定义:分布式系统一定是由多个节点(计算机服务器)组成的系统,这些互通的节点上部署了各个服务,并且操作需要协同。
分布式系统对于终端用户而言,他们面对的就好像是一个服务器,提供用户需要的服务而已。
什么是cap?
定义:任何一个分布式系统都无法同时满足一致性consistency、可用性availability、和分区容错性partition tolerance,最多只能同时满足两项。
因为单个服务器偶尔宕机不可避免(或者因为停电或者自然灾害导致单机房不可用),网络状况不稳定,时而会有网络抖动,时而延时比较高,所以必须满足p,所以一般会出现ap系统和cp系统。
c、a+b==》什么是分布式锁?
定义:在分布式环境下,一个共享的可见的公共资源,各个线程通过对这个公共资源的抢占,能够使得一个代码块在同一时间只能被一个机器的一个线程执行,那这个公共资源就是分布式锁,或者说这整个机制就是分布式锁。
或者从使用场景定义:分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性
二、分布式锁实现方式
锁的实现方式有多种,只要能满足所有线程都能看得到这个锁标记即可。
常见的方式是使用数据库、缓存或者zookeeper来实现分布式锁,除了这些,其实一个网络中的共享可见的可读写的资源就可以用作实现锁。
锁的操作主要有两个,即lock和unlock。
a、数据库
我们可以利用数据库的特性,lock即插入一条数据到数据库,unlock即删除该条数据。直接看图
1、建mysql表,lock_name上建立唯一性索引;
2、server1开始获取分布式锁"aaa";
3、server1调用lock("aaa"),数据库中没有"aaa",插入"aaa"成功,server1成功获得分布式锁"aaa";
4、server2调用lock("aaa"),插入"aaa"失败,获取锁失败;
5、server1调用unlock("aaa"),删除数据行"aaa";
6、server2调用lock("aaa"),插入"aaa"成功,获取锁成功;
熟悉java锁的同学发现问题了,这个锁也太简陋了,锁都不阻塞的。
好,我们严格地来讨论下锁的特性,
首先,我们罗列下锁的特性,我能想到的有以下这些,欢迎留言补充:
分布式锁的需求
1、在分布式环境下,一个代码块在同一时间只能被一个机器的一个线程执行
2、高可用地获取锁和释放锁
3、高性能的获取锁和释放锁
4、具备锁失效机制,防止死锁
5、是否可重入
6、是否非阻塞
7、自旋锁
8、乐观锁/悲观锁
9、是否公平
10、独享锁/共享锁
11、分段锁
刚才提到的问题,锁最基本的应该具有阻塞特性,我们修正一下,如下图
在插入数据失败后,我们通过不断地重试来达到阻塞的特性。
下面我们来逐条讨论下以上提到的特性:
分布式锁的需求
1、在分布式环境下,一个代码块在同一时间只能被一个机器的一个线程执行
基本的,所有的锁都应该满足
2、高可用地获取锁和释放锁
可用性受数据库单点限制,如果要提高可用性,使用主备库,通过数据同步主备切换达到在主库宕机时的高可用
3、高性能的获取锁和释放锁 数据库读写性能
锁的获取和释放就是数据库的插入和删除,性能因具体数据库不同而不同,表的行数的规模大小也影响着性能,性能还受网络的影响,在笔者的网络中插入大概耗时约80ms
4、具备锁失效机制,防止死锁
如果节点在获取锁之后释放锁之前宕机,会发生死锁。可以在表中加入时间字段,用一个定时任务,定期地清理时间过期的锁,但是这个过期时间的大小值得探讨,很深入地探讨。
5、是否可重入
如果想获得类似reentrantlock的特性,可以在表中写入线程的信息(ip地址+进程号+线程号),可以先查询,如果是线程自己拥有的锁,直接返回成功并计数count
6、是否非阻塞
如果要获得阻塞特性,通过不断自旋重试
7、自旋锁
同上
8、乐观锁/悲观锁
分布式锁就是悲观锁。如果不想用分布式锁,对数据不上锁,在表中加入列version,通过cas版本号来控制并发写
9、是否公平
首先解释下公平锁:公平锁即是根据lock()的时间顺序,先到先得,即fifo,这个是常见的规则。
单机情况下非公平锁的好处:公平锁忽略了线程之间切换以及线程内核态和用户态转换的耗时,其实可以利用线程切换的间隙,让其它正在被调度执行的线程插队去获取锁,然后释放锁,还给等待线程。这样不耽误事的前提下,提高了锁的性能。请自行去了解下非公平锁。
言归正传,如果需要实现公平锁,设计一张排队表,让lock()的线程首先在排队表中排队,当发现自身排在队头才有资格去真正获取锁。当然引入复杂性,必然会导致其它问题,兵来将挡,水来土掩。
10、独享锁/共享锁
以上的分布式锁,是定义的一把独享锁(独占锁、互斥锁、排它锁)。
reentrantlock就是一种排它锁。countdownlatch是一种共享锁。这两类都是单纯的一类,即,要么是排它锁,要么是共享锁。
reentrantreadwritelock是同时包含排它锁和共享锁特性的一种锁,这里主要以reentrantreadwritelock为例来进行分析学习。
如果要实现共享锁,锁类型+排队表
e、结论
分布式锁可以根据业务需要,量身定制。
三、存在的问题