本文主要是介绍如何基于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实现分布式锁,详细教程拿走不送~的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!