歡迎您光臨本站 註冊首頁

· redis setnx雜誌閱讀

redis分散式鎖,你真的用對了嗎?

admin @ 2020-04-20 reply:0

隨著業務場景越來越複雜,使用的架構也就越來越複雜,分散式、高併發已經是業務要求的常態。說到分散式,不得不提的就是分散式鎖和分散式事物。今天我們就來談談redis實現的分散式鎖的問題!

實現要求:

1.互斥性,在同一時刻,只能有一個客戶端持有鎖2.防止死鎖,如果持有鎖的客戶端崩潰而且沒有主動釋放鎖,怎樣保證鎖可以正常釋放,使得客戶端可以正常加鎖3.加鎖和釋放鎖必須是同一個客戶端。4.容錯性,只有redis還有節點存活,就可以正常的加鎖解鎖操作。錯誤使用方式一:

保證互斥和防止死鎖,首先想到的使用redis的setnx命令保證互斥,為了防止死鎖,需要設定一個超時時間。

public Object getAndSet(String key, Object value, long timeout) {

Object object = redisTemplate.opsForValue().getAndSet(key, value);

redisTemplate.expire(key, timeout, TimeUnit.SECONDS);

return object;

}

在多執行緒併發環境下,任何非原子性的操作,都可能導致問題。在這段程式碼中,如果設定過期時間,redis例項崩潰,就無法設定過期時間。如果客戶端沒有正確釋放鎖,那么該鎖永遠不會過期,就永遠不會被釋放。

錯誤方式二

比較容易想到的就是設定值和超時時間為原子操作不就可以了嗎。那么使用方法就是這樣了

public static boolean wrongLock(Jedis jedis, String key, int expireTime) {

long expireTs = System.currentTimeMillis() + expireTime;

// 鎖不存在,當前執行緒加鎖成果

if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {

return true;

}

String value = jedis.get(key);

//如果當前鎖存在,且鎖已過期

if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {

//鎖過期,設定新的過期時間

String oldValue = jedis.getSet(key, String.valueOf(expireTs));

if (oldValue != null && oldValue.equals(value)) {

// 多執行緒併發下,只有一個執行緒會設定成功

// 設定成功的這個執行緒,key的舊值一定和設定之前的key的值一致

return true;

}

}

// 其他情況,加鎖失敗

return true;

}

這段程式碼,乍一眼看沒啥問題,你仔細看就會發現:

1.value 設定為過期時間,就要要求各個客戶端嚴格的時鐘同步,這需要使用到同步時鐘。即使有同步時鐘,分散式的伺服器一般也會有少許誤差,這不重要

2. 鎖過期時,使用jedis.getSet雖然可以保證一個執行緒設定成功,但不能保證加鎖和解鎖為同一個客戶端,因為沒有標誌時那個客戶端設定的

解鎖錯誤方式一:

直接刪除key

public sttic void wrongReleaseLock(Jedis jedis, String key) {

//不是自己加鎖的key,也會被釋放

jedis.del(key);

}

簡單粗暴,這樣做的話,不是自己的鎖也會被刪除掉。不夠嚴謹

解鎖錯誤方式二:

判斷自己是不是鎖的持有者,只有持有者才可以釋放鎖

public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {

if (uniqueId.equals(jedis.get(key))) {

// 如果這時鎖過期自動釋放,又被其他執行緒加鎖,該執行緒就會釋放不屬於自己的鎖

jedis.del(key);

}

}

完美!

真的完美?

看起來很完美,但是如果你判斷的時候鎖是自己持有的,這時候超時自動釋放了,然後又被其他客戶端重新上鎖了,然後你刪除的不就是其他客戶端的鎖,一樣不就亂套了?

基於以上息探索,給出以下示例,僅供學習交流!

1.命令必須保證是互斥的2. 設定的key必須要有過期時間3. value使用唯一id,標誌每個客戶端。只有鎖的持有者才能釋放鎖。加鎖直接使用set命令同時設定唯一id和過期時間;其中解鎖些微複雜些,加鎖後可以返回唯一ID,標誌此鎖是該客戶端鎖擁有;釋放鎖時要先判斷是否是自己,只有自己才有刪除操作,程式碼示例如下:

@Component

@Slf4j

public class RedisLockUtil {

// 超時時間

private static int EXPIRE_TIME = 5 * 1000;

@Autowired

private RedisTemplate redisTemplate;

private static Map<String, Thread> threadMap = new ConcurrentHashMap();

public Object lock(String key, Long timeOut) {

log.info("加鎖開始");

try {

// 超時等待時間

Long waitEnd = System.currentTimeMillis() + EXPIRE_TIME;

// 生成一個uuid,使得分散式呼叫有一個擁有者

String uuid = UUID.randomUUID().toString();

String value = key + uuid;

// 在等待時間內,嘗試獲取鎖

while (System.currentTimeMillis() < waitEnd) {

log.info("嘗試獲取鎖");

// 同步程式碼,使得操作原子性

synchronized (this) {

if (Objects.nonNull(redisTemplate.opsForValue().get(key))) {

continue;

}

Object result = redisTemplate.opsForValue().getAndSet(key, value);

if (Objects.isNull(result)) {

log.info("成功獲取鎖");

}

// 設定過期時間,以防死鎖

redisTemplate.expire(key, timeOut, TimeUnit.MILLISECONDS);

// 開啟一個守護程序,給當前鎖動態新增時間

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

while (true) {

try {

if(System.currentTimeMillis() > waitEnd) {

System.out.println(Thread.currentThread().getName() + "-->" + " 更新redis時間2s ");

redisTemplate.expire(key, 1 * 60000, TimeUnit.MILLISECONDS);

Thread.sleep(1000);

}

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

});

thread.setDaemon(true); // 守護程序

threadMap.put(value, thread);

thread.setName(key+"-"+value);

thread.start();

return value;

}

}

}catch (Exception e) {

log.error("lock error:", e);

throw new RuntimeException("未能獲取分散式鎖");

}

log.info("獲取鎖失敗");

throw new RuntimeException("獲取分散式鎖超時");

}

public boolean unLock(String key, Object value) {

log.info("釋放鎖:{}--{}", key, value);

cts.isNull(key) ) {

return false;

}

DefaultRedisScript script = new DefaultRedisScript();

script.setResultType(List.class);

script.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end");

Object o = redisTemplate.execute(script, Collections.singletonList(key), value);

if (Objects.nonNull(o) && ((ArrayList)o).size() !=0) {

threadMap.remove(value).stop();

}

log.info("釋放鎖{}", o);

return true;

}

}

模擬呼叫程式碼

@GetMapping("/hello")

public Object hello(String hello) {

log.info("設定key值開始!");

Object object = redisLockUtil.lock(REDIS_KEY, 1*60000L);

try {

log.info("設定key值{}", object);

// 這裡是模擬業務處理場景

try {

Thread.sleep(1 * 60000L);

} catch (InterruptedException e) {

e.printStackTrace();

}

} catch (Exception e) {

}finally {

redisLockUtil.unLock(REDIS_KEY, object);

}

return object;

}



[admin via ] redis分散式鎖,你真的用對了嗎?已經有452次圍觀

http://coctec.com/magazine/show-post-item-104.html