正常高并用Synchronized(this){}进程同步锁就够了,但是他不能解决分布式问题。只适用于单个服务。
分布式锁可以用setnx 将key的值设为value, setnx的作用key不存在,若给定的key已经存在,则setnx不做任何动作 setnx是set if not exists的简写 可以通过这个简单的实现一个分布式锁。
stringRedisTemplate.opsForValue().seIfAbsent(“lockKye”,”aa”) 相当于jedis,setnx(k,v)
Boolean result=stringRedisTemplate.opsForValue().seIfAbsent(“lockKye”,”aa”) //jedis,setnx(k,v)
不要忘了再用完之后释放,这就是一个简单的分布式锁。
如果代码运行出了问题的话,不能释放锁,这时候就需要加入try-catch。
加入再代码执行到一半的时候系统宕机了你的所没有释放,这样你也释放不了了你是不是可以加一个过期时间来规避宕机的问题呢。
stringRedisTemplate.opsForValue().seIfAbsent(“lockKye”,”aa”,10,TimeUnit.SRCONDS)
/保证原子性,这个也存在问题再高并发的情况下容易造成被其它线程释放锁。
如果再执行过程中执行时间过长,你的锁失效了呢,其它的线程就会进入最终可能导致的结果是你当前释放的锁的线程不是你自己的锁而是其它线程的 所以优化改进随机一个值加入到redis中再释放锁的时候判断是不是当前的锁。在进行要不要释放。优化的代码
stringRedisTemplate.opsForValue().seIfAbsent(“lockKye”,”加入的值”,10,TimeUnit.SRCONDS)
if(threadId.equals("加入的值")){
stringRedisTemplate.delete("lockKEy")
}
因为下边的if判断和释放不是原子性,所以这种方法你最终还是会发现还是会存在当您在今日判断之后系统突然卡顿了其它线程进来了可能释放其它线程的锁的情况,优化方法可以使用lua脚本
---Lua脚本
---比较线程标识与锁种的标识是否一致
if(redis.call('get',KEY[1]== ARGV[1]) then
---释放锁 del key
return redis.call('del',KEYS[1])
)
end
return 0
//上边lua脚本执行 和所需的参数
stringRedisTemplate.execute(
锁名,Collections.singletonList(redis的key),
锁的标识
)
还有有可能锁过期你还是会 释放其它线程的锁,并且没有解决这种情况
基于setnx实现的分布式锁存在下面的问题:
1、不可重入:同一线程无法多次获取同一把锁
2、不可重试:获取锁只尝试一次就返回false,没有重试机制
3、超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致所释放,存在安全隐患
4、主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
这时就引入了一个成熟的框架redisson:
redisson入门
1.引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
<dependency>
2.配置Redisson客户端:
@Configuration
public Class RedisConfig{
@Bean
public RedissonClient redissonClient(){
//配置类
Config config = new Config();
//添加redis地址,这里添加了单点的地址,也可以使用config.userClusterServers()添加集群地 址
config.useSingleServer().setAddress("你的地址").setPassowrd("密码");
//创建客户端
return Redisson.create(config);
}
}
3.使用Redisson的分布式锁
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterrupedException{
//获取锁(可重入),指定锁的名称
RLock lock =redissonClient.getLock("Lock");
//尝试获取锁,参数分别时:获取锁的最大等待时间(期间会重试),锁自动施放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断释放获取成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
Redisson可重入锁原理
----获取锁的脚本
local key= KEYS[1]; ---锁的key
local threadId = ARGV[1]; ---线程唯一标识
local releaseTime = ARGV[2]; ----锁的自动释放时间
----判断是否存在
if(redis.call('exists',key)==0) then
---不存在,获取锁
redis.call('hset',key,releaseTime);
---设置有效期
redis.call('expire',key,releaseTime);
return 1; --返回结果
end;
---锁已经存在,判断threadId是否是自己
if(redis.call('hexists',key,threadId)==1) then
--不存在,获取锁,重置次数+1
redis.call('hincrby',key,theadId,'1');
--设置有效期
redis.call('expire',key,releaseTime);
return 1;---返回结果
end;
return 0; ---代码走到这里,说明获取锁的不是自己,获取失败
KEYS[1] 用来表示在redis 中用作键值的参数占位,主要用來传递在redis 中用作keyz值的参数。
ARGV[1] 用来表示在redis 中用作参数的占位,主要用来传递在redis中用做 value值的参数。
----释放锁的脚本
local key= KEYS[1]; ---锁的key
local threadId = ARGV[1]; ---线程唯一标识
local releaseTime = ARGV[2]; ----锁的自动释放时间
----判断当前锁是否持有自己的锁
if(redis.call('HEXISTS',key,threadId)==0) then
return nil; ---如果已经不是自己,则直接返回
end;
----是自己的锁,则冲入次数-1
local count = redis.call('HINCRBY',key,threadId,-1);
---判断是否重入次数是否已经为0
if(count>0) then
----大于0说明不能释放锁,重置有效期然后返回
redis.call('EXPIRE',key,releaseTime);
return nil;
else ---等于0说明可以释放锁,直接删除
redis.call('DEL',key)
return nil;
end;
核心:就是利用一个hash线程记录锁的线程和次数;
Redisson分布式锁原理
总结Redisson分布式锁原理:
可重入:利用hash结构记录线程id和重入次数
可重试:利用信号量和Pub功能实现等待、唤醒、获取锁失败的重试机制
超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间
Redisson分布式锁主从一致性问题:
创建多个独立的redis来解决主从一致性的问题
配置redisson连锁:
@Configuration
public Class RedisConfig{
@Bean
public RedissonClient redissonClient(){
//配置类
Config config = new Config();
//添加redis地址,这里添加了单点的地址,也可以使用config.userClusterServers()添加集群地 址
config.useSingleServer().setAddress("你的地址").setPassowrd("密码");
//创建客户端
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient2(){
//配置类
Config config = new Config();
//添加redis地址,这里添加了单点的地址,也可以使用config.userClusterServers()添加集群地 址
config.useSingleServer().setAddress("你的地址").setPassowrd("密码");
//创建客户端
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient3(){
//配置类
Config config = new Config();
//添加redis地址,这里添加了单点的地址,也可以使用config.userClusterServers()添加集群地 址
config.useSingleServer().setAddress("你的地址").setPassowrd("密码");
//创建客户端
return Redisson.create(config);
}
}
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;
@BeforeEach
void setUp(){
RLock lock1 = redissonClient.getLock("order");
RLock lock2 = redissonClient2.getLock("ordder");
RLock lock3 = redissonClient3.getLock("ordder");
//创建联锁 multiLock
lock =redissonClient.getMultiLock(lock1,lock2,lock3);
}
@Test
void testRedisson() throws InterrupedException{
//尝试获取锁,参数分别时:获取锁的最大等待时间(期间会重试),锁自动施放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断释放获取成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
总结:
1.不可以重入Redis分布式锁:
原理 :利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
缺陷:不可重入、无法重试、锁超时失效
2.可重入的Redis分布式锁:
原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
缺陷:redis宕机引起锁失效问题
3.Redisson的multiLock:
原理;多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功‘
缺陷:运维成本高,实现复杂
这是时候就要涉及到另一个优化方案了,锁续命,通过增加锁的时间来带实现高并发的一致性
引入一个框架
Redisson
RLock redissonLock =redisson.getLock(lodcKey)
Try中写redissonLock.lock();
Finally中redissonLock.unLock()释放锁
在性能上还可以优化用分段锁 ,将原本很多的数据分批进行处理。