如何基于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

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形