Redisson 作為分散式鎖
官方文件:https://github.com/redisson/redisson/wiki
-
引入依賴
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.1</version> </dependency>
2.配置redission
@Configuration public class MyRedissonConfig { /** * 所有對 Redisson 的使用都是通過 RedissonClient * * @return * @throws IOException */ @Bean(destroyMethod = "shutdown") public RedissonClient redisson() throws IOException { // 1、建立配置 Config config = new Config(); // Redis url should start with redis:// or rediss:// config.useSingleServer().setAddress("redis://192.168.163.131:6379"); // 2、根據 Config 建立出 RedissonClient 例項 return Redisson.create(config); } }
3.測試
@Autowired RedissonClient redissonClient; @Test public void redission() { System.out.println(redissonClient); }
4.使用
@ResponseBody @GetMapping("/hello") public String hello() { // 1. 獲取一把鎖 RLock lock = redisson.getLock("my-lock"); // 2. 加鎖, 阻塞式等待 lock.lock(); try { System.out.println("加鎖成功,執行業務..."); Thread.sleep(15000); } catch (Exception e) { } finally { // 3. 解鎖 假設解鎖程式碼沒有執行,Redisson 會出現死鎖嗎?(不會) System.out.println("釋放鎖"+Thread.currentThread().getId()); lock.unlock(); } return "hello"; }
假設解鎖程式碼沒有執行,Redisson 會出現死鎖嗎?
不會
- 鎖的自動續期,如果業務時間很長,執行期間自動給鎖續期 30 s,不用擔心業務時間過長,鎖自動過期被刪掉;
- 加鎖的業務只要執行完成,就不會給當前鎖續期,即使不手動續期,預設也會在 30 s 後解鎖
原始碼分析-Redission如何解決死鎖
Ctrl+Alt檢視方法實現
這是一個加鎖方法,不傳過期時間
public void lock() {
try {
//這裡過期時間自動賦值成-1
this.lock(-1L, (TimeUnit)null, false);
} catch (InterruptedException var2) {
throw new IllegalStateException();
}
}
然後會呼叫 this.lock(-1L, (TimeUnit)null, false)方法
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//得到執行緒ID
long threadId = Thread.currentThread().getId();
//通過執行緒ID獲取到鎖
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);
}
}
}
獲取鎖方法
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}
裡面又呼叫了tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
//如果傳了過期時間
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//沒有傳過期時間
else {
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
有指定過期時間走tryLockInnerAsync
方法,嘗試用非同步加鎖
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
//先把時間轉換成internalLockLeaseTime
this.internalLockLeaseTime = unit.toMillis(leaseTime);
//然後執行lua指令碼 發給redis執行
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', 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.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
沒有指定過期時間呼叫getLockWatchdogTimeout()方法,獲取鎖的預設看門狗時間,30秒
public long getLockWatchdogTimeout() {
return this.lockWatchdogTimeout;
}
this.lockWatchdogTimeout = 30000L;
還是呼叫tryLockInnerAsync
給redis
傳送命令,佔鎖成功返回一個以不變非同步編排的RFuture<Long>
物件,來進行監聽,裡面有兩個引數ttlRemaining, e
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
this.scheduleExpirationRenewal(threadId);
}
}
});
裡面有個scheduleExpirationRenewal
方法
private void scheduleExpirationRenewal(long threadId) {
RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
//重新設定過期時間
this.renewExpiration();
}
}
裡面的關鍵方法renewExpiration
執行定時任務,
private void renewExpiration() {
RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
//裡面會執行一個定時任務
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
if (res) {
RedissonLock.this.renewExpiration();
}
}
});
}
}
}
//看門狗時間/3 10秒鐘重試一次
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
主要是來執行renewExpirationAsync
這個方法
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
裡面傳入了一個internalLockLeaseTime
時間引數
又是獲取看門狗時間
總結
-
如果傳了鎖的超時時間,就傳送給redis執行指令碼,進行佔鎖,預設超時就是我們指定的時間
-
如果未指定鎖的超時時間,就是使用lockWatchdogTimeout的預設時間30秒,只要佔鎖成功就會啟動一個定時任務【重新給所設定時間,新的過期時間就是
lockWatchdogTimeout
的預設時間】最佳實踐使用自定義過期時間,省掉了自動續期時間,自動加鎖
讀寫鎖測試
@GetMapping("/write")
@ResponseBody
public String writeValue()
{
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String s="";
RLock rLock=readWriteLock.writeLock();
try{
//加寫鎖
rLock.lock();
s= UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue",s);
}catch (Exception e){
e.printStackTrace();
}
finally {
rLock.unlock();
}
return s;
}
@GetMapping("/read")
@ResponseBody
public String readValue()
{
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String s="";
//加讀鎖
RLock rLock=readWriteLock.readLock();
rLock.lock();
try{
s=redisTemplate.opsForValue().get("writeValue");
}catch (Exception e){
e.printStackTrace();
}
finally {
rLock.unlock();
}
return s;
}
寫鎖沒釋放讀鎖就必須等待,沒有寫鎖讀鎖都可以讀
保證資料的一致性,寫鎖是一個排他鎖、互斥鎖,讀鎖是共享鎖。
讀讀共享、讀寫互斥、寫寫互斥、寫讀互斥,只要有寫的存在都必須等待
訊號量測試
像車庫停車,每進來一輛車,車庫減少一個車位,只有當車庫還有車位才可以停車
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
//獲取一個訊號 佔一個值
park.acquire();
return "ok";
}
@GetMapping("/go")
@ResponseBody
public String go(){
RSemaphore park = redisson.getSemaphore("park");
//釋放一個車位
park.release();
return "ok";
}
訪問:
訊號量可以用作分散式的限流
閉鎖
只有等待所有活動都完成才發生,例如當所有班級放學走完才關閉學校大門
@GetMapping("/lockdoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await();//等待閉鎖都完成
return "放假啦....";
}
@GetMapping("/gogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown();
return id+"班都走了";
}
快取一致性解決
在我們讀快取的時候可能會有資料被修改過,為了讓我們能夠讀到最新的資料,有兩種處理方法:
雙寫模式
在把資料寫入資料庫的時候,同時寫入到快取中
問題:在寫的過程中,可能會在第一個執行緒快取還沒寫進,但是第二個查詢到快取又開始寫資料,讀到的最新資料有延遲,導致產生髒資料
失效模式
在把資料寫入資料更新的時候,把快取刪除,下次查詢沒有快取再新增快取
問題:線上程1更新資料的時候消耗大量時間,還沒刪快取,執行緒2進來也沒有快取,讀取到原來老的資料,然後更新快取
我們系統的一致性解決方案:
1、快取的所有資料都有過期時間,資料過期下一次查詢觸發主動更新
2、讀寫資料的時候,加上分散式的讀寫鎖。
3、遇到實時性、一致性要求高的資料,就應該查資料庫,即使慢點。