满足分布式系统或集群模式下 多进程可见并且互斥的锁。
特性:
SETNX lock thread1
# 添加锁, NX互斥,EX设置超时时间
SET lock thread1 NX EX 10
DEL lock
EXPIRE lock 5
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功; false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
/**
* 实现分布式锁
*/
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 尝试获取锁
*/
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
Long threadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent("lock:" + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁
*/
@Override
public void unlock() {
stringRedisTemplate.delete("lock:" + name);
}
}
// 创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//尝试获取锁
boolean isLock = lock.tryLock(1000);
//判断
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
log.error("不允许重复下单!");
return;
}
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent("lock:" + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁中的线程标识
String id = stringRedisTemplate.opsForValue().get("lock:" + name);
//判断是否与当前线程标识一致
if (threadId.equals(id)) {
//一致,释放锁
stringRedisTemplate.delete("lock:" + name);
}
//不一致,什么都不做
}
Redis提供了Lua脚本在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
redis.call('命令名称', 'key', '其他参数', ...)
# 示例
redis.call('set', 'name', 'allen')
EVAL "脚本" numberkeys key
# 示例
EVAL "return redis.call('set', 'name', 'allen')" 0
# 脚本中key、value作为参数传递
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name allen
@Override
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
return scriptExecutor.execute(script, keys, args);
}
初始化脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
//找到unlock.lua文件
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
可重入锁:同一个线程可以多次获取同一把锁
基于setnx实现的分布式锁存在以下问题:
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.6version>
dependency>
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissionClient(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6357").setPassword("123456");
return Redisson.create(config);
}
}
@Autowired
private RedissonClient redissonClient;
@Test
void testString() throws InterruptedException {
//创建锁对象
RLock lock = redissonClient.getLock("anyLock");
//获取锁
boolean b = lock.tryLock(1, 10, TimeUnit.SECONDS);
//判断是否成功
if (b) {
try {
System.err.println("执行任务");
} finally {
//释放锁
lock.unlock();
}
}
}
Redission分布式原理:
原理 | 缺陷 | |
---|---|---|
不可重入Redis分布式锁 | 利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标识 | 不可重入、无法重试、锁超时失效 |
可重入Redis分布式锁 | 利用hash结构,记录线程标识和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待 | redis宕机引起锁失效问题 |
Redis的multiLock | 多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功 | 运维 成本高、实现复杂 |