分布式锁
- 为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。
- 由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题.
分布式锁应该具备哪些条件
在分析分布式锁的实现方式之前,先了解一下分布式锁应该具备哪些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁三种实现方式
1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁。
一 基于数据库实现分布式锁
- 悲观锁
利用select … where … for update 排他锁
注意: 其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。
- 乐观锁
乐观锁基于CAS 思想,不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。
通过增加递增的版本号字段实现乐观锁:
常见应用:shedLock+mysql
shedLock支持注解,和spring结合使用更简单。
- 建表
CREATE TABLE shedlock(
name VARCHAR(64),
lock_until TIMESTAMP(3) NULL,
locked_at TIMESTAMP(3) NULL,
locked_by VARCHAR(255),
PRIMARY KEY (name)
)
name是全局唯一的,用这个来标识全局唯一的定时任务。用此来变相实现一个悲观锁。
@Component public class RiskAdminScheduler {
private static final String SIXTY_MIN = "PT60M";
private static final String THREE_MIN = "PT3M";
@Scheduled(cron = "0 */1 * * * ? ")
@SchedulerLock(name = "test",lockAtLeastForString = THREE_MIN,lockAtMostForString = SIXTY_MIN)
public void test() {
System.out.println(LocalDateTime.now());
System.out.println(Thread.currentThread().getName()+"--executed......"); }
}
@Scheduler(cron=xxxx) 这个是spring的定时任务触发器。每分钟跑一次。
@SchedulerLock这个是shedlock的注解方式。
lockAtLeastForString & lockAtMostForString我想引用官方的解释更明白一点:By setting lockAtMostFor we make sure that the lock is released even if the node dies and by setting lockAtLeastFor we make sure it's not executed more than once in fifteen minutes.
意思就是,当节点挂掉后,这个锁还是要释放的。最长时间就是most设置的。
least时间设置了后,必须等到2分钟结束,锁释放后,才能再次执行。
二 基于缓存(Redis等)实现分布式锁
使用命令
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key
实现思想
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
简单实现代码
/**
* 分布式锁的简单实现代码
*/
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock ( JedisPool jedisPool ) {
this.jedisPool = jedisPool;
}
/**
* 加锁
* @param lockName 锁的key
* @param acquireTimeout 获取超时时间
* @param timeout 锁的超时时间
* @return 锁标识
*/
public String lockWithTimeout ( String lockName, long acquireTimeout, long timeout ) {
Jedis conn = null;
String retIdentifier = null;
try {
// 获取连接
conn = jedisPool.getResource () ;
// 随机生成一个value
String identifier = UUID.randomUUID () .toString () ;
// 锁名,即key值
String lockKey = "lock:" + lockName;
// 超时时间,上锁后超过此时间则自动释放锁
int lockExpire = ( int ) ( timeout/1000 ) ;
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
while ( System.currentTimeMillis() < end ) {
if ( conn.setnx ( lockKey, identifier ) == 1 ) {
conn.expire ( lockKey, lockExpire ) ;
// 返回value值,用于释放锁时间确认
retIdentifier = identifier;
return retIdentifier;
}
// 返回-代表key没有设置超时时间,为key设置一个超时时间
if ( conn.ttl ( lockKey ) == -1) {
conn.expire ( lockKey, lockExpire ) ;
}
try {
Thread.sleep (10);
} catch ( InterruptedException e ) {
Thread.currentThread() .interrupt () ;
}
}
} catch ( JedisException e ) {
e.printStackTrace () ;
} finally {
if ( conn != null ) {
conn.close () ;
}
}
return retIdentifier;
}
/**
* 释放锁
* @param lockName 锁的key
* @param identifier 释放锁的标识
* @return
*/
public boolean releaseLock ( String lockName, String identifier ) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource () ;
while ( true ) {
// 监视lock,准备开始事务
conn.watch ( lockKey ) ;
// 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
if ( identifier.equals ( conn.get ( lockKey ))) {
Transaction transaction = conn.multi () ;
transaction.del ( lockKey ) ;
List < Object > results = transaction.exec () ;
if ( results == null ) {
continue;
}
retFlag = true;
}
conn.unwatch () ;
break;
}
} catch ( JedisException e ) {
e.printStackTrace () ;
} finally {
if ( conn != null ) {
conn.close () ;
}
}
return retFlag;
}
}
三 基于 Zookeeper 实现分布式锁
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。
原理
(一) ZooKeeper的每一个节点,都是一个天然的顺序发号器。
在每一个节点下面创建临时顺序节点(EPHEMERAL_SEQUENTIAL)类型,新的子节点后面,会加上一个次序编号,而这个生成的次序编号,是上一个生成的次序编号加一。
(二) ZooKeeper节点的递增有序性,可以确保锁的公平
一个ZooKeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程,都在这个节点下创建个临时顺序节点。由于ZK节点,是按照创建的次序,依次递增的。
为了确保公平,可以简单的规定:编号最小的那个节点,表示获得了锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。
(三)ZooKeeper的节点监听机制,可以保障占有锁的传递有序而且高效
每个线程抢占锁之前,先尝试创建自己的ZNode。释放锁的时候,需要删除创建的Znode。创建成功后,如果不是排号最小的节点,就处于等待通知的状态。需要等前一个节点的通知。
ZooKeeper的节点监听机制,能够非常完美地实现这种击鼓传花似的信息传递。具体的方法是,每一个等通知的Znode节点,只需要监听(linsten)或者监视(watch)排号在自己前面那个,而且紧挨在自己前面的那个节点,就能收到其删除事件了。
ZooKeeper内部优越的机制,能保证由于网络异常或者其他原因,集群中占用锁的客户端失联时,锁能够被有效释放。一旦占用Znode锁的客户端与ZooKeeper集群服务器失去联系,这个临时Znode也将自动删除。排在它后面的那个节点,也能收到删除事件,从而获得锁。正是由于这个原因,在创建取号节点的时候,尽量创建临时znode节点。
(四)ZooKeeper的节点监听机制,能避免羊群效应
ZooKeeper这种首尾相接,后面监听前面的方式,可以避免羊群效应。所谓羊群效应就是一个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来巨大压力,所以有了临时顺序节点,当一个节点挂掉,只有它后面的那一个节点才做出反应。
实现
基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
对比
数据库分布式锁实现
优点:好理解
缺点:
1.db操作性能较差,并且有锁表的风险
2.非阻塞操作失败后,需要轮询,占用cpu资源;
3.长时间不commit或者长时间轮询,可能会占用较多连接资源
Redis(缓存)分布式锁实现
优点:性能好
缺点:
1.锁删除失败 过期时间不好控制
2.非阻塞,操作失败后,需要轮询,占用cpu资源;
基于Redis的分布式锁,适用于并发 量很大、性能要求很高的、而可靠性问题可以通过其他方案去弥补的场景。
ZK 分布式锁实现
优点:好实现,可靠性强
缺点:性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。
在高性能,高并发的场景下,不建议使用ZooKeeper的分布式锁。而由于ZooKeeper的高可用特性,所以在并发量不是太高的场景,推荐使用ZooKeeper的分布式锁。
综合比较
总之:ZooKeeper有较好的性能和可靠性。
从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库
参考资料
- 分布式锁的三种实现方式
- zookeeper ZkClient API 集成
- ZK分布式锁的JAVA实现
- 分布式锁原理与实战 www.cnblogs.com/crazymakerc…
- ShedLock锁 www.jianshu.com/p/03ef62079…
- 分布式锁简单入门 blog.csdn.net/weixin_4380…