美文网首页
分布式锁实现

分布式锁实现

作者: 会飞的蜗牛F | 来源:发表于2020-08-18 19:42 被阅读0次

基于数据库实现分布式锁
基于缓存(redis,memcached)实现分布式锁
基于Zookeeper实现分布式锁

source: 《分布式锁的几种实现方式》

实现方式一:利用mysql的隔离性:唯一索引

use test;
CREATE TABLE `DistributedLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_name` (`name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

//数据库中的每一条记录就是一把锁,利用的mysql唯一索引的排他性

//操作前先插入一条数据,因为是唯一索引,所以其他人也想拿到锁插入数据的话会报错!
lock(name,desc){
    insert into DistributedLock(`name`,`desc`) values (#{name},#{desc});
}
//===处理业务===  do something
//解锁,删除数据库记录,删除后,其他线程可以继续插入数据,也就是可以继续获得锁。
unlock(name){
    delete from DistributedLock where name = #{name}
}

锁重入:可增加可重入功能(避免再次获取锁导致死锁)
增加字段进程识别信息(ip、服务名称、线程id) 与 重入计数count,如果是同一个进程同一个线程则允许重入。

获取:再次获取锁的同时更新count(+1).
释放:更新count-1,当count==0删除记录。

可靠性
主从mysql:mysql宕机,立刻切换。
锁的持有者挂掉:定时任务清楚持有一定时间的锁。
性能
db操作都有一定性能损耗
阻塞锁
有此需求的业务线需要使用自旋多次尝试获取锁的实现。

实现方式二:利用select … where … for update 排他锁

boolean lock(){
    connection.setAutoCommit(false) //设置手动提交,释放锁的时候提交
    while(true){
        try{
            result = select ... from DistributedLock where name=lock for update;
            if(result==null){
                return true;
            }
        }catch(Exception e){
        connection.commit();
        }
        sleep(*);
    }
    return false;
}
//释放锁,提交
void unlock(){
    connection.commit();
}

其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。

实现方式三:version 乐观锁

所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。
通过增加递增的版本号字段实现乐观锁

 select ...,version 
 update  table set version+1 where version=xx
image.png

Redis实现分布式锁(推荐)

使用Redis的 SETNX 命令可以实现分布式锁

SETNX key value
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。

返回值
返回整数,具体为

  • 1,当 key 的值被设置
  • 0,当 key 的值没被设置
1.分布式锁使用(不严谨,高并发会出现问题)
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
 
/**
 * @Author: 
 * @Date: 2019/5/25 21:54
 * @Description:
 */
public class RedisManager {
 
    private static JedisPool jedisPool;
 
    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(18);
        jedisPoolConfig.setMaxIdle(5);
        jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,5000);
    }
 
    public static Jedis getJedis() throws Exception {
        if (null != jedisPool){
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool was not init");
    }
}

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
 
import java.util.List;
 
/**
 * @Author: 
 * @Date: 2019/5/25 22:04
 * @Description:
 */
public class RedisLock {
 
    public String getLock(String key, int timeout) {
        Jedis jedis = null;
        try {
            jedis = RedisManager.getJedis();
//            String value = UUID.randomUUID().toString();
            //以当前线程id作为value
            String value = String.valueOf(Thread.currentThread().getId());
            long end = System.currentTimeMillis() + timeout;
            while (System.currentTimeMillis() < end) {
                if (jedis.setnx(key, value) == 1) {
                    jedis.expire(key, timeout);
                    return value;
                }
                if (jedis.ttl(key) == -1) {
                    jedis.expire(key, timeout);
                }
                Thread.sleep(500);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }
        return null;
    }
 
    public boolean releaseLock(String key, String value) {
        Jedis jedis = null;
        try {
            jedis = RedisManager.getJedis();
            while (true) {
                jedis.watch(key);
                System.out.println(jedis.get(key));
                if (value.equals(jedis.get(key))) {
                    Transaction transaction = jedis.multi();
                    transaction.del(key);
                    List<Object> list = transaction.exec();
                    if (list == null) {
                        continue;
                    }
                }
                jedis.unwatch();
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }
        return false;
    }
 
    public static void main(String[] args) {
        RedisLock redisLock = new RedisLock();
        String thredId = redisLock.getLock("test", 1000);
        System.out.println("返回值value:" + thredId);
        boolean b = redisLock.releaseLock("test", String.valueOf(Thread.currentThread().getId()));
        System.out.println(b);
    }
 
}
2.基于Redis实现分布式锁(推荐)

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。目前有很多成熟的缓存产品,包括Redis,memcached以及Tair。在这里就以常见的Redis来讲讲其实现:

Quartz定时器到时间被触发,程序开始先争取一个redis锁来执行任务。redis的命令可以采用如下:

SET key value [EX seconds] [PX milliseconds] [NX|XX]
   public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

第一个为key,使用key来当锁,因为key是唯一的。
第二个为value,可以传的是requestId,有key作为锁不就够了吗为什么还需要value呢?原因就是在上面讲到可靠性时,分布式锁需要特定的客户端来解锁,通过给value赋值为requestId就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为timev,与第四个参数相呼应,代表key的过期时间,1000等价于1s。
总的来说,执行上面的set()方法就只会导致两种结果:

当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
已有锁存在,不做任何操作。
加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。

当然除了setnx(),还有基于Redlock做分布式锁、基于Redisson做分布式锁,Redis做分布式锁是性能最好的(优于Zookeeper锁),但是可靠性方面逊于ZK,且通过超时时间来控制锁的失效时间并不是十分的靠谱。

source:Redis分布式锁的正确实现方式

基于Zookeeper实现分布式锁

基于zookeeper临时有序节点可以实现的分布式锁。

大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

来看下Zookeeper能不能解决前面提到的问题。

  • 锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

  • 非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

  • 不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

  • 单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

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;
}

Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

使用ZK实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)


总结

使用Zookeeper实现分布式锁的优点

有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

使用Zookeeper实现分布式锁的缺点

性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。


三种方案的比较

上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)

Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库

Zookeeper source:
http://www.hollischuang.com/archives/1716

相关文章

网友评论

      本文标题:分布式锁实现

      本文链接:https://www.haomeiwen.com/subject/oooiohtx.html