分布式锁入门

分布式锁

当应用不再是单机情况,单纯的加锁已经不能保证数据安全了,所以要使用分布式锁。

最基础的分布式锁: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);//加锁并设置过期时间

注意:这种情况避免了大部分问题,但是还存在一个比较极端的问题。

  1. 比如A应用、B应用都在调用这个锁,A应用执行的很慢,在还没有删除锁的时候,但是过期时间到了,那么A的锁就过期了。
  2. B应用重新获取了这个锁,B应用还没执行完毕的时候,A应用执行完毕了,执行了锁操作,此时A删除的锁就是B的锁。
  3. 这样此时如果有一个应用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。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值