分布式锁的实现方式

2021-03-04  本文已影响0人  因你而在_caiyq

原创文章,转载请注明原文章地址,谢谢!

分布式锁实现方式

基于数据库实现分布式锁

基于数据库实现分布式锁,主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,就判定当前竞争者加锁失败。防重业务id需要自己来定义,例如锁对象是一个方法,则业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。

表设计
CREATE TABLE `distributed_lock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `unique_mutex` varchar(255) NOT NULL COMMENT '业务防重id',
  `holder_id` varchar(255) NOT NULL COMMENT '锁持有者id',
  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `mutex_index` (`unique_mutex`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

id字段是数据库的自增id,unique_mutex字段就是防重id,也就是加锁的对象,此对象唯一。在这张表上加了一个唯一索引,保证unique_mutex唯一性。holder_id代表竞争到锁的持有者id。

加锁
insert into distributed_lock(unique_mutex, holder_id) values ('unique_mutex', 'holder_id');

如果当前sql执行成功代表加锁成功,如果抛出唯一索引异常(DuplicatedKeyException)则代表加锁失败,当前锁已经被其他竞争者获取。

解锁
delete from methodLock where unique_mutex='unique_mutex' and holder_id='holder_id';
分析

基于缓存实现分布式锁

基于缓存实现分布式锁,理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存操作,而基于数据库的方案和基于Zookeeper的方案都会涉及到磁盘文件IO,效率相对低下。一般使用Redis来实现分布式锁都是利用Redis的setnx key value这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败。

加锁
public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 加锁
     * @param jedis Redis客户端
     * @param lockKey 锁的key
     * @param requestId 竞争者id
     * @param expireTime 锁超时时间,超时之后锁自动释放
     * @return 
     */
    public static boolean getDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        //加锁的代码
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        return "OK".equals(result);
    }
}

上述加锁的代码,五个参数。第一个参数是key,用key来当做锁,因为key是唯一的。第二个参数是value,锁的竞争者id,在解锁时,需要判断当前解锁的竞争者id是否为锁持有者。第三个参数是nx,当key不存在时,进行set操作,如果key已经存在,则不作任何操作。第四个参数是px,给key加一个过期时间。第五个参数是time,代表key的过期时间。总结,当执行set方法时,会出现2种结果:第一种是当前没有锁,即key不存在,那么就进行加锁操作,并对锁设置一个有效期,同时value表示加锁的客户端。第二种是锁已经存在,不做任何操作。

解锁
public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 锁持有者id
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        return RELEASE_SUCCESS.equals(result);
    }
}

注意到这里解锁其实是分为2个步骤,涉及到解锁操作的一个原子性操作问题。这也是为什么解锁的时候用Lua脚本来实现,因为Lua脚本可以保证操作的原子性。那么这里为什么需要保证这两个步骤的操作是原子操作呢?设想:假设当前锁的持有者是竞争者1,竞争者1来解锁,成功执行第1步,判断自己就是锁持有者,这时还未执行第2步。这时锁过期了,然后竞争者2对这个key进行了加锁。加锁完成后,竞争者1又来执行第2步,此时错误产生了,竞争者1解锁了不属于自己持有的锁。可能会有人问为什么竞争者1执行完第1步之后突然停止了呢?这个问题其实很好回答,例如竞争者1所在的JVM发生了GC停顿,导致竞争者1的线程停顿。这样的情况发生的概率很低,但是请记住即使只有万分之一的概率,在线上环境中完全可能发生。因此必须保证这两个步骤的操作是原子操作。

分析

基于Zookeeper实现分布式锁

基于Zookeeper实现分布式锁,Zookeeper一般用作配置中心,其实现分布式锁的原理和Redis类似,在Zookeeper中创建瞬时节点,利用节点不能重复创建的特性来保证排他性。

加锁和解锁
分析
利用curator实现

Zookeeper第三方客户端curator中已经实现了基于Zookeeper的分布式锁。

//加锁,支持超时,可重入
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    try {
        return interProcessMutex.acquire(timeout, unit);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
//解锁
public boolean unlock() {
    try {
        interProcessMutex.release();
    } catch (Throwable e) {
        log.error(e.getMessage(), e);
    } finally {
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
    }
    return true;
}

博客内容仅供自已学习以及学习过程的记录,如有侵权,请联系我删除,谢谢!

上一篇 下一篇

猜你喜欢

热点阅读