点击上方蓝字关注我们!
场景升级为分布式所带来的问题
众所周知,在单体服务中处理高并发场景时,可以使用java提供的两种内置的锁来保证数据的一致性。
例如:
业界目前的第三方系统的实现方案有数据库锁、Redis分布式锁和ZooKeeper分布式锁,各个方案各有优缺点。其中Redis的使用范围最广,相关文章也最杂乱,随着Redis的不断演化,其中不少文章已经不具备时效性了。所以本文将重点梳理Redis分布式锁的演化过程,让你从头到尾的了解Redis分布式锁。
Redis分布式锁的演化过程
01
基于setNX实现
setNX是Redis提供的一个原子操作,如果指定key存在,那么setNX失败,如果不存在会进行Set操作并返回成功。
if (setnx(key, 1) == 1){
expire(key, 30);
try {
//TODO 业务逻辑
} finally {
del(key);
}
}
存在问题:
由于获取锁和给锁设置超时时间是两步操作,如果获取锁成功后应用异常或者重启,锁将无法过期。
02
基于set实现
if (set(key, value, 30)){
try {
//TODO 业务逻辑
} finally {
del(key);
}
}
如果A线程拿到锁后,业务逻辑的执行超过30秒,A线程的锁会自动释放。此时B线程获取到了锁,开始执行业务逻辑,但是A线程中继续执行了删除锁的操作,此时会出现A线程误删除了B线程的锁。
03
基于set实现-引入UUID
String uuid = UUID.randomUUID().toString();
if (set(key, uuid, 30)){
try {
//TODO 业务逻辑
} finally {
if(get(key)==uuid){
del(key);
}
}
}
如果A线程正好已经判断完上锁的是当前线程,但是在删除锁之前正好A线程的锁过期导致锁自动释放了,此时B线程加锁成功,依然会出现A线程误删B线程锁的问题。根本原因就是判断是否为当前锁和删除锁是两个步骤,导致删除锁不具备原子性。
04
基于set实现-引入Lua脚本
```java
String uuid = UUID.randomUUID().toString();
if (set(key, uuid, 30)){
try {
//TODO 业务逻辑
} finally {
EVAL (
//LuaScript
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0 end
)
}
}
如果在Redis集群环境下,由于Redis集群数据同步为异步,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端A的线程A成功加锁,指令还未同步到从节点,此时如果主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端B的线程B加锁时就会成功加锁。
05
基于Redisson实现
RLock lock = redisson.getLock("foobar"); // 1.获得锁对象实例
lock.lock(); // 2.获取分布式锁
try {
//TODO 业务逻辑
} finally {
lock.unlock(); // 3.释放锁
}
这时会有细心的同学发现了,之前的写法依次增加了锁超时时间,加锁和设置超时的原子化,解锁的防误解锁和解锁的原子化操作,但是Redisson加解锁的写法从表面上是看不出来是否有这些特性。
接下来我们来剖析一下RedissonLock中的源码实现(基于redisson-spring-boot-starter的3.11.4版本):
@Override
public RLock getLock(String name) {
return new RedissonLock(this.connectionManager.getCommandExecutor(),name);
}
public RedissonLock(CommandAsyncExecutor commandExecutor,String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.id = commandExecutor.getConnectionManager().getId();
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = this.id + ":" + name;
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();}
通过以上三点,getLock()方法可以实现锁超时时间,加锁和设置超时的原子化。
public void unlock() {
try {
this.get(this.unlockAsync(Thread.currentThread().getId()));
} catch (RedisException var2) {
if (var2.getCause() instanceof IllegalMonitorStateException){
throw (IllegalMonitorStateException)var2.getCause();
} else {
throw var2;
}
}
}
其中unlockAsync()方法的源码:
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise();
RFuture<Boolean> future = this.unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
if (e != null) {
this.cancelExpirationRenewal(threadId);
result.tryFailure(e);
} else if (opStatus == null) {
IllegalMonitorStateException cause = new
IllegalMonitorStateException("attempt to unlock lock, not locked by current
thread by node id: " + this.id + " thread-id: " + threadId);
result.tryFailure(cause);
} else {
this.cancelExpirationRenewal(threadId);
result.trySuccess((Object)null);
}
});
return result;
}
通过以上三点,unlock()方法可以实现解锁的防误解锁和解锁的原子化操作。
public void lock() {
try {
this.lock(-1L, (TimeUnit)null, false);
} catch (InterruptedException var2) {
throw new IllegalStateException();
}
}
其中lock方法的源码:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
while(true) {
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return;
}
if (ttl >= 0L) {
try {
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException var13) {
if (interruptibly) {
throw var13;
}
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else if (interruptibly) {
this.getEntry(threadId).getLatch().acquire();
} else {
this.getEntry(threadId).getLatch().acquireUninterruptibly();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}
通过以上的源码解读,大家对于Redisson的功能强大应该有了初步了解。Redisson除了上面列出的基本的可重入锁之外,还提供了公平锁、联锁、红锁、读写锁、信号量等多种锁的方式,感兴趣的同学可以前往Redisson的github查看研究,希望本片内容可以帮助你在工作中更合理的选择和正确的使用分布式锁。
参考文献
[Redis 分布式锁进化史](https://my.oschina.net/u/3972077/blog/2873842)
[Redisson 分布式锁实现分析](https://www.jianshu.com/p/a8b3473f9c24)
[Redisson的github](https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8)