Redis实现锁机制的方式大致有两种,第一种是借助setnx命令实现锁机制,第二种是通过lua脚本实现锁机制。
RedisConfig类:
package com.fly.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.*;
@Configuration
public class RedisConfiguration {
private StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
private GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer=new GenericJackson2JsonRedisSerializer();
@Bean
public RedisConnectionFactory redisConnectionFactory(){
RedisStandaloneConfiguration config=new RedisStandaloneConfiguration();
config.setHostName("172.16.178.128");
config.setPort(6379);
config.setPassword("123456");
LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
return factory;
}
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
/**
* 设置key采用String序列化方式,Redis存取默认使用JdkSerializationRedisSerializer序列化,
* 这种序列化会key的前缀添加奇怪的字符,例如\xac\xed\x00\x05t\x00user_id,
* 使用StringRedisSerializer序列化可以去掉这种字符
*/
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
/*
* 开启Redis事务,默认是关闭的。也可以手动开启事务,
* 通过template.multi()开启事务,template.exec()关闭事务
*/
template.setEnableTransactionSupport(true);
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory){
StringRedisTemplate template=new StringRedisTemplate();
template.setConnectionFactory(factory);
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
template.setEnableTransactionSupport(true);
return template;
}
/**
* 注入Redis消息监听器
* @param connectionFactory
* @return
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
#setnx命令复习
set k1 zxp
setnx k1 111 #返回nil,setnx设置的key已存在
setnx k2 222 #返回OK
1.基于setnx命令实现分布式锁(写法1)
Redis实现锁机制的原理是依靠setnx命令,setnx在设置key时只有当key不存在时才会设置成功,否则会设置key失败。而锁机制的特性就是互斥,同一时间只能拥有一把锁,其他资源则等待已经获取锁的资源释放锁,且获取锁的资源能重新获取锁,即可得出锁具有互斥性和可重入性的。
把思维转变过来,我们可以把setnx设置key看作为加锁,当setnx设置key成功时就说明加锁成功,当setnx设置key失败时可以看做加锁失败,可能有其他资源在持有锁,那么剩余的资源都需要等待锁。锁应该具有瞬时性,不然一个资源可以一直持有一个锁,则会导致其他资源一直处于等待获取锁状态,这显然是不友好的,所以锁应该具有超时时间,过了超时时间锁会自动被释放掉,通过Redis key的过期时间机制非常容易实现这个功能。
具体流程如下:
(1).加锁时使用setnx命令设置一个key,key对应的value值为过期时间,过期时间为当前时间+默认的过期时间,这个过期时间也就是锁的过期时间。
(2).若setnx命令设置key成功则说明加锁成功。
(3).若setnx命令设置key失败说明Redis中可能已经存在对应key了,表示加锁失败。
(4).如果Redis存在key且对应value不为空,而且这个key已过期,这说明一直持有锁没有释放掉,所以需要重新上锁,防止死锁,避免多个线程抢锁。
package com.fly.lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 使用setnx命令实现分布式锁机制写法1。
* 核心原理是通过setnx命令实现加锁动作,通过delete命令实现解锁动作。
* 优点:轻量级、性能好。
* 缺点:
* (1).delete命令存在误删除非当前线程持有的锁的可能
* (2).不支持阻塞等待、不可重入
*/
@Component
public class BaseLock01 {
/**
* 分布式锁前缀
*/
private static final String LOCK_PREFIX="redis_lock";
/**
* 分布式锁过期时间,默认300ms
*/
private static final Long LOCK_EXPIRE = 300L;
@Autowired
RedisTemplate<String,Object> redisTemplate;
/**
* 加锁
* @param k
* @return true为加锁成功,false为加锁失败
*/
public boolean tryLock(String k){
String keyName=LOCK_EXPIRE+'_'+k;
Long expire=LOCK_EXPIRE+System.currentTimeMillis();
/**
* setIfAbsent 对标setnx命令,若设置key在Redis不存在则设置成功,
* 否则设置失败。
*/
Boolean result=redisTemplate.opsForValue().setIfAbsent(keyName,expire);
if(result){
return true;
}
/**
* 若Redis存在当前设置的key则获取到key对应的值
*/
Long value =(Long) redisTemplate.opsForValue().get(keyName);
/**
* 如果key(锁)过期,此时可能会出现死锁,一个线程长期持有锁却未释放掉,
* 需要重新加锁。下面代码有防止死锁、避免多个线程抢锁的作用。
*/
if(null!=value && value < System.currentTimeMillis()){
/**
* 重新加锁,getAndSet对标getset命令,获取key对应的value并设置key的value值
*/
Object o=redisTemplate.opsForValue().getAndSet(keyName,value+System.currentTimeMillis());
//判断重新加锁是否成功
if(Objects.nonNull(o) && o.equals(value)){
return true;
}
}
return false;
}
/**
* 解锁
*/
public Boolean unLock(String k){
String keyName=LOCK_PREFIX+"_"+k;
return redisTemplate.delete(keyName);
}
}
2.基于setnx命令实现分布式锁(写法2)
package com.fly.lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 使用setnx命令实现分布式锁机制写法2。
*/
@Component
public class BaseLock02 {
/**
* 分布式锁前缀
*/
private static final String LOCK_PREFIX="redis_lock";
/**
* 分布式锁过期时间,默认300ms
*/
private static final Long LOCK_EXPIRE = 100L;
private RedisTemplate<String,Object> redisTemplate;
@Autowired
public BaseLock02(RedisTemplate<String,Object> redisTemplate){
this.redisTemplate=redisTemplate;
}
/**
* 加锁
* @return
*/
public Boolean tryLock(String k){
/*key的全名称*/
final byte[] keyName=(LOCK_PREFIX +'_'+k).getBytes();
/*key过期时间,也是key对应的value值*/
final Long expire=LOCK_EXPIRE+System.currentTimeMillis();
/**
* conn是一个RedisConnection对象,RedisConnection对象提供了通过二进制的方式操作Redis
*/
redisTemplate.execute((RedisCallback) conn->{
//setNX()对标setnx命令
Boolean acquire = conn.setNX(keyName, String.valueOf(expire).getBytes());
if(acquire) return true;
/**
* 加锁失败说明Redis中已存在设置key
*/
final byte[] value = conn.get(keyName);
if(Objects.nonNull(value) && value.length > 0){
//获取过期时间
Long expireTime = Long.parseLong(new String(value));
//如果锁已经过期,则需要重新加锁,防止死锁、避免多个线程抢锁
if(expireTime < System.currentTimeMillis()){
byte[] oldValue = conn.getSet(keyName,String.valueOf(expireTime+System.currentTimeMillis()).getBytes());
//判断是否加锁成功
return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
}
}
return false;
});
return false;
}
/**
* 解锁
*/
public Boolean unLock(String k){
String keyName=LOCK_PREFIX+"_"+k;
return redisTemplate.delete(keyName);
}
}