`

分布式锁之基于Redis的分布式锁

阅读更多

一、简述

       分布式锁一般有三种实现方式:第一,数据库乐观锁;第二,基于Redis的分布式锁;第三,基于Zookeeper的分布式锁。目前,在项目中有需要用到分布式锁的场景,因此学习并总结了。今天,咱们先来聊聊基于Redis的分布式锁。

       要保证基于Redis的分布式锁可用,必须同时满足以下四个条件:1、互斥性:在任何时刻只能有一个客户端持有锁;2、避免死锁:即使有一个客户端在持锁阶段出现崩溃而没有主动释放锁,也要保证后续其他客户端能加锁;3、具有容错性:只要大部分Redis的节点能正常运行,客户端就可以加锁和解锁;4、唯一性:客户端在加锁和解锁的过程中,必须只能是同一个客户端,客户端自己不能把别的客户端的锁解了。以上这四点,称之为可靠性。

 

二、简单示例

1、maven依赖

<properties>
    <jedis.version>2.9.0</jedis.version>
</properties>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>${jedis.version}</version>
</dependency>

 2、RedisTool工具类

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";
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 尝试获取分布式锁
     *
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超时时间
     * @return
     */
    public static boolean tryGetLock(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;
    }

    /**
     * 释放分布式锁
     *
     * @param jedis Jedis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return
     */
    public static boolean releaseLock(Jedis jedis, String lockKey, String requestId){
         String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
         Object object = jedis.eval(luaScript);
         if (RELEASE_SUCCESS.equals(object)) {
            return true;
         }
         return false;
    }
}

       从加锁的方法中可以看出,加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set方法一共五个参数:

  • key:使用key来作为锁,因为key是唯一的。
  • value:我们请求的是requestId,之所以有这个参数,就是根据可靠性的第四点,换言之,解铃还须系铃人。一般来说,我们会通过UUID.randomUUID().toString()的方式生成requestId。
  • nxxx:这个参数请求的是NX,即SET IF NOT EXIST。当key不存在时,进行set操作;若key已经存在,则不做任何操作。
  • expx:请求的是PX,就是给key设置一个过期的时间,具体时间由参数time决定。

上面的代码执行结果只有两种:1.当前没有锁,就进行加锁操作,并设置锁的过期时间,同时value设置为加锁的客户端;2.锁已经存在,不做任何操作。

 

三、加锁示例分析

        如果你认真阅读了前面的内容,你会发现,上面的示例并没有满足可靠性中的容错性。这是因为,容错性是在redis集群的环境下需要考虑的因素。而单机部署redis,容错性的优先级是最低的。

        在阅读团队中其他小伙伴写的关于redis实现分布式锁的代码中,发现了以下两种错误的实现方式,分别如下:

  • 错误示例一
public static void tryGetLockWithWrong(Jedis jedis, String lockKey, String requestId, int expireTime){
         Long result = jedis.setnx(lockKey, requestId);
         if (result == 1) {
             jedis.expire(lockKey, expireTime);
         }
    }

setnx()的作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。然而,由于这两条redis命令不具备原子性,如果jedis.expire(String key, int expireTime)发生异常或出现崩溃,此方法设置的锁过期时间就不会生效,就会导致死锁的出现。expire源码片段如下:

//Jedis.java
public Long expire(final String key, final int seconds) {
    checkIsInMultiOrPipeline();
    client.expire(key, seconds);
    return client.getIntegerReply();
}

//Client.java
public void expire(final String key, final int seconds) {
    expire(SafeEncoder.encode(key), seconds);
}

//BinaryClient.java
public void expire(final byte[] key, final int seconds) {
    sendCommand(EXPIRE, key, toByteArray(seconds));
}

//Connection.java
protected Connection sendCommand(final Command cmd, final byte[]... args) {
    try {
      connect();
      Protocol.sendCommand(outputStream, cmd, args);
      pipelinedCommands++;
      return this;
    } catch (JedisConnectionException ex) {
      try {
        String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
        if (errorMessage != null && errorMessage.length() > 0) {
          ex = new JedisConnectionException(errorMessage, ex.getCause());
        }
      } catch (Exception e) {
      }
      // Any other exceptions related to connection?
      broken = true;
      throw ex;
    }
  }

 之所以出现这种写法,是因为低版本的jedis不支持多参数的set方法。

 

  • 错误示例二
public static boolean getLockWithWrong(Jedis jedis, String lockKey, int expireTime) {
        long expires = System.currentTimeMillis() + expireTime;
        String expiresStr = String.valueOf(expires);
        if (jedis.setnx(lockKey, expiresStr) == 1) {
            return true;
        }
        String currentValueStr = jedis.get(lockKey);
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
            String oldValueStr = jedis.getSet(lockKey, expiresStr);
            if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                return true;
            }
        }
        return false;
    }

       这段代码的实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。

       乍看之下,这段逻辑没有问题,仔细分析,这里面存在以下的问题:1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

 

四、解锁示例分析

       从解锁的方法中可以看出,解锁只需要两行代码:第一行代码,一个简单的Lua脚本代码;第二行代码,将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

       引入lua脚本只是为了保证解锁操作的原子性(在eval命令执行Lua代码的时候,lua代码将被当成一个命令去执行,并且直到eval命令执行完成,redis才会执行其他命令)。逻辑很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。

       在阅读团队中小伙伴解锁方法时,也存在两种错误的实现方式,分别如下:

  • 错误示例一
public static void releaseLockWithWrong(Jedis jedis, String lockKey) {
      jedis.del(lockKey);
}

       直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

  • 错误示例二
public static void wrongReleaseLock(Jedis jedis, String lockKey, String requestId) {
        if (requestId.equals(jedis.get(lockKey))) {
            jedis.del(lockKey);
        }
    }

       问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

 

五、总结

       基于Redis实现的分布式锁在实现的时候应该要多看多思考,而不能一味地盲目在网上找一找。

分享到:
评论

相关推荐

    Redis思维导图分布式缓存-基于Redis集群解决单机Redis存在的问题

    分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。...

    C++基于redis的分布式锁redisAPI

    本文将深入探讨如何使用C++结合Redis实现分布式锁,并详细讲解Redis API在C++中的应用,以及如何处理与Boost库的集成。 首先,Redis是一个高性能的键值存储数据库,广泛用于缓存、消息队列、分布式锁等场景。分布式...

    springboot基于redis分布式锁

    本教程将深入探讨如何在SpringBoot应用中实现基于Redis的分布式锁。 首先,Redis之所以常被用作分布式锁的实现,是因为其具有以下优点: 1. **高可用性**:Redis支持主从复制,可以确保在单点故障时仍有服务可用。...

    Java基于redis实现分布式锁代码实例

    Java基于Redis实现分布式锁代码实例 分布式锁的必要性 在多线程环境中,资源竞争是一个常见的问题。例如,在一个简单的用户操作中,一个线程修改用户状态,首先在内存中读取用户状态,然后在内存中进行修改,然后...

    基于Redis方式实现分布式锁

    以下是一个简单的Java示例,展示了如何使用Jedis客户端库来实现Redis分布式锁。 ```java public class RedisLock { private JedisPool jedisPool; public RedisLock(JedisPool jedisPool) { this.jedisPool = ...

    C#.net Redis分布式锁源码实现

    本篇文章将深入探讨如何在C#.NET环境下利用Redis实现分布式锁,以及相关的核心知识点。 首先,让我们理解什么是分布式锁。分布式锁是在分布式系统中,用于协调不同节点间对共享资源访问的一种工具。它确保在任何...

    redis实现分布式锁,自旋式加锁,lua原子性解锁

    Redis中的分布式锁实现通常基于`SETNX`命令或`SET`命令的`nx`与`ex`组合。`SETNX`命令用于设置键值,但如果键已经存在,则不执行任何操作,这可以确保锁的互斥性。`SET key value EX timeout NX`则同时设置了超时...

    分布式锁实现(基于redis-mysql)1

    本文主要探讨了三种常见的分布式锁实现方式,包括基于Redis、MySQL以及Zookeeper的实现方法。 **基于Redis实现分布式锁** Redis是一个内存数据库,其命令执行是单线程的,这使得它非常适合用来实现分布式锁。Redis...

    基于Redis的分布式锁的实现方案.pdf

    基于Redis的分布式锁的实现方案 本文讨论了分布式锁的实现方案,主要基于Redis实现分布式锁,以解决分布式系统中资源访问的同步问题。在分布式系统中,需要协调各个系统或主机之间的资源访问,以避免彼此干扰和保证...

    redis分布式锁工具类

    现在很多项目单机版已经不满足了,分布式变得越受欢迎,同时也带来很多问题,分布式锁也变得没那么容易实现,分享一个redis分布式锁工具类,里面的加锁采用lua脚本(脚本比较简单,采用java代码实现,无须外部调用...

    C#实操控制并发之Lock和Redis分布式锁

    本文将深入探讨C#中如何使用Lock和Redis分布式锁来解决并发问题,以秒杀系统为例进行阐述。 首先,让我们理解什么是并发控制。并发控制是指在多线程环境下确保数据的一致性和完整性,防止多个线程同时访问同一资源...

    记录redisson实现redis分布式事务锁

    本篇文章将详细探讨如何使用Redisson实现Redis分布式事务锁,以及在Spring Boot环境中如何进行集成。 首先,Redis作为一个内存数据库,其高速读写性能使其成为实现分布式锁的理想选择。分布式锁的主要作用是在多...

    基于 Redis 的分布式锁

    在实现基于Redis的分布式锁时,通常会用到两个命令:NX(Not eXists)和EX(过期时间)。NX命令确保只有在键不存在时才能被设置,这样可以保证锁的互斥性。EX命令则是用来设置键的过期时间,保证锁可以在一段时间后...

    redis实现分布式锁(java/jedis)

    redis实现分布式锁(java/jedis),其中包含工具方法以及使用demo 本资源是利用java的jedis实现 redis实现分布式锁(java/jedis),其中包含工具方法以及使用demo 本资源是利用java的jedis实现

    用Redis实现分布式锁_redis_分布式_

    一、Redis分布式锁的优势 1. 响应速度:Redis是内存数据库,读写速度快,非常适合处理高并发的锁请求。 2. 原子性:Redis的操作是原子性的,如`SETNX`(设置如果不存在)、`EXPIRE`(设置过期时间),确保了锁的获取与...

    解析分布式锁之redis实现1

    本文将探讨如何利用Redis实现一个高效的分布式锁。 首先,分布式锁的基本应用场景通常包括保证任务的唯一执行、防止数据冲突等。在上述描述中,我们看到一个例子,涉及订单服务、报表服务和推送服务。报表服务需要...

    Sync 是一个分布式场景下基于Redis 的安全的一个线程同步组件,提供分布式可重入互斥锁、分布式可重入读写锁、分布式信号量

    Sync 是一个分布式场景下基于Redis 的安全的一个线程同步组件,提供分布式可重入互斥锁、分布式可重入读写锁、分布式信号量。提供相应的注释,使用简单,可以与 spring-boot 无缝集成。 环境要求 JDK1.8及以上

    redis分布式锁.zip

    Redis 分布式锁是分布式系统中解决并发控制和数据一致性问题的一种常见机制。在大型分布式应用中,单机锁无法满足需求,因为它们局限于单个服务器。Redis 的高可用性和低延迟特性使其成为实现分布式锁的理想选择。...

    redis和zookeeper实现分布式锁的区别

    Redis分布式锁主要基于`SETNX`命令或者RedLock算法。`SETNX`命令在键不存在时设置键值,但如果存在则返回失败,这可以用来简单地实现锁。然而,单实例的Redis分布式锁存在一定的风险,如主节点故障可能导致锁无法...

    Redis分布式锁使用+Redis处理数据并发+springboot整合Redis

    springboot整合Redis实现在分布式情况,使用分布式锁解决数据并发的方案,主要使用Redis提供的setIfAbsent方法实现,并且考虑到了setIfAbsent在极端情况下的多实例同时设置一个key成功的情况。 本案例通过实现对库存...

Global site tag (gtag.js) - Google Analytics