圖解 Redis 分佈式鎖
基本原理
我們可以同時去一個地方 “佔坑”,如果佔到,就執行邏輯。否則就必須等待,直到釋放鎖。“佔坑” 可以去 redis,可以去數據庫,可以去任何大家都能訪問的地方。等待可以自旋的方式。
階段一
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
//階段一
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
//獲取到鎖,執行業務
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
//刪除鎖,如果在此之前報錯或宕機會造成死鎖
stringRedisTemplate.delete("lock");
return categoriesDb;
}else {
//沒獲取到鎖,等待100ms重試
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}
public Map<String, List<Catalog2Vo>> getCategoryMap() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("緩存不命中,準備查詢數據庫。。。");
Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb();
String toJSONString = JSON.toJSONString(categoriesDb);
ops.set("catalogJson", toJSONString);
return categoriesDb;
}
System.out.println("緩存命中。。。。");
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
}
問題: setnx 佔好了位,業務代碼異常或者程序在頁面過程中宕機。沒有執行刪除鎖邏輯,這就造成了死鎖
解決: 設置鎖的自動過期,即使沒有刪除,會自動刪除
階段二
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock) {
//設置過期時間
stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
stringRedisTemplate.delete("lock");
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}
問題: setnx 設置好,正要去設置過期時間,宕機。又死鎖了。
解決: 設置過期時間和佔位必須是原子的。redis 支持使用 setnx ex 命令
階段三
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
//加鎖的同時設置過期時間,二者是原子性操作
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
//模擬超長的業務執行時間
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringRedisTemplate.delete("lock");
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}
問題: 刪除鎖直接刪除???如果由於業務時間很長,鎖自己過期了,我們直接刪除,有可能把別人正在持有的鎖刪除了。
解決: 佔鎖的時候,值指定爲 uuid,每個人匹配是自己的鎖才刪除。
階段四
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
//爲當前鎖設置唯一的uuid,只有當uuid相同時纔會進行刪除鎖的操作
Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
if (lockValue.equals(uuid)) {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringRedisTemplate.delete("lock");
}
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}
問題: 如果正好判斷是當前值,正要刪除鎖的時候,鎖已經過期,別人已經設置到了新的值。那麼我們刪除的是別人的鎖
解決: 刪除鎖必須保證原子性。使用 redis+Lua 腳本完成
階段五 - 最終形態
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}
保證加鎖【佔位 + 過期時間】和刪除鎖【判斷 + 刪除】的原子性。更難的事情,鎖的自動續期。
Redisson
Redisson 是一個在 Redis 的基礎上實現的 Java 駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分佈式的 Java 常用對象,還提供了許多分佈式服務。
其中包括 (BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service)
Redisson 提供了使用 Redis 的最簡單和最便捷的方法。Redisson 的宗旨是促進使用者對 Redis 的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。
更多請參考官方文檔:
https://github.com/redisson/redisson/wiki
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/T7xajzhwUwFUuXov76ymlg