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

2024-01-12 19:48

本文主要是介绍如何基于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时,直接可以设置超时时间

<!-- 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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://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;@Autowiredprivate 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,如果没有该值,就返回-2Long 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锁的keyString 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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://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不存在的时候返回-2Long 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锁的keyString 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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://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不存在的时候返回-2Long 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锁的keyString 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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://redisson.org/schema/redissonhttp://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();}}}

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

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

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

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

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

这篇关于如何基于Redis实现分布式锁,详细教程拿走不送~的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/598971

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

将Mybatis升级为Mybatis-Plus的详细过程

《将Mybatis升级为Mybatis-Plus的详细过程》本文详细介绍了在若依管理系统(v3.8.8)中将MyBatis升级为MyBatis-Plus的过程,旨在提升开发效率,通过本文,开发者可实现... 目录说明流程增加依赖修改配置文件注释掉MyBATisConfig里面的Bean代码生成使用IDEA生

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

Linux系统中卸载与安装JDK的详细教程

《Linux系统中卸载与安装JDK的详细教程》本文详细介绍了如何在Linux系统中通过Xshell和Xftp工具连接与传输文件,然后进行JDK的安装与卸载,安装步骤包括连接Linux、传输JDK安装包... 目录1、卸载1.1 linux删除自带的JDK1.2 Linux上卸载自己安装的JDK2、安装2.1

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同