前言
在多级部署的情况下,很容易出现多机竞争问题,我最近做的项目中就有遇到过,我详细的研究了一下市场上常用的一些分布式锁,在这里分享一下每种分布式锁的原理
1、分布式锁定义
其实在项目开发中,我们经常涉及到锁的概念,就是当不同线程或进程对同一个资源进行访问时,就需要使用锁
来保证并发安全问题,保护我们的临界资源。
在 Java 的各个线程之间,我们使用Synchronized
来保证并发安全,Synchronized
只能应用于单个进程里边的锁,不同进程中的线程无法使用单机锁老保证并发安全。
随着网络服务的进一步增加,很多场景都需要我们从单机环境提升到分布式环境,所以我们就需要使用分布式锁
来保证分布式环境的并发安全问题,因为同一进程之间能够共享进程的数据,所以可以实现互斥锁,但是不同进程之间就需要公共存储组件来帮助实现分布式情况下的锁。
2、分布式锁的核心特点
分布式锁需要具备以下几个特点:
- 互斥性:同一个时刻只能有一个人占据锁
- 健壮性:即使在锁的一方没有释放锁成功的情况下,也要保证锁能够正确的传递给其他用户,不能出现死锁的情况
- 对称性:加锁和解锁必须是同一个人,不允许释放掉其他人的锁
- 高可用性:能够有一定的容灾能力
3、分布式锁的作用
当多台服务器对临界资源进行访问的时候,就需要使用公共存储组件来控制访问顺序
,使用公共组件来模拟锁的功能,保证各个进程的并发安全,防止类似于超卖问题的发生,这就是分布式锁的作用。
4、分布式锁的实现方式
目前主流的方法有主动轮询型(MySQL 和 Redis)以及监听回调型(Zookeeper)
MySQL 实现分布式锁
实现原理:
在数据库中创建一个锁信息的表,并为锁名字字段设置一个唯一索引,当有进程进行加锁的时候,就是在该表中插入一条记录,释放锁的时候就是删除对应的记录。
性能分析:
MySQL 来实现分布式锁不要引入第三方组件,实现起来比较简单,但是很容易造成死锁的情况,并且MySQL操作的是磁盘,在高并发场景下读写都十分缓慢。
因为 MySQL不能像Redis一样设置过期时间,所以当持有锁的进程挂掉了以后,没有办法进行锁释放,其他进程没法获取到当前的锁,很容易导致死锁的情况。如果通过引入定时器来检查锁过期,很容易导致持有锁的进程在未执行完任务的情况下就将锁释放的问题。
所以基于关系型数据库
实现分布式锁的方式在性能上有缺陷,更多的是使用内存存储的组件
来实现分布式锁
Redis实现分布式锁
Redis是基于内存存储的组件,操作起来轻便快捷,而且支持原子指令,用来实现分布式锁非常的合适。
实现原理: Redis为每一个锁设置了一个唯一标识,加锁操作就是加入一条数据,释放锁操作就是删除一条数据,然后在加锁前会检查是否数据存在,如果存在的话就是轮询,直到持有锁的进程释放锁以后,才能加锁成功。
1、加锁字段:通过使用如下命令来保证两个操作的原子性
redis
SET lock_key unique_value NX PX 10000
lock_key: 每个锁的标识
unique_value 是客户端生成的唯一标识
NX 代表只在lock_key不存在时才会对lock_key进行操作
PX 10000表示设置过期时间为10s,避免客户端异常导致无法释放锁资源
通过设置过期时间来保证健壮性
2、在上锁期间,通过设置看门狗
机制来保证不会把锁提前释放。看门狗就是额外开启一个线程,定期对Redis进行续期操作,虽然会消耗一定的资源,但是可以防止客户端在没有完成操作的情况下,把锁提前释放点,保证了操作的安全性。
3、释放锁过程:通过lua脚本来实现操作的原子性,防止锁的错误释放问题。
当获取锁的进程宕机时,其他进程会抢占该锁资源,当原进程恢复服务以后,会释放锁,如果在释放锁时没有对锁进行检验,很有可能释放掉其他进程的锁,所以在释放锁的时候需要先进行检查,如果不是自己的锁,就不会进行释放,防止误删别人的锁
。这里就需要使用lua脚本来实现事务。
但是 Redis 实现分布式锁时也容易产生单点故障,无法保证锁的高可用性,虽然主从版的Redis服务可以改善这种情况,但是由于主从延迟,主从节点的数据有可能不一致,导致锁的信息丢失所以需要使用其他的方案。
多机部署解决分布式锁不一致
RedLock算法:基于多个独立的 Redis Master节点来保证分布式锁的安全问题。通过让客户端同时向多个独立的 Redis 实例请求加锁,当有半数的实例统一完成加锁操作,我们就认为客户端加锁成功,否则就是加锁失败。
通过使用红锁可以避免主从复制时产生的一些问题,但是需要依赖系统时间,当时钟发生跳跃的时候,也可能会导致一些安全上的问题。
Zookeeper实现分布式锁
Zookeeper 分布式锁就是监听回调型的分布式锁,Zookeeper 会维护临时顺序节点,当前节点通过监听上一个节点释放锁的动作,来决定上锁时间。
Zookeeper 的实现过程:Zookeeper会创建一个节点,当每一个客户端或者线程来获取锁的时候,会在该节点下面创建一个临时顺序节点(按照请求顺序依次创建,先请求上锁的节点序号在前面),当前顺序节点会会检查是否是这些临时顺序节点下的序号最小的节点,如果是的话,就加锁成功,如果不是,就会在当前节点给上一个节点安装一个监听器,监听上一个节点的释放操作。当上一个节点释放了以后,Zookeeper会通知当前节点的监听器,当前客户端会重新尝试去获取锁,完成接下来的操作。
注意: 如果当客户端创建临时节点以后宕机了,Zookeeper 会感知到客户端宕机,会自动删除对应的临时顺序节点(释放锁或者自动取消排队)。
5、分布式锁的选择
根据性能进行排序:
Redis > Zookeeper > MySQL
根据可靠性进行排序
Zookeeper > Redis > MySQL
在追求高性能的情况下,使用Redis分布式锁,如果需要增加安全性,可以考虑使用RedLock,如果对安全性要求很高,可以牺牲一点性能,可以选用Zookeeper分布式锁。
在我的项目中,对安全性要求没有很高,更多的是对性能的考虑,所以使用的是Redis,在多数情况下也已经够用了。