黑马点评分布式锁篇
摘要:Synchronized在集群部署时会失效,需采用分布式锁。基于Redis的分布式锁实现核心是互斥性、安全释放和防误删。通过线程标识+UUID确保锁归属,并利用Lua脚本保证原子性操作。针对原方案存在的不可重入、不可重试等问题,推荐使用Redisson框架,其内置看门狗机制自动续期,通过RLock接口简化锁操作,底层采用Lua脚本保证主从一致性。关键实现包括:1)SETNX+过期时间获取锁;
synchronized一个jvm只有一个锁监视器 当tomact集群部署的话,锁监视器synchronized会失效,所以集群部署采用分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
(多进程可见,互斥,高可用,高性能,安全性)
基于redis的分布式锁
获取锁
互斥:确保只能有一个线程获取锁
释放锁:手动释放
超时释放
方案一 我们需要在redis钟利用set key value ex time nx 建立分布式锁
public boolean tryLock(long timeoutSec) {
//获取线程的id
String id =Thread.currentThread().getId();
//获取锁
Boolean success= stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);//自动拆箱防止空指针
}
如果redis中已经存在直接return false 这样多个集群只有这一个锁
然后释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
但是如图所示

加入线程一获取锁但是业务阻塞,此时redis锁因为超时释放锁,正好线程二乘虚而入,获取到锁了,但是显线程一业务完成了,再次执行释放锁这个任务,此时释放的就是线程二的锁,所以我们要在原来的基础上进行value的判断,我们需要进行判断是否是自己的线程,如果是就删除,不是就不用管
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
String id =ID_PREFIX+Thread.currentThread().getId();
这样去定义value可以增强value的复杂度,避免两个jvm中线程id正好相等导致误删
在获取锁时存入线程标识(UUID) 一致释放,不一致不释放
采用的是非阻塞机制
这是最终的实现代码unlock中
@Override
public void unLock() {
//获取线程标识
String threadId=ID_PREFIX+Thread.currentThread().getId(); //获取锁中的标识
String s= stringRedisTemplate.opsForValue().get(KEY_PREFIX+name); //判断表示是否一致
//进行释放锁
if(s.equals(threadId)) {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
但是没有确保原子一致性,当线程一判断是属于自己的id后,此时jvm垃圾回收导致线程阻塞,此时线程二获取锁,业务没有执行完,线程一开始释放锁了,就会导致发生误删
我们可以采用lua脚本的形式进行删除,在确保valu相同的同时还进行释放锁
具体代码如下
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
private StringRedisTemplate stringRedisTemplate;
private static final DefaultRedisScript UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT =new DefaultRedisScript();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate,String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name=name;
}
private String name;
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
private static final String KEY_PREFIX="lock:";
@Override
public boolean tryLock(long timeoutSec) {
//获取线程的id
String id =ID_PREFIX+Thread.currentThread().getId();
//获取锁
Boolean success= stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);//自动拆箱防止空指针
}
@Override
public void unLock() {
//使用lua脚本进行删除
stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name),ID_PREFIX+Thread.currentThread().getId());
}
// @Override
// public void unLock() {
// //获取线程标识
// String threadId=ID_PREFIX+Thread.currentThread().getId();
// //获取锁中的标识
// String s= stringRedisTemplate.opsForValue().get(KEY_PREFIX+name);
// //判断表示是否一致
// //进行释放锁
// if(s.equals(threadId)) {
// stringRedisTemplate.delete(KEY_PREFIX + name);
// }
// }
}
--对其进行比较
if(redis.call('get',KEYS[1]==ARGV[1]))then
return redis.call('del',KEYS[1])
end
return 0
分布式锁的优化
使用redisson
代码中redis分布式锁中主要有几个不足的地方1,不可重入2,不可重试3,超时释放导致的安全问题4,主从一致性


可重试锁原理

然后构建redisson来进行trylock和unlock
package com.hmdp.config;
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 RedissonConfig {
@Bean
public RedissonClient redissonClient(){
//配置
Config config = new Config();
config.useSingleServer().setAddress("redis://.....:6379");
//创建redissonClient对象
return Redisson.create(config);
}
}
然后直接用,因为redisson释放锁和获取底层都是用lua代码进行写的,所以也确保了redis和mysql原子一致性
//创建锁对象
//SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
RLock lock = redissonClient.getLock("lock:order:" + userId);
//获取锁
// boolean isLock = simpleRedisLock.tryLock(1200);
boolean isLock = lock.tryLock();
//判断是否成功
if(!isLock){
//获取锁失败 返回错误 或者 进行重试
return Result.fail("一个人不允许重复下单");
}
try {
//获取动态代理对象
IVoucherOrderService o = (IVoucherOrderService) AopContext.currentProxy();
//因为直接调用是this目标对象,而不是动态代理对象 我们需要得到动态代理对象 事务提交是由spring进行管理的
return o.createVoucherOrder(voucherId);
}finally {
//进行释放锁
// simpleRedisLock.unLock();
lock.unlock();//看门狗机制 30s
}
更多推荐

所有评论(0)