Redis分散式實現原理

程式設計師Forlan發表於2022-04-03

 

一、使用

1、pom.xml匯入依賴

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.integration</groupId>
	<artifactId>spring-integration-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis分散式實現原理

2、配置檔案

@Configuration
public class RedissonConfig {

	// 自定義在yml或properties檔案中
	@Value("${spring.redis.host}")
	private String host;

	@Value("${spring.redis.port}")
	private String port;

	@Value("${spring.redis.password}")
	private String password;

	@Bean
	public RedissonClient getRedisson() {
		Config config = new Config();
		if (StringUtils.isBlank(password)) {
			config.useSingleServer().setAddress("redis://" + host + ":" + port);
		} else {
			config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
		}
		return Redisson.create(config);
	}

}
Redis分散式實現原理

3、使用類

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.redis.util.RedisLockRegistry;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * @author Forlan
 * @since 2022-04-03
 */
public class ForlanTest {

	private static final Logger logger = LoggerFactory.getLogger(ForlanTest.class);

	@Autowired
	private RedisLockRegistry redisLockRegistry;

	public void test() {
		String lockKey = "key_id";
		Lock loginLock = redisLockRegistry.obtain(lockKey.intern());
		boolean getLock = false;
		try {
			// tryLock()底層呼叫this.tryLock(0L, TimeUnit.MILLISECONDS)
			getLock = loginLock.tryLock(5, TimeUnit.SECONDS);
			if (getLock) {
				//獲得鎖執行業務
			}
		} catch (Exception e) {
			logger.error("異常資訊...", e);
		} finally {
			if (getLock) {
				//釋放鎖
				loginLock.unlock();
			}
		}
	}
}
Redis分散式實現原理

二、原理

obtain方法

private final Map<String, RedisLockRegistry.RedisLock> locks;

private final class RedisLock implements Lock {
	private final String lockKey;
	private final ReentrantLock localLock;
	private volatile long lockedAt;

	private RedisLock(String path) {
		this.localLock = new ReentrantLock();
		this.lockKey = this.constructLockKey(path);
	}
}

public Lock obtain(Object lockKey) {
    Assert.isInstanceOf(String.class, lockKey);
    String path = (String)lockKey;
    return (Lock)this.locks.computeIfAbsent(path, (x$0) -> {
        return new RedisLockRegistry.RedisLock(x$0);
    });
}
Redis分散式實現原理

主要是根據lockKey去查locks這個map中是否已經存在這個key
如果存在就返回內部類RedisLock
如果不存在就建立一個RedisLock,以lockKey為key,RedisLock為value放入map中 

備註:每個分散式應用自己都會建立一個RedisLockRegistry例項,同一個應用的多個執行緒共享RedisLock類

tryLock方法

public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
	long now = System.currentTimeMillis();
	// 嘗試拿取本地鎖
	if (!this.localLock.tryLock(time, unit)) {
		return false;
	} else {
		try {
			long expire = now + TimeUnit.MILLISECONDS.convert(time, unit);

			boolean acquired;
			// 當前時間還沒過期並且還未獲得redis鎖,睡眠100ms繼續重試
			while(!(acquired = this.obtainLock()) && System.currentTimeMillis() < expire) {
				Thread.sleep(100L);
			}

			if (!acquired) {
				this.localLock.unlock();
			}

			return acquired;
		} catch (Exception var9) {
			this.localLock.unlock();
			this.rethrowAsLockException(var9);
			return false;
		}
	}
}
Redis分散式實現原理

主要過程 

先獲得本地鎖,拿不到直接返回失敗
當前時間還沒過期並且還沒拿到redis鎖,睡眠100ms繼續重試

如果拿到redis鎖,結束迴圈,返回成功

如果超時了還沒拿到,釋放鎖,返回失敗

拿redis鎖的過程

private boolean obtainLock() {
	boolean success = (Boolean)RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript, Collections.singletonList(this.lockKey), new Object[]{RedisLockRegistry.this.clientId, String.valueOf(RedisLockRegistry.this.expireAfter)});
	if (success) {
		this.lockedAt = System.currentTimeMillis();
	}

	return success;
}
Redis分散式實現原理

通過obtainLock方法,執行lua指令碼來獲取

redisTemplate.execute()引數說明:

第一個obtainLockScript引數就是要執行的lua指令碼;

local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
  redis.call('PEXPIRE', KEYS[1], ARGV[2])
  return true
elseif not lockClientId then
  redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
  return true
end
return false
Redis分散式實現原理

第二個引數就是表示在指令碼中所用到的那些 Redis 鍵(key),這些鍵名引數可以在 Lua 中通過全域性變數 KEYS 陣列,用1為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推);

第三個參是附加引數 arg [arg …] ,可以在 Lua 中通過全域性變數 ARGV 陣列訪問,訪問的形式和 KEYS 變數類似( ARGV[1] 、 ARGV[2] ,諸如此類)

為什麼要用本地鎖

  • 為了可重入
  • 為了減輕redis伺服器的壓力

為什麼要用lua指令碼

  • 保證原子性
  • 減少網路開銷
  • 替代redis的事務功能

unlock方法

public void unlock() {
    if (!this.localLock.isHeldByCurrentThread()) {
        throw new IllegalStateException("You do not own lock at " + this.lockKey);
    } else if (this.localLock.getHoldCount() > 1) {
        this.localLock.unlock();
    } else {
        try {
            if (Thread.currentThread().isInterrupted()) {
                RedisLockRegistry.this.executor.execute(this::removeLockKey);
            } else {
                this.removeLockKey();
            }
            if (RedisLockRegistry.logger.isDebugEnabled()) {
                RedisLockRegistry.logger.debug("Released lock; " + this);
            }
        } catch (Exception var5) {
            ReflectionUtils.rethrowRuntimeException(var5);
        } finally {
            this.localLock.unlock();
        }
    }
}
Redis分散式實現原理

釋放鎖的過程
1、判斷是否是當前執行緒持有鎖,如果不是,拋異常(本地鎖)
2、判斷當前執行緒持有鎖的計數
如果當前執行緒持有鎖的計數 > 1,說明本地鎖被當前執行緒多次獲取,這時只會釋放本地鎖,釋放之後當前執行緒持有鎖的計數-1。
否則,釋放本地鎖和redis鎖。

相關文章