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

文章目录

  • 1 为什么要使用分布式锁
  • 2 基于RedisTemplate实现分布式锁(推荐)
    • 2.1 添加相关maven依赖
    • 2.2 注入分布式锁工具类
    • 2.3 相关配置
    • 2.4 实现分布式锁
    • 2.5 功能测试
  • 3 基于Jedis客户端,实现分布式锁
    • 3.1 添加依赖
    • 3.2 创建Jedis实例对象
    • 3.3 相关配置
    • 3.4 实现分布式锁
    • 3.5 功能测试
  • 4 基于Lettuce客户端,实现分布式锁
    • 4.1 添加依赖
    • 4.2 创建LettuceConnection对象
    • 4.3 相关配置
    • 4.4 实现分布式锁
    • 4.5 功能测试
  • 5 基于Redisson客户端,实现分布式锁(推荐)
    • 5.1 添加依赖
    • 5.2 相关配置
    • 5.3 实现分布式锁

本系列文章,笔者准备对互联网缓存利器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时,直接可以设置超时时间


<dependency>
    <groupId>org.springframework.datagroupId>
    <artifactId>spring-data-redisartifactId>
    <version>2.1.0.RELEASEversion>
dependency>

<dependency>
   <groupId>io.lettucegroupId>
    <artifactId>lettuce-coreartifactId>
    <version>5.2.2.RELEASEversion>
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配置文件】


<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"/>

    
    <context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/>

    
    <bean id="lettuceConnectionFactory"
          class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
        
        <constructor-arg index="0" ref="redisStandaloneConfiguration"/>
    bean>

    
    <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}"/>
        
        <property name="password" ref="redisPassword"/>
    bean>

    
    <bean id="redisPassword" class="org.springframework.data.redis.connection.RedisPassword">
        
        <constructor-arg index="0" value="${redis.password}"/>
    bean>

    
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>

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


/**
 * 

* Redis中,基于redisTemplate的分布式锁 * 注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级(默认是最低优先级,值越小优先级越高),而不是定义Bean的加载顺序 *

* * @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; /** *

* 创建RedisLockHelper对象的时候,监听redis节点key是否需要续期(不推荐使用) * 这里进行redis分布式锁的续期,针对业务执行时,如果超过了锁超时时间的一半还没有释放锁,说明该业务方法比较耗时,进行自动续期。 * 防止业务未执行完,锁过期了导致自动释放锁,造成业务数据问题 *

* 注意:通常情况下不需要考虑续期问题,如果业务方法确实执行的比较耗时,才考虑此种问题 *

* * @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 + "】续期......"); } }); } /** *

* Redis老版本实现方案(2.1以下) *

* * @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; } /** *

* Redis老版本实现方案(2.1以下),老版本未提供原子操作,自己实现原子操作 * 注:在2.1以上版本,已经实现了原子性操作,无需自己实现 *

* * @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; } /** *

* Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent *

* * @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(); } /** *

* Redis在2.1.0以上版本,已经实现了原子性操作,无需自己实现 *

* * @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; } /** *

* 释放分布式锁,保证只能释放自己的锁(自己的锁自己解,不要把别人的锁给解了) *

* * @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 功能测试

/**
     * 

* 分布式锁测试 *

* * @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 添加依赖


<dependency>
    <groupId>org.springframework.datagroupId>
    <artifactId>spring-data-redisartifactId>
    <version>2.1.10.RELEASEversion>
dependency>


<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
    <version>3.3.0version>
dependency>

3.2 创建Jedis实例对象

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

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

/**
 * 

* 获取Jedis实例对象 *

* * @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配置】


<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:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/>

    
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        
        <constructor-arg index="0" ref="redisStandaloneConfiguration"/>
    bean>

    
    <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}"/>
        
        <property name="password" ref="redisPassword"/>
    bean>

    
    <bean id="redisPassword" class="org.springframework.data.redis.connection.RedisPassword">
        
        <constructor-arg index="0" value="${redis.password}"/>
    bean>

    
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>

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

/**
 * 

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

* * @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); } /** *

* 获取分布式锁 *

* * @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; } /** *

* 释放redis锁 *

* * @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 功能测试

/**
     * 

* 分布式锁测试 *

* * @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 添加依赖


<dependency>
    <groupId>org.springframework.datagroupId>
    <artifactId>spring-data-redisartifactId>
    <version>2.1.10.RELEASEversion>
dependency>


<dependency>
    <groupId>io.lettucegroupId>
    <artifactId>lettuce-coreartifactId>
    <version>5.2.2.RELEASEversion>
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配置】


<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"/>

    
    <context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/>

    
    <bean id="lettuceConnectionFactory"
          class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
        
        <constructor-arg index="0" ref="redisStandaloneConfiguration"/>
    bean>

    
    <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}"/>
        
        <property name="password" ref="redisPassword"/>
    bean>

    
    <bean id="redisPassword" class="org.springframework.data.redis.connection.RedisPassword">
        
        <constructor-arg index="0" value="${redis.password}"/>
    bean>

    
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>

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

/**
 * 

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

* * @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); } /** *

* 获取分布式锁 *

* * @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; } /** *

* 释放redis锁 *

* * @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 功能测试

/**
     * 

* 分布式锁测试 *

* * @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 添加依赖


<dependency>
    <groupId>org.springframework.datagroupId>
    <artifactId>spring-data-redisartifactId>
    <version>2.1.10.RELEASEversion>
dependency>


<dependency>
	<groupId>org.redissongroupId>
	<artifactId>redissonartifactId>
	<version>3.12.5version>
dependency>

5.2 相关配置

【redis配置】

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

【Spring配置】


<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:client>
        <redisson:single-server address="redis://localhost:6379" password="123456"/>
    redisson:client>

beans>

5.3 实现分布式锁

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

/**
     * 

* 分布式锁测试 *

* * @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(); } } }

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

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

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

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

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

你可能感兴趣的:(分布式专题,Redis,redis,java,分布式)