如何基于Redis实现分布式锁,详细教程拿走不送~

本系列文章,笔者准备对互联网缓存利器Redis的使用,做一下简单的总结,内容大概如下:

博文内容资源链接
Linux环境下搭建Redis基础运行环境https://blog.csdn.net/smilehappiness/article/details/107298145
互联网缓存利器-Redis的使用详解(基础篇)https://blog.csdn.net/smilehappiness/article/details/107592368
Redis基础命令使用Api详解https://blog.csdn.net/smilehappiness/article/details/107593218
Redis编程客户端Jedis、Lettuce和Redisson的基础使用https://blog.csdn.net/smilehappiness/article/details/107301988
互联网缓存利器-Redis的使用详解(进阶篇)https://blog.csdn.net/smilehappiness/article/details/107592336
如何基于Redis实现分布式锁https://blog.csdn.net/smilehappiness/article/details/107592896
基于Redis的主从复制、哨兵模式以及集群的使用,史上最详细的教程来啦~https://blog.csdn.net/smilehappiness/article/details/107433525
Redis相关的面试题总结https://blog.csdn.net/smilehappiness/article/details/107592686

1 为什么要使用分布式锁

使用分布式锁的目的,为了保证一个方法在高并发情况下,同一时间只能被同一个线程执行,即保证同一时间只有一个客户端可以对共享资源进行操作

在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

2 基于RedisTemplate实现分布式锁(推荐)

使用Spring提供的redisTemplate,操作redis还是比较简单的,使用RedisTemplate如何实现分布式锁呢?

2.1 添加相关maven依赖

这里,建议使用2.1.0以上的版本,因为Redis在2.1.0以上版本,已经实现了原子性操作,在设置key、value时,直接可以设置超时时间

<!-- spring-data-redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

<dependency>
   <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

2.2 注入分布式锁工具类

	//使用RedisTemplate实现分布式锁
    private final RedisLockHelper redisLockHelper;

    public GoodsServiceImpl(RedisLockHelper redisLockHelper) {
        this.redisLockHelper = redisLockHelper;
    }

2.3 相关配置

【redis配置】

#ip地址
redis.hostName=127.0.0.1
#端口号
redis.port=6379
#如果有密码
redis.password=123456
redis.database=0

【spring配置文件】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="cn.smilehappiness.distributed"/>

    <!--读取redis.properties属性配置文件-->
    <context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/>

    <!-- 配置redis连接工厂 -->
    <bean id="lettuceConnectionFactory"
          class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
        <!--构造方法初始化-->
        <constructor-arg index="0" ref="redisStandaloneConfiguration"/>
    </bean>

    <!-- lettuce连接配置信息 -->
    <bean id="redisStandaloneConfiguration"
          class="org.springframework.data.redis.connection.RedisStandaloneConfiguration">
        <property name="hostName" value="${redis.hostName}"/>
        <property name="port" value="${redis.port}"/>
        <property name="database" value="${redis.database}"/>
        <!--配置redis密码-->
        <property name="password" ref="redisPassword"/>
    </bean>

    <!-- lettuce连接密码信息 -->
    <bean id="redisPassword" class="org.springframework.data.redis.connection.RedisPassword">
        <!--构造方法初始化-->
        <constructor-arg index="0" value="${redis.password}"/>
    </bean>

    <!--手动设置 key  与 value的序列化方式-->
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>

    <!--redis的操作模板-->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="lettuceConnectionFactory"/>
        <property name="keySerializer" ref="keySerializer"/>
        <property name="valueSerializer" ref="valueSerializer"/>
        <property name="hashKeySerializer" ref="keySerializer"/>
        <property name="hashValueSerializer" ref="valueSerializer"/>
    </bean>
</beans>

如果你是spring boot项目,参考网上配置即可,网上资源有很多。

下面,重点来啦,如何实现分布式锁?

2.4 实现分布式锁

【分布式锁工具类】

package cn.smilehappiness.distributed.lock.redistemplate;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


/**
 * <p>
 * Redis中,基于redisTemplate的分布式锁
 * 注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级(默认是最低优先级,值越小优先级越高),而不是定义Bean的加载顺序
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/8/2 13:53
 */
@Order(1)
@Component
public class RedisLockHelper {

    private static Logger logger = LoggerFactory.getLogger(RedisLockHelper.class);
    /**
     * 设置分布式锁业务前缀
     */
    private static final String REDIS_LOCK_PREFIX = "redis:lock:";
    /**
     * 设置分布式锁超时(过期)时间,单位是秒
     */
    private static final Long LOCK_TIME_OUT = 60L;
    /**
     * 获取分布式锁超时时间,单位是秒,如果指定时间内还未获取到锁,则不能进行业务处理
     */
    private static final Long ACQUIRE_LOCK_TIME_OUT = 15L;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * <p>
     * 创建RedisLockHelper对象的时候,监听redis节点key是否需要续期(不推荐使用)
     * 这里进行redis分布式锁的续期,针对业务执行时,如果超过了锁超时时间的一半还没有释放锁,说明该业务方法比较耗时,进行自动续期。
     * 防止业务未执行完,锁过期了导致自动释放锁,造成业务数据问题
     * <p>
     * 注意:通常情况下不需要考虑续期问题,如果业务方法确实执行的比较耗时,才考虑此种问题
     * <p/>
     *
     * @param
     * @return
     * @Date 2020/8/2 17:18
     */
    public RedisLockHelper() {
        //分布式锁的自动续期,因为锁超时时间内,可能还没有执行完业务处理
        //启动一个后台任务,定时去检查redis的分布式锁是否需要续期(过期时间往后延一下)
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate(this::checkRedisExpire, 5, 10, TimeUnit.SECONDS);
    }

    private void checkRedisExpire() {
        Set<String> keys = redisTemplate.keys(REDIS_LOCK_PREFIX + "*");
        keys.forEach(key -> {
            //目前redis中锁剩下的过期时间
            //从redis中获取key对应的过期时间:如果该值有过期时间,就返回相应的过期时间,如果该值没有设置过期时间,就返回-1,如果没有该值,就返回-2
            Long expireTime = redisTemplate.opsForValue().getOperations().getExpire(key);
            //如果小于一半过期时间,因为还没有执行完,延长过期时间
            if (expireTime >= -1 && expireTime <= LOCK_TIME_OUT / 2) {
                redisTemplate.expire(key, LOCK_TIME_OUT, TimeUnit.SECONDS);
                System.out.println("执行redisTemplate分布式锁【" + key + "】续期......");
            }
        });
    }

    /**
     * <p>
     * Redis老版本实现方案(2.1以下)
     * <p/>
     *
     * @param lockName
     * @return java.lang.String
     * @Date 2020/8/2 15:40
     */
    public String getLockOld(String lockName) {
        String redisLockKey = REDIS_LOCK_PREFIX + lockName;
        String uniqueValue = UUID.randomUUID().toString();

        //往后延acquireLockTimeOut秒(比如30秒)
        Long endTime = System.currentTimeMillis() + ACQUIRE_LOCK_TIME_OUT * 1000;

        //如果不超过指定的锁获取时间,有资格重复获取锁
        while (System.currentTimeMillis() < endTime) {
            // 注意:这里不是原子操作(不在一个步骤中,是分了两步),有可能会有小问题(如果设置了锁还没来得及设置过期时间,redis服务挂了,可能导致死锁问题)
            //解决方案:下边判断过期时间,如果没有设置超时时间,来避免死锁问题
            if (redisTemplate != null && !redisTemplate.hasKey(redisLockKey) && redisTemplate.opsForValue().setIfAbsent(redisLockKey, uniqueValue)) {
                redisTemplate.expire(redisLockKey, LOCK_TIME_OUT, TimeUnit.SECONDS);
                return uniqueValue;
            }

            /**
             * 从redis中获取key对应的过期时间:
             * 如果该值有过期时间,就返回相应的过期时间
             * 如果该值没有设置过期时间,就返回-1
             * 如果没有该值,就返回-2
             */
            Long expireTime = redisTemplate.opsForValue().getOperations().getExpire(redisLockKey);
            if (expireTime != null && expireTime == -1) {
                //设置过期时间
                redisTemplate.expire(redisLockKey, LOCK_TIME_OUT, TimeUnit.SECONDS);
            }

            try {
                //立刻马上去循环再获取锁,其实不是很好也没有什么意义,最好是稍等片刻再去重试获取锁(这里可以根据实际场景去设计,是否需要重试获取锁,如果不需要,就不用设置while循环)
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return null;
    }

    /**
     * <p>
     * Redis老版本实现方案(2.1以下),老版本未提供原子操作,自己实现原子操作
     * 注:在2.1以上版本,已经实现了原子性操作,无需自己实现
     * <p/>
     *
     * @param lockName
     * @return java.lang.String
     * @Date 2020/8/2 16:30
     */
    public String getLockOldTwo(String lockName) {
        try {
            String redisLockKey = REDIS_LOCK_PREFIX + lockName;
            String uniqueValue = UUID.randomUUID().toString();

            //往后延acquireLockTimeOut秒(比如30秒)
            Long endTime = System.currentTimeMillis() + ACQUIRE_LOCK_TIME_OUT * 1000;

            //如果不超过指定的锁获取时间,有资格获取锁
            while (System.currentTimeMillis() < endTime) {
                // 上面方案中,虽然可以解决可能的死锁问题,但是,既然本质上是因为不是原子操作导致的问题,那么,能不能改成原子操作呢?
                if (redisTemplate != null && !redisTemplate.hasKey(redisLockKey) && this.setIfAbsent(redisLockKey, uniqueValue, LOCK_TIME_OUT, TimeUnit.SECONDS)) {
                    return uniqueValue;
                }

                /**
                 * 从redis中获取key对应的过期时间:
                 * 如果该值有过期时间,就返回相应的过期时间,如果该值没有设置过期时间,就返回-1
                 * 如果没有该值,就返回-2
                 * 这里由于上面可以保证原子性操作了,所以这里可以不用再判断是否有设置过过期时间
                 */
                Long expireTime = redisTemplate.opsForValue().getOperations().getExpire(redisLockKey);
                if (expireTime != null && expireTime == -1) {
                    //设置过期时间
                    redisTemplate.expire(redisLockKey, LOCK_TIME_OUT, TimeUnit.SECONDS);
                }

                try {
                    //立刻马上去循环再获取锁,其实不是很好也没有什么意义,最好是稍等片刻再去重试获取锁(这里可以根据实际场景去设计,是否需要重试获取锁,如果不需要,就不用设置while循环)
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * <p>
     * Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent
     * <p/>
     *
     * @param key
     * @param value
     * @param timeout
     * @param unit
     * @return java.lang.Boolean
     * @Date 2020/3/13 11:03
     */
    private Boolean setIfAbsent(Object key, Object value, long timeout, TimeUnit unit) {

        byte[] rawKey = rawKey(key);
        byte[] rawValue = rawValue(value);

        Expiration expiration = Expiration.from(timeout, unit);
        return redisTemplate.execute(connection -> connection.set(rawKey, rawValue, expiration, RedisStringCommands.SetOption.ifAbsent()), true);
    }

    byte[] rawKey(Object key) {
        Assert.notNull(key, "non null key required");
        return this.keySerializer() == null && key instanceof byte[] ? (byte[]) ((byte[]) key) : this.keySerializer().serialize(key);
    }

    byte[] rawValue(Object value) {
        return this.valueSerializer() == null && value instanceof byte[] ? (byte[]) ((byte[]) value) : this.valueSerializer().serialize(value);
    }

    RedisSerializer keySerializer() {
        return this.redisTemplate.getKeySerializer();
    }

    RedisSerializer valueSerializer() {
        return this.redisTemplate.getValueSerializer();
    }

    /**
     * <p>
     * Redis在2.1.0以上版本,已经实现了原子性操作,无需自己实现
     * <p/>
     *
     * @param lockName
     * @return java.lang.String
     * @Date 2020/8/2 17:30
     */
    public String getLock(String lockName) {
        try {
            String redisLockKey = REDIS_LOCK_PREFIX + lockName;
            String uniqueValue = UUID.randomUUID().toString();

            //往后延acquireLockTimeOut秒(比如30秒)
            Long endTime = System.currentTimeMillis() + ACQUIRE_LOCK_TIME_OUT * 1000;

            //如果不超过指定的锁获取时间,有资格获取锁
            while (System.currentTimeMillis() < endTime) {
                // 上面方案中,虽然可以解决可能的死锁问题,但是,既然本质上是因为不是原子操作导致的问题,那么,能不能改成原子操作呢?
                if (redisTemplate != null && !redisTemplate.hasKey(redisLockKey) && redisTemplate.opsForValue().setIfAbsent(redisLockKey, uniqueValue, LOCK_TIME_OUT, TimeUnit.SECONDS)) {
                    return uniqueValue;
                }

                /**
                 * 从redis中获取key对应的过期时间:
                 * 如果该值有过期时间,就返回相应的过期时间,如果该值没有设置过期时间,就返回-1
                 * 如果没有该值,就返回-2
                 * 这里由于上面可以保证原子性操作了,所以这里可以不用再判断是否有设置过过期时间
                 */
                Long expireTime = redisTemplate.opsForValue().getOperations().getExpire(redisLockKey);
                if (expireTime != null && expireTime == -1) {
                    //设置过期时间
                    redisTemplate.expire(redisLockKey, LOCK_TIME_OUT, TimeUnit.SECONDS);
                }

                try {
                    //立刻马上去循环再获取锁,其实不是很好也没有什么意义,最好是稍等片刻再去重试获取锁(这里可以根据实际场景去设计,是否需要重试获取锁,如果不需要,就不用设置while循环)
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * <p>
     * 释放分布式锁,保证只能释放自己的锁(自己的锁自己解,不要把别人的锁给解了)
     * <p/>
     *
     * @param lockName
     * @param value
     * @return java.lang.Boolean
     * @Date 2020/8/2 16:57
     */
    public Boolean releaseLock(String lockName, String value) {
        //redis锁的key
        String redisLockKey = REDIS_LOCK_PREFIX + lockName;

        //保证只能释放自己的锁(自己的锁自己解,不要把别人的锁给解了)
        Object object = redisTemplate.opsForValue().get(redisLockKey);
        if (object != null && StringUtils.equals(value, String.valueOf(object))) {
            return redisTemplate.delete(redisLockKey);
        }

        return false;
    }

}  

2.5 功能测试

/**
     * <p>
     * 分布式锁测试
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/8/2 20:56
     */
    private void testDistributedLock() {
        String lockName = "lockName";
        String lockUniqueValue = null;

        try {
            //获取分布式锁,然后下面的业务代码就会按顺序排队执行
            lockUniqueValue = redisLockHelper.getLock(lockName);
            //拿到redis分布式锁
            if (lockUniqueValue != null) {
                //TODO 执行业务代码
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放分布式锁
            redisLockHelper.releaseLock(lockName, lockUniqueValue);
        }
    }

以上,就基于RedisTemplate模板,实现了分布式锁。有需要的童鞋们,可以实际操作一下,该方案适用于多线程且高并发的场景

3 基于Jedis客户端,实现分布式锁

3.1 添加依赖

<!-- spring-data-redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.1.10.RELEASE</version>
</dependency>

<!-- jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>

3.2 创建Jedis实例对象

package cn.smilehappiness.distributed.lock.jedis.util;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * <p>
 * 获取Jedis实例对象
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/8/2 11:58
 */
public class JedisPoolInstance {

    /**
     * redis服务器的ip地址
     */
    private static final String HOST = "localhost";
    /**
     * redis服务器的端口
     */
    private static final int PORT = 6379;
    /**
     * 连接redis服务器的密码
     */
    private static final String PASSWORD = "123456";

    private static final int TIMEOUT = 10000;

    /**
     * redis连接池对象,单例的连接池对象
     */
    private static JedisPool jedisPool = null;

    //私有构造方法
    private JedisPoolInstance() {
    }

    /**
     * 获取线程池实例对象
     *
     * @return
     */
    public static JedisPool getJedisPoolInstance() {
        //双重检测锁
        if (null == jedisPool) {
            synchronized (JedisPoolInstance.class) {
                if (null == jedisPool) {
                    //对连接池的参数进行配置,根据项目的实际情况配置这些参数
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    //最大连接数
                    poolConfig.setMaxTotal(1000);
                    //最大空闲连接数
                    poolConfig.setMaxIdle(32);
                    //获取连接时的最大等待毫秒数
                    poolConfig.setMaxWaitMillis(90 * 1000);
                    //在获取连接的时候检查连接有效性
                    poolConfig.setTestOnBorrow(true);

                    jedisPool = new JedisPool(poolConfig, HOST, PORT, TIMEOUT, PASSWORD);
                }
            }
        }

        return jedisPool;
    }
}

3.3 相关配置

【redis配置】

#ip地址
redis.hostName=127.0.0.1
#端口号
redis.port=6379
#如果有密码
redis.password=123456
redis.database=0

【Spring配置】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">

    <!--读取redis.properties属性配置文件-->
    <context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/>

    <!-- 配置redis连接工厂 -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!--构造方法初始化-->
        <constructor-arg index="0" ref="redisStandaloneConfiguration"/>
    </bean>

    <!-- jedis连接配置信息 -->
    <bean id="redisStandaloneConfiguration"
          class="org.springframework.data.redis.connection.RedisStandaloneConfiguration">
        <property name="hostName" value="${redis.hostName}"/>
        <property name="port" value="${redis.port}"/>
        <property name="database" value="${redis.database}"/>
        <!--配置redis密码-->
        <property name="password" ref="redisPassword"/>
    </bean>

    <!-- jedis连接密码信息 -->
    <bean id="redisPassword" class="org.springframework.data.redis.connection.RedisPassword">
        <!--构造方法初始化-->
        <constructor-arg index="0" value="${redis.password}"/>
    </bean>

    <!--手动设置 key  与 value的序列化方式-->
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>

    <!--redis的操作模板-->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer" ref="keySerializer" />
        <property name="valueSerializer" ref="valueSerializer" />
        <property name="hashKeySerializer" ref="keySerializer" />
        <property name="hashValueSerializer" ref="valueSerializer" />
    </bean>

</beans>

3.4 实现分布式锁

package cn.smilehappiness.distributed.lock.jedis;

import cn.smilehappiness.distributed.lock.jedis.util.JedisPoolInstance;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 基于Jedis客户端,实现分布式锁
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/8/2 12:05
 */
public class JedisDistributeLock {

    private static final String redisLockPrefix = "redis:lock:";
    /**
     * 设置锁超时时间,单位是毫秒
     */
    private static final Long LockTimeOut = 30000L;

    /**
     * 静态代码块在类加载的时候执行,只会一次
     */
    static {
        //分布式锁的自动续期,因为锁超时时间内,可能还没有执行完业务处理
        //启动一个后台任务,定时去检查redis的分布式锁是否需要续期(过期时间往后延一下)
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate(() -> {
                    System.out.println("执行Jedis分布式锁续期..........");
                    Jedis jedis = JedisPoolInstance.getJedisPoolInstance().getResource();
                    try {
                        Set<String> setKeys = jedis.keys(redisLockPrefix + "*");
                        setKeys.forEach((String key) -> {
                            //目前redis中锁剩下的过期时间
                            //key不存在的时候返回-2
                            Long leftExpire = jedis.ttl(key);
                            if (leftExpire >= -1 && leftExpire <= (LockTimeOut - 10000) / 1000) {
                                jedis.pexpire(key, LockTimeOut);
                            }
                        });
                    } finally {
                        if (jedis != null) {
                            jedis.close();
                        }
                    }
                },
                5,
                3,
                TimeUnit.SECONDS);
    }

    /**
     * <p>
     * 获取分布式锁
     * <p/>
     *
     * @param lockName
     * @param acquireTimeOut 单位是毫秒
     * @param lockTimeOut    单位是毫秒
     * @return java.lang.String
     * @Date 2020/8/2 12:13
     */
    public String getRedisLock(String lockName, Long acquireTimeOut, Long lockTimeOut) {
        String redisLockKey = redisLockPrefix + lockName;
        String uniqueValue = UUID.randomUUID().toString();

        JedisPool jedisPool = JedisPoolInstance.getJedisPoolInstance();
        Jedis jedis = jedisPool.getResource();
        System.out.println("获取jedis连接池:" + Thread.currentThread().getName() + ":" + jedisPool + ", " + jedisPool.getNumActive() + ", " + jedisPool.getNumIdle());

        try {
            //往后延acquireTimeOut秒(比如3秒)
            Long endTime = System.currentTimeMillis() + acquireTimeOut;

            //如果不超过指定的锁获取时间,有资格获取锁
            while (System.currentTimeMillis() < endTime) {

                // 设置key和设置key的过期时间
                // 注意:这里不是原子操作(不在一个步骤中,是分了两步),有可能会有小问题(如果设置了锁还没来得及设置过期时间,redis服务挂了,可能导致死锁问题,下边双重判断过期时间,如果没有设置超时时间,来避免死锁问题)
                if (jedis.setnx(redisLockKey, uniqueValue) == 1) {
                    //设置key成功,表示拿到锁
                    jedis.pexpire(redisLockKey, lockTimeOut);
                    return uniqueValue;
                }

                //这里如果不做处理,可能产生死锁,因为上边不是原子操作
                if (jedis.ttl(redisLockKey) == -1) {
                    //设置过期时间
                    jedis.pexpire(redisLockKey, lockTimeOut);
                }

                try {
                    //立刻马上去循环再获取锁,其实不是很好也没有什么意义,最好是稍等片刻再去重试获取锁
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            if (jedis != null) {
                jedis.close();

                System.out.println("关闭jedis连接池:" + Thread.currentThread().getName() + ":" + jedisPool + ", " + jedisPool.getNumActive() + ", " + jedisPool.getNumIdle());
            }
        }
        return null;
    }

    /**
     * <p>
     * 释放redis锁
     * <p/>
     *
     * @param lockName
     * @param uniqueValue
     * @return void
     * @Date 2020/8/2 12:12
     */
    public void releaseRedisLock(String lockName, String uniqueValue) {
        //redis锁的key
        String redisLockKey = redisLockPrefix + lockName;
        Jedis jedis = JedisPoolInstance.getJedisPoolInstance().getResource();

        try {
            //保证只能释放自己的锁(自己的锁自己解,不要把别人的锁给解了)
            if (jedis.get(redisLockKey).equals(uniqueValue)) {
                jedis.del(redisLockKey);
            }
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

3.5 功能测试

/**
     * <p>
     * 分布式锁测试
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/8/2 20:56
     */
    private void testDistributedLock() {
		//使用jedis客户端实现的redis锁
        JedisDistributeLock redisDistributeLock = new JedisDistributeLock();

        String lockName = "lockName";
        String lockUniqueValue = null;

        try {
            //获取分布式锁,然后下面的业务代码就会按顺序排队执行
            lockUniqueValue = redisDistributeLock.getRedisLock(lockName, 3000L, 30000L);
            //拿到redis分布式锁
            if (lockUniqueValue != null) {
                //TODO 执行业务代码
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放分布式锁
            redisDistributeLock.releaseRedisLock(lockName, lockUniqueValue);
        }
    }

4 基于Lettuce客户端,实现分布式锁

4.1 添加依赖

<!-- spring-data-redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.1.10.RELEASE</version>
</dependency>

<!-- lettuce-core -->
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

4.2 创建LettuceConnection对象

package cn.smilehappiness.distributed.lock.lettuce.util;

import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;

public class LettuceConnection {

    private static final String REDIS_ADDRESS = "redis://123456@localhost:6379/0";

    private static StatefulRedisConnection<String, String> statefulRedisConnection = null;

    private LettuceConnection() {
    }

    public static StatefulRedisConnection<String, String> getStatefulRedisConnection() {
        //双重检测锁
        if (null == statefulRedisConnection) {
            synchronized (LettuceConnection.class) {
                if (null == statefulRedisConnection) {
                    RedisClient redisClient = RedisClient.create(REDIS_ADDRESS);
                    statefulRedisConnection =  redisClient.connect();
                }
            }
        }
        return statefulRedisConnection;
    }
}

4.3 相关配置

【redis配置】

#ip地址
redis.hostName=127.0.0.1
#端口号
redis.port=6379
#如果有密码
redis.password=123456
redis.database=0

【Spring配置】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="cn.smilehappiness.distributed"/>

    <!--读取redis.properties属性配置文件-->
    <context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/>

    <!-- 配置redis连接工厂 -->
    <bean id="lettuceConnectionFactory"
          class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
        <!--构造方法初始化-->
        <constructor-arg index="0" ref="redisStandaloneConfiguration"/>
    </bean>

    <!-- lettuce连接配置信息 -->
    <bean id="redisStandaloneConfiguration"
          class="org.springframework.data.redis.connection.RedisStandaloneConfiguration">
        <property name="hostName" value="${redis.hostName}"/>
        <property name="port" value="${redis.port}"/>
        <property name="database" value="${redis.database}"/>
        <!--配置redis密码-->
        <property name="password" ref="redisPassword"/>
    </bean>

    <!-- lettuce连接密码信息 -->
    <bean id="redisPassword" class="org.springframework.data.redis.connection.RedisPassword">
        <!--构造方法初始化-->
        <constructor-arg index="0" value="${redis.password}"/>
    </bean>

    <!--手动设置 key  与 value的序列化方式-->
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>

    <!--redis的操作模板-->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="lettuceConnectionFactory"/>
        <property name="keySerializer" ref="keySerializer"/>
        <property name="valueSerializer" ref="valueSerializer"/>
        <property name="hashKeySerializer" ref="keySerializer"/>
        <property name="hashValueSerializer" ref="valueSerializer"/>
    </bean>
</beans>

4.4 实现分布式锁

package cn.smilehappiness.distributed.lock.lettuce;

import cn.smilehappiness.distributed.lock.lettuce.util.LettuceConnection;
import io.lettuce.core.api.sync.RedisCommands;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 基于Lettuce客户端,实现分布式锁
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/8/2 12:05
 */
public class LettuceDistributeLock {

    private static final String redisLockPrefix = "redis:lock:";
    /**
     * 设置锁超时时间,单位是毫秒
     */
    private static final Long LockTimeOut = 30000L;

    /**
     * 静态代码块在类加载的时候执行,只会一次
     */
    static {
        //分布式锁的自动续期,因为锁超时时间内,可能还没有执行完业务处理
        //启动一个后台任务,定时去检查redis的分布式锁是否需要续期(过期时间往后延一下)
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate(() -> {
                    System.out.println("执行Lettuce分布式锁续期..........");
                    RedisCommands<String, String> syncCommands = LettuceConnection.getStatefulRedisConnection().sync();

                    List<String> setKeys = syncCommands.keys(redisLockPrefix + "*");
                    setKeys.forEach((String key) -> {
                        //目前redis中锁剩下的过期时间
                        //key不存在的时候返回-2
                        Long leftExpire = syncCommands.ttl(key);
                        if (leftExpire >= -1 && leftExpire <= (LockTimeOut - 10000) / 1000) {
                            syncCommands.pexpire(key, LockTimeOut);
                        }
                    });
                },
                5,
                3,
                TimeUnit.SECONDS);
    }

    /**
     * <p>
     * 获取分布式锁
     * <p/>
     *
     * @param lockName
     * @param acquireTimeOut 单位是毫秒
     * @param lockTimeOut    单位是毫秒
     * @return java.lang.String
     * @Date 2020/8/2 12:13
     */
    public String getRedisLock(String lockName, Long acquireTimeOut, Long lockTimeOut) {
        String redisLockKey = redisLockPrefix + lockName;
        String uniqueValue = UUID.randomUUID().toString();

        RedisCommands<String, String> syncCommands = LettuceConnection.getStatefulRedisConnection().sync();

        //往后延acquireTimeOut秒(比如3秒)
        Long endTime = System.currentTimeMillis() + acquireTimeOut;

        //如果不超过指定的锁获取时间,有资格获取锁
        while (System.currentTimeMillis() < endTime) {

            // 设置key和设置key的过期时间
            // 注意:这里不是原子操作(不在一个步骤中,是分了两步),有可能会有小问题(如果设置了锁还没来得及设置过期时间,redis服务挂了,可能导致死锁问题,下边双重判断过期时间,如果没有设置超时时间,来避免死锁问题)
            if (syncCommands.setnx(redisLockKey, uniqueValue)) {
                //设置key成功,表示拿到锁
                syncCommands.pexpire(redisLockKey, lockTimeOut);
                return uniqueValue;
            }

            if (syncCommands.ttl(redisLockKey) == -1) {
                //设置过期时间
                syncCommands.pexpire(redisLockKey, lockTimeOut);
            }

            try {
                //立刻马上去循环再获取锁,其实不是很好也没有什么意义,最好是稍等片刻再去重试获取锁
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return null;
    }

    /**
     * <p>
     * 释放redis锁
     * <p/>
     *
     * @param lockName
     * @param uniqueValue
     * @return void
     * @Date 2020/8/2 12:12
     */
    public void releaseRedisLock(String lockName, String uniqueValue) {
        //redis锁的key
        String redisLockKey = redisLockPrefix + lockName;
        RedisCommands<String, String> syncCommands = LettuceConnection.getStatefulRedisConnection().sync();

        //保证只能释放自己的锁(自己的锁自己解,不要把别人的锁给解了)
        if (syncCommands.get(redisLockKey).equals(uniqueValue)) {
            syncCommands.del(redisLockKey);
        }
    }
}

4.5 功能测试

/**
     * <p>
     * 分布式锁测试
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/8/2 20:56
     */
    private void testDistributedLock() {
		 //使用Lettuce客户端实现的分布式锁
    	LettuceDistributeLock redisDistributeLock = new LettuceDistributeLock();

        String lockName = "lockName";
        String lockUniqueValue = null;

        try {
            //获取分布式锁,然后下面的业务代码就会按顺序排队执行
            lockUniqueValue = redisDistributeLock.getRedisLock(lockName, 3000L, 30000L);
            //拿到redis分布式锁
            if (lockUniqueValue != null) {
                //TODO 执行业务代码
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放分布式锁
            redisDistributeLock.releaseRedisLock(lockName, lockUniqueValue);
        }
    }

5 基于Redisson客户端,实现分布式锁(推荐)

5.1 添加依赖

<!-- spring-data-redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.1.10.RELEASE</version>
</dependency>

<!-- redisson -->
<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.12.5</version>
</dependency>

5.2 相关配置

【redis配置】

#ip地址
redis.hostName=127.0.0.1
#端口号
redis.port=6379
#如果有密码
redis.password=123456
redis.database=0

【Spring配置】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:redisson="http://redisson.org/schema/redisson"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
       http://redisson.org/schema/redisson
       http://redisson.org/schema/redisson/redisson.xsd">

    <!--redisson客户端对象-->
    <redisson:client>
        <redisson:single-server address="redis://localhost:6379" password="123456"/>
    </redisson:client>

</beans>

5.3 实现分布式锁

Redisson客户端还是非常强大的,客户端本身提供的有分布式锁,而且支持自动续期等,推荐使用。

/**
     * <p>
     * 分布式锁测试
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/8/2 20:56
     */
    private void testDistributedLock() {
        String lockName = "lockName";
		RLock rLock = redissonClient.getLock(lockName);

        try {
            //获取分布式锁,然后下面的业务代码就会按顺序排队执行
            rLock.lock();
            //TODO 拿到redis分布式锁,执行业务代码
      
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放分布式锁
            if (rLock.isHeldByCurrentThread() && rLock.isLocked()) {
                rLock.unlock();
            }
        }
    }

好啦,本界内容就介绍到这里了,如果对你有帮助,老铁们给个赞支持下呗!

有问题的伙伴们,欢迎评论讨论哈。鉴于笔者理解的深度,可能理解的有不到位的地方,欢迎各路大神指点!

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家评论,一起探讨,代码如有问题,欢迎各位大神指正!

给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
基于 Redis 实现分布式锁可以利用 Redis 的原子性操作和超时特性来实现。下面是一个基本的实现思路: 1. 获取锁:使用 Redis 的 SETNX 命令,如果指定的锁 key 不存在,则设置该 key 的值为当前时间戳加上锁的超时时间,并返回成功;否则,返回失败。 2. 释放锁:使用 Redis 的 EVAL 命令,通过 Lua 脚本来实现原子性的删除锁。脚本的内容是先判断锁是否存在且超时,如果是则删除锁并返回成功;否则,返回失败。 下面是一个简单的 Python 代码示例: ```python import redis import time class RedisLock: def __init__(self, redis_client, lock_key, expire_time): self.redis = redis_client self.lock_key = lock_key self.expire_time = expire_time def acquire(self): while True: timestamp = int(time.time() * 1000) + self.expire_time acquired = self.redis.set(self.lock_key, timestamp, nx=True, px=self.expire_time) if acquired: return True time.sleep(0.001) def release(self): lua_script = """ if redis.call("exists", KEYS[1]) == 1 then local current_value = tonumber(redis.call("get", KEYS[1])) if current_value and current_value <= tonumber(ARGV[1]) then return redis.call("del", KEYS[1]) end end return 0 """ self.redis.eval(lua_script, 1, self.lock_key, int(time.time() * 1000) + self.expire_time) # 使用示例 redis_client = redis.Redis(host='localhost', port=6379, db=0) lock = RedisLock(redis_client, 'my_lock', 1000) # 锁的超时时间为 1000 毫秒 if lock.acquire(): try: # 执行需要加锁的代码 pass finally: lock.release() ``` 需要注意的是,以上代码仅是一个简单的实现示例,实际使用中还需要考虑异常处理、锁的可重入性、锁的可拥有时间等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值