分布式锁在分布式系统经常都会使用到,但是很多同学并不完全了解分布式锁,下面重点详解分布式锁的原理与实现方式。
什么是分布式锁?
分布式锁,即分布式系统中的锁,是为了解决分布式系统共享资源访问的问题,在分布式系统加锁,这就是分布式锁。
为什么需要分布式锁?
首先在传统单机部署的情况下,在Java多线程并发竞争资源场景,可以使用ReentrantLcok 、Synchronized进行互斥控制,用于解决单机并发共享资源问题。
如下图所示:
但是在分布式系统后,由于分布式系统是分布在不同机器上,这将使原单机并发控制锁策略失效。
为了解决这个问题就需要一种跨JVM的互斥机制,来控制共享资源的访问,这就是分布式锁要解决的问题。
分布式锁的特点
首先为了确保分布式锁可用,我们至少要确保分布式锁的实现同时满足以下3个条件:
1.互斥性
在分布式系统环境下,一个方法在同一时间,只能被一个机器的一个线程执行。
任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
2.避免死锁
获取锁的客户端因为某些原因,花了太长时间处理,或者客户端发生了故障(比如:down机等),锁无法释放会导致整个处理流程无法进行下去,所以要避免死锁。
最常见的是通过设置一个 TTL,Time To Live,存活时间,来避免死锁。
3.容错特性
就是当部分节点,比如:redis节点等down机时,客户端仍然能够获取锁和释放锁,为避免单点故障,锁服务需要具有一定容错性。
比如:锁服务本身是一个集群(Redis集群),单点出现故障,可以通过集群自动故障切换来解决容错的问题。
分布式锁的实现方式
分布式锁的实现方式,常见有如下3种:
1.基于数据实现分布式锁
要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。
创建这样一张数据库表:
当我们想要锁住某个方法时,执行以下SQL:
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:
数据库实现分布式锁总结:
1.数据库实现分布式锁优点
优点是直接借助数据库,简单容易理解。
2.数据库实现分布式锁缺点
缺点是操作数据库需要一定的开销,性能问题需要考虑。
2.Redis分布式锁实现
既然数据库性能不够好,我们看一下用缓存中间件,也就是我们最经常使用的Redis,如果用来实现锁要怎么样做。
Redis分布式锁实现,主要包含如下3个步骤:
第一步:获取锁
可以通过Redis的Set语法来实现:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
下面我分别谈谈这几个参数:
Key:这个是作为锁的唯一标识,用于获取和释放锁;
Value:这个是作为使用者的唯一标识,用来表示当前持有锁的是具体哪个使用者,可以起到一个标记的作用;
EX second :设置键的过期时间为 second 秒;
PX millisecond :设置键的过期时间为 millisecond 毫秒;
NX :只在键不存在时,才对键进行设置操作;
XX :只在键已经存在时,才对键进行设置操作。
第二步:解锁
那如何释放锁呢,通常我们会使用引入Lua脚本,我们看一下下面这个语句块:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
那么这段Lua代码的功能是什么呢?
KEYS[1]:它代表的是获取锁时输入的key,也就是共享资源名称;
ARGV[1]:它代表的是获取锁时输入的value,这个value的唯一性决定了使用者只能删除自身已经获取的锁,不会误删除别人的;
当输入的KEYS[1]在Redis里面的值等于输入的AEGV[1]时,则删除这个原有的KEY,即代表释放锁操作,这里查不到返回0,也可能是因为锁已经过期了,前面我们获取锁的时候设置了过期时间。
3.ZooKeeper分布式锁实现
Zookeeper 分布式锁是:基于 临时顺序节点 来实现的,实现原理分为如下几大步骤:
- 锁可理解为 Zookeeper 上的一个节点,当需要获取锁时,就在这个锁节点下创建一个临时顺序节点;
- 当存在多个客户端同时来获取锁,就按顺序依次创建多个临时顺序节点,但只有排列序号是第一的那个节点能获取锁成功;
- 其他节点则按顺序分别监听前一个节点的变化,当被监听者释放锁时,监听者就可以马上获得锁;
- 而且用临时顺序节点的另外一个用意是如果某个客户端创建临时顺序节点后,自己意外宕机了也没关系,Zookeeper 感知到某个客户端宕机后会自动删除对应的临时顺序节点,相当于自动释放锁。
比如上图,获取锁分为如下:
1.ClientA 和 ClientB 同时想获取锁,所以都在 locks 节点下创建了一个临时节点 1 和 2;
2.而 1 是当前 locks 节点下排列序号第一的节点,所以 ClientA 获取锁成功;
3.而 ClientB 处于等待状态,这时 Zookeeper 中的 2 节点会监听 1 节点;
4.当 1节点锁释放,也就是节点被删除时,2 就变成了 locks 节点下排列序号第一的节点,这样 ClientB 就获取锁成功了。
最后总结下Zookeeper 分布式锁:
1.Zookeeper分布式锁的优点
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题,实现起来较为简单。
2.Zookeeper分布式锁的缺点
- 性能上不如使用Redis分布式锁缓存实现,性能上没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。
- Zookeeper的并发安全问题,因为可能存在网络抖动,客户端和Zookeeper集群的session连接断了,Zookeeper集群以为客户端挂了就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。
以上我主要讲解了:为什么需要分布式锁,以及分布式锁需要具备的条件,以及分布式锁的实现方式,希望对你掌握分布式锁有所帮助!
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》