分布式锁
当应用不再是单机情况,单纯的加锁已经不能保证数据安全了,所以要使用分布式锁。
最基础的分布式锁:Redis-SETNX
Version1.0
setnx key value
将key的值设置为value,当且仅当key不存在,如果key存在,那么不进行任何操作。
@Autowired
StringRedisTemplate stringRedisTemplate;
public String transaction() {
String lock = "LockKEY";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,"hzp");
//设置成功返回true,表示redis中不存在这个数据,也就是没有应用拿到这把锁
if(!result){
return "ERROR";//可以进行其他操作,比如业务繁忙,请稍后再试
}
//业务代码
//......
stringRedisTemplate.delete(lock);//应用执行完毕,删除记录,相当于释放锁资源
return "trans";
}
这样就实现了一个最基础的分布式锁
注意:这样实现最基础的分布式锁很可能造成死锁的情况,比如在应用执行完毕之前出现了异常,程序没有执行到删除记录的语句,那么redis里面就存在一条记录表示有一个应用已经获取了这把锁,但是实际上并没有这个应用存在,就会发生死锁。
Version2.0
因此进行优化,使用try-catch-finally进行捕获异常并且最终释放锁资源
try{
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,"hzp");
//设置成功返回true,表示redis中不存在这个数据,也就是没有应用拿到这把锁
if(!result){
return "ERROR";
}
//业务代码
//...
}catch (Exception e){
}finally {
stringRedisTemplate.delete(lock);//应用执行完毕,删除记录,相当于释放锁资源
}
注意:这样就解决了出现异常的情况,但是如果在finally语句执行之前,系统宕机或者停电那么finally内的语句就无法执行,记录就会被一直保存到redis中,又出现死锁问题。
Version3.0
既然记录可能无法被删除,那么我们可以通过设置过期时间来清除记录。
try{
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,"hzp");
stringRedisTemplate.expire(lock,10, TimeUnit.SECONDS);//设置过期时间
//设置成功返回true,表示redis中不存在这个数据,也就是没有应用拿到这把锁
if(!result){
return "ERROR";
}
//业务代码
//...
}catch (Exception e){
}finally {
stringRedisTemplate.delete(lock);//应用执行完毕,删除记录,相当于释放锁资源
}
但是这里有一个很容易出现问题,如果在设置过期时间之前遇到了停电,没有执行到过期时间的语句,还是会出现死锁的情况。所以要保证加锁和设置过期时间操作的原子性。
而serIfAbsent方法是可以有设置过期时间这个参数的,
Boolean setIfAbsent(K var1, V var2, long var3, TimeUnit var5);
我们直接将两行语句合并成一行语句
stringRedisTemplate.opsForValue().setIfAbsent(lock,"hzp",10,TimeUnit.SECONDS);//加锁并设置过期时间
注意:这种情况避免了大部分问题,但是还存在一个比较极端的问题。
- 比如A应用、B应用都在调用这个锁,A应用执行的很慢,在还没有删除锁的时候,但是过期时间到了,那么A的锁就过期了。
- B应用重新获取了这个锁,B应用还没执行完毕的时候,A应用执行完毕了,执行了锁操作,此时A删除的锁就是B的锁。
- 这样此时如果有一个应用C,C就会重新获取锁,然后B执行完毕又删除了C获取的锁,以此类推,后面的锁都失效了,应用还没执行完毕就有新应用可以获取到锁。
出问题的原因就在于我加的锁,可能会被另一个应用删除,所以要进行优化,让我的锁只能被我删除。
String lock = "LockKEY";
String clientId = UUID.randomUUID().toString();//利用UUID为当前应用创建一个唯一标识
try{
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,clientId,10,TimeUnit.SECONDS);//将唯一标识存入锁的value中
//设置成功返回true,表示redis中不存在这个数据,也就是没有应用拿到这把锁
if(!result){
return "ERROR";
}
//业务代码
//...
}catch (Exception e){
}finally {
if(clientId.equals(stringRedisTemplate.opsForValue().get(lock))){//判断现在redis中的是不是我自己的那把锁
stringRedisTemplate.delete(lock);//应用执行完毕,删除记录,相当于释放锁资源
}
}
这样最基础的分布式锁就已经比较完善了,但是在过期时间的设置上难以控制,设置长了影响效率,设置短了又无法控制并发问题,此时就需要引入Redisson。
Redisson
先引入依赖
<!-- 分布式锁Redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.18.0</version>
</dependency>
springboot启动类设置初始代码
@Bean
public Redisson redisson(){
//单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://39.108.213.211:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
注入Redisson
@Autowired
Redisson redisson;
修改代码
String lock = "LockKEY";
String clientId = UUID.randomUUID().toString();
RLock rLock = redisson.getLock(lock);//创建一个RLock对象
try{
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,clientId,10,TimeUnit.SECONDS);
rLock.lock();//对我们redis中的锁进行Redisson加锁
//设置成功返回true,表示redis中不存在这个数据,也就是没有应用拿到这把锁
if(!result){
return "ERROR";
}
//业务代码
//...
}catch (Exception e){
}finally {
rLock.unlock();//释放锁
if(clientId.equals(stringRedisTemplate.opsForValue().get(lock))){//判断现在redis中的是不是我自己的那把锁
stringRedisTemplate.delete(lock);//应用执行完毕,删除记录,相当于释放锁资源
}
}
Redisson进行lock的作用就是当获取锁之后,会定期检查(一般是过期时间的1/3)应用是否还持有锁,如果有就延长锁的过期时间。
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}
Redisson源码使用Lua脚本进行控制,Lua脚本是具备原子性的,它对redis的操作相当于一条语句,也就是虽然这里的脚本有很多句,但是它们实际上执行相当于一条语句。
this.lockWatchdogTimeout = 30000L;//默认时间,package org.redisson.config下的Config类设置了默认值
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();//package org.redisson;中的RedissonBaseLock类中默认时间赋值给类中的属性internalLockLeaseTime
this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);//延时任务,30000毫秒/3也就是30s/3执行一次
到目前为止,通过Redisson已经解决了并发问题,但是如果在使用redis集群情况下,如果Master主节点在极端情况下突然出问题了,并且没有来得及同步key到Slave从节点中,此时Slave就会变为Master,但是它并没有原本的key,假如有一个应用已经获取了锁,但是当新的应用获取值时,就会认为没有应用得到这把锁,就会出现并发问题。
这种极端情况可以通过zookeeper来解决,因为Redis集群满足的是AP,zookeeper满足的是CP。
CAP原则:CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
Redis也可以通过RedLock解决上述问题,但是还是不如zookeeper。