用setnx实现一个分布式锁
简介
利用Redis的单线程特性,在多个Redis客户端同时通过SETNX命令尝试获取锁,如果返回1表示获取锁成功,否则表示获取锁失败。
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0 。
因为Redis的单线程机制,所以可以保证只会有一个客户端成功获取到锁,而其他客户端则会失败。如果获取锁成功,则设置一个过期时间,防止该客户端挂了之后一直持有该锁。客户端释放锁的时候,需要先判断该锁是否仍然属于该客户端,如果是,则通过DEL命令释放锁。
实现
1.添加pom.xml
<!-- redis相关依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
2.添加配置文件
redis:
host: localhost
port: 6379
jedis:
pool:
max-active: 8 # 连接池最大连接数(使用Jedis时)
max-wait: -1ms # 连接池最大阻塞等待时间(使用Jedis时)
max-idle: 8 # 连接池中的最大空闲连接(使用Jedis时)
min-idle: 0 # 连接池中的最小空闲连接(使用Jedis时)
3.编写redisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 key 值
template.setKeySerializer(new StringRedisSerializer());
// 使用 JdkSerializationRedisSerializer 来序列化和反序列化 value 值
// 也可以自定义序列化器
template.setValueSerializer(new GenericToStringSerializer<>(Object.class));
template.afterPropertiesSet();
return template;
}
}
4.编写加锁与解锁
@Slf4j
@Component
public class RedisDistributedLock {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
private final JedisPool jedisPool;
public RedisDistributedLock() {
log.info("host:{},port:{}", host, port);
JedisPool jedisPool1 = new JedisPool(host, port);
this.jedisPool = jedisPool1;
}
/**
* 加锁
* @param lockKey
* @param requestId
* @param expireTime
* @return
*/
public boolean tryLock(String lockKey, String requestId, int expireTime) {
try (Jedis jedis = jedisPool.getResource()) {
String result = getSet(lockKey, requestId, expireTime, jedis);
return "OK".equals(result);
}
}
private static String getSet(String lockKey, String requestId, int expireTime, Jedis jedis) {
if (jedis.exists(lockKey)) {
return "False";
}else {
return jedis.setex(lockKey,expireTime,requestId);
}
}
/**
* 解锁
* @param lockKey
* @param requestId
* @return
*/
public boolean unlock(String lockKey, String requestId) {
try (Jedis jedis = jedisPool.getResource()) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return Long.parseLong(result.toString()) == 1L;
}
}
}
tryLock方法接收三个参数,分别是锁的键值lockKey、加锁的请求标识requestId和锁的过期时间expireTime。该方法会尝试使用Redis的set命令加锁,如果加锁成功则返回true,否则返回false。其中NX表示只在锁的键不存在时设置锁,PX表示锁的过期时间为expireTime毫秒。
SETNX命令自身是不支持设置超时时间的,一般是结合EXPIRE一起使用,常见用法:
SETNX key value
EXPIRE key 10或者:
SET key value EX 10 NX
unlock方法接收两个参数,分别是锁的键值lockKey和加锁的请求标识requestId。该方法会执行一个Lua脚本,判断当前锁的值是否等于请求标识requestId,如果是则删除锁并返回true,否则返回false。该方法使用eval命令执行Lua脚本,传入锁的键值和请求标识两个参数,返回值是执行结果。
测试
@Test
public void demo(){
Order order = new Order();
order.setId(snowflakeIdWorker.nextId());
order.setName("测试");
order.setNum(0L);
for(int i=0;i<1000;i++){
if(i%2==0){
order.setNum(order.getNum()-i);
}else{
order.setNum(order.getNum()+i);
}
//模拟高并发情况下下发订单
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
log.info("下单中,订单号:{}",order.getId());
//下单
orderServiceimpl.update(order);
log.info("下单完成,订单号:{}",order.getId());
}
});
}
}
测试结果
总结
优点
(1)实现简单:SETNX命令实现简单,易于理解和使用。
(2)性能较高:由于SETNX命令的执行原子性,保证了分布式锁的正确性,而且在Redis中,SETNX命令是单线程执行的,所以性能较高。
缺点
(1)锁无法续期:如果加锁方在加锁后的执行时间较长,而锁的超时时间设置的较短,可能导致锁被误释放。
(2)无法避免死锁:如果加锁方在加锁后未能及时解锁(也未设置超时时间),且该客户端崩溃,可能导致死锁。
(3)存在竞争:由于SETNX命令是对Key的操作,所以在高并发情况下,多个客户端之间仍可能存在竞争,从而影响性能。
单线程执行的,所以性能较高。
缺点
(1)锁无法续期:如果加锁方在加锁后的执行时间较长,而锁的超时时间设置的较短,可能导致锁被误释放。
(2)无法避免死锁:如果加锁方在加锁后未能及时解锁(也未设置超时时间),且该客户端崩溃,可能导致死锁。
(3)存在竞争:由于SETNX命令是对Key的操作,所以在高并发情况下,多个客户端之间仍可能存在竞争,从而影响性能。
(4)setnx不支持可重入,可以借助redission封装的能力实现可重入锁。