目录
🧂1.锁的类型
🌭2.基于redis实现分布式
🥓3. 基于redisson实现分布式锁
1.锁的类型
- 1.本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
- 2.分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper, Mysql等都可以
2.基于redis实现分布式锁
1.加锁 setnx key value
- setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
- 如果 key 不存在,则设置当前 key 成功,返回 1;
- 如果当前 key 已经存在,则设置当前 key 失败,返回 0
2.解锁 del(key)
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)
3.配置锁超时 expire(key,30)
客户端崩溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放
4.代码
正常的加锁逻辑,但存在问题
public void coupon(){
String key="coupon_id";
if (setnx(key,1)==1){
expire(key,30, TimeUnit.MILLISECONDS)
try{
//业务
}finally {
del(key);
}
}else {
//睡眠,然后自旋调用
coupon();
}
}
5.存在的问题
- 1.问题一:
- 多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,则这个资源就是死锁但是expire失败,且宕机了,则这个资源就是死锁。
- 2.解决:
- 使用原子命令:同时设置和配置过期时间 setnx / setex
- redisTemplate.opsForValue().setIfAbsent(key, v: "lock", l: 30,TimeUnit.SECoNDS);
- 1.问题二 :
- 业务超时,存在其他线程勿删,key 30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁
- 2.解决:
- 可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁,那 value 应该是存当前线程的标识或者uuid
- 3.但是,删除锁时也不是原子性操作。
- 当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调用del方法,结果就是删除了新设置的线程B的值
- 核心还是判断和删除命令 不是原子性操作导致
6.使用lua脚本
- 问题:加锁使用setnx setex可以保证原子性,那解锁使用判断和删除怎么保证原子性 ????
- 答:多个命令的原子性:采用lua脚本+redis,由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
- 使用redis+lua分布式锁,正常严谨的逻辑
redisTemplate.execute返回值不必须是
long
类型,但你必须确保 Redis 脚本返回的数据与你在DefaultRedisScript
中指定的类型相匹配。
public class Main {
@Autowired
StringRedisTemplate redisTemplate;
/**
* 分布式锁-redis
*/
@Test
public void test() {
//根据业务,动态获取key
String key = "coupon_id";
//随机生成uuid,作为value值
String uuid = CommonUtil.generateUUID();
//设置key的同时,设置过期时间;获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, uuid, 30, TimeUnit.MILLISECONDS);
//判断是否加锁成功
if (lock) {
//枷锁成功
try {
//执行业务
} finally {
//释放锁,使用lua脚本保持原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//执行lua脚本
Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), uuid);
}
} else {
//睡眠一定时间
//加锁失败,自旋
test();
}
}
}
- 锁的过期时间,如何实现锁的自动过期 或者 避免业务执行时间过长,锁过期了?
- 答:一般把锁的过期时间设置久一点,比如10分钟时间
3. 基于redisson实现分布式锁
1.添加依赖
<!--分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
2.添加redissonClient
如果报错的话,查看java版本是否正确
@Configuration
@Data
public class RedissonConfig {
//从配置文件获取redis的相关信息
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
/**
* 使用redisson作为分布式锁
* @return
*/
@Bean
public RedissonClient redissonClient() {
//1.创建配置对象
Config config = new Config();
//2.连接redis
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
//3,创建redissonClient对象
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
3.实现分布式锁
myLock.lock()如果不指定过期时间,则默认30s后过期
@Autowired
private RedissonClient redissonClient;
@GetMapping("/lock")
public JsonData test() {
//1.获取锁
RLock myLock = redissonClient.getLock("my_lock");
//2.手动加锁
myLock.lock();
try {
//3.业务实现
System.out.println("加锁成功~" + Thread.currentThread().getName());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//4.手动解锁
myLock.unlock();
System.out.println("解锁成功~" + Thread.currentThread().getName());
}
return JsonData.buildSuccess();
}
4.看门狗机制
Redis锁的过期时间小于业务的执行时间该如何续期?
- 为了避免这种情况的发生, Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
- Redisson中客户端一旦加锁成功,就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。
- 1.指定加锁时间
myLock.lock(10, TimeUnit.SECONDS)
- 指定加锁时间后,默认10秒后过期,并且没有看门狗机制
- 2.不指定加锁时间
myLock.lock(10, TimeUnit.SECONDS)
- 默认30秒后过期,存在看门狗机制
- 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔(【LockWatchingTimeOut看门狗默认时间】/3)这么长时间自动续期;