編輯:業(yè)余草來源:https://www./?p=4994在現(xiàn)代的編程語言中,接觸過多線程編程的程序員多多少少對鎖有一定的了解。簡單的說,多線程中的鎖就是在多線程環(huán)境下,多個線程對共享資源進(jìn)行修改的時候,保證共享資源一致性的機(jī)制。這里不展開說。在分布式環(huán)境下,原來的多線程的鎖就不管用了,也就出現(xiàn)了分布式鎖的需求。所謂分布式鎖服務(wù)也就是在分布式環(huán)境下,保證多個分布式的服務(wù)共享的資源一致性的服務(wù)。
在分布式環(huán)境下實現(xiàn)一個分布式鎖服務(wù)并不太容易,需要考慮很多在單進(jìn)程下的鎖服務(wù)不需要考慮的問題。分布式鎖鎖的實現(xiàn)也有很多。這里我們討論在 Java 中通過 redis 來實現(xiàn)。 在 GitHub 中的 redisson 項目中已經(jīng)有開源的實現(xiàn)。但是那個太復(fù)雜了。現(xiàn)在我們來基于單機(jī)的 redis 實現(xiàn)一個簡單的分布式鎖服務(wù)。這個服務(wù)必須滿足下面的要求
下面我們用實例來演示在 Java 中利用 redis 實現(xiàn)分布式鎖服務(wù) 加鎖通過 redis 來實現(xiàn)分布式鎖的加鎖邏輯如下所示: 
根據(jù)這個邏輯,實現(xiàn)上鎖的核心代碼如下所示: jedis.select(dbIndex); String key = KEY_PRE + key; String value = fetchLockValue(); if(jedis.exists(key)){ jedis.set(key,value); jedis.expire(key,lockExpirseTime); return value; }
表面上看這段代碼好像沒有什么問題,實際上并不能在分布式環(huán)境中正確的實現(xiàn)加鎖的操作。要能夠正確的實現(xiàn)加鎖操作,“判斷 key 是否存在”、“保存 key-value”、“設(shè)置 key 的過期時間”這三步操作必須是原子操作。如果不是原子操作,那么可能會出現(xiàn)下面兩種情況: 在“判斷 key 是否存在”得出 key 不存在的結(jié)果步驟后,“保存 key-value”步驟前,另一個客戶端執(zhí)行同樣的邏輯,并且執(zhí)行到了“判斷 key 是否存在”步驟,同樣得出了 key 不存在的結(jié)果。這樣回導(dǎo)致多個客戶端獲得了同一把鎖; 在客戶端執(zhí)行完“保存 key-value” 步驟后,需要設(shè)置一個 key 的過期時間,防止客戶端因為代碼質(zhì)量未解鎖,在或者進(jìn)程崩潰未解鎖導(dǎo)致的死鎖情況。在“保存 key-value”步驟之后,“設(shè)置 key 的過期時間”步驟之前,可能進(jìn)程崩潰,導(dǎo)致“設(shè)置 key 的過期時間”步驟失??;
redis 在2.6.12版本之后,對 set 命令進(jìn)行了擴(kuò)充,能夠規(guī)避上面的兩個問題。新版的 redis set 命令的參數(shù)如下 SET key value [EX seconds] [PX milliseconds] [NX|XX]
新版的 set 命令增加了 EX 、 PX 、 NX|XX 參數(shù)選項。他們的含義如下 EX seconds – 設(shè)置鍵 key 的過期時間,單位時秒
PX milliseconds – 設(shè)置鍵 key 的過期時間,單位時毫秒
NX – 只有鍵 key 不存在的時候才會設(shè)置 key 的值
XX – 只有鍵 key 存在的時候才會設(shè)置 key 的值
這樣,原來的三步操作就可以在一個 set 的原子操作里面來完成,規(guī)避了上面我們提到的兩個問題。 新版的 redis 加鎖核心代碼修改如下所示: jedis = redisConnection.getJedis(); jedis.select(dbIndex); String key = KEY_PRE + key; String value = fetchLockValue(); if ('OK'.equals(jedis.set(key, value, 'NX', 'EX', lockExpirseTime))) { return value; }
解鎖解鎖的基本流程如下: 
根據(jù)這個邏輯,在 Java 中解鎖的核心代碼如下所示: jedis.select(dbIndex); String key = KEY_PRE + key; if(jedis.exists(key) && value.equals(jedis.get(key))){ jedis.del(key); return true; } return false;
和加鎖的時候一樣,key 是否存在、判斷是否自己持有鎖、**刪除 key-value **這三步操作需要是原子操作,否則當(dāng)一個客戶端執(zhí)行完“判斷是否自己持有鎖”步驟后,得出自己持有鎖的結(jié)論,此時鎖的過期時間到了,自動被 redis 釋放了,同時另一個客戶端又基于這個 key 加鎖成功,如果第一個客戶端還繼續(xù)執(zhí)行刪除 key-value的操作,就將不屬于自己的鎖給釋放了。 這顯然是不運行的。在這里我們利用 redis 執(zhí)行 Lua 腳本的能力來解決原子操作的問題。修改后的解鎖核心代碼如下所示: jedis.select(dbIndex); String key = KEY_PRE + key; String command = 'if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end'; if (1L.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) { return true; }
另外,判斷是否自己持有鎖的機(jī)制是用加鎖的時候的 key-value 來判斷當(dāng)前的 key 的值是否等于自己持有鎖時獲得的值。所以加鎖的時候的 value 必須是一個全局唯一的字符串。擴(kuò)展:分布式全局唯一ID生成策略 完整的代碼如下所示package com.x9710.common.redis.impl;
import com.x9710.common.redis.LockService; import com.x9710.common.redis.RedisConnection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import redis.clients.jedis.Jedis;
import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.UUID;
/** * 分布式鎖 redis 實現(xiàn) * * @author 楊高超 * @since 2017-12-14 */ public class LockServiceRedisImpl implements LockService {
private static Log log = LogFactory.getLog(LockServiceRedisImpl.class);
private static String SET_SUCCESS = 'OK';
private static String KEY_PRE = 'REDIS_LOCK_';
private DateFormat df = new SimpleDateFormat('yyyyMMddHHmmssSSS');
private RedisConnection redisConnection;
private Integer dbIndex;
private Integer lockExpirseTime;
private Integer tryExpirseTime;
public void setRedisConnection(RedisConnection redisConnection) { this.redisConnection = redisConnection; }
public void setDbIndex(Integer dbIndex) { this.dbIndex = dbIndex; }
public void setLockExpirseTime(Integer lockExpirseTime) { this.lockExpirseTime = lockExpirseTime; }
public void setTryExpirseTime(Integer tryExpirseTime) { this.tryExpirseTime = tryExpirseTime; }
public String lock(String key) { Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); key = KEY_PRE + key; String value = fetchLockValue(); if (SET_SUCCESS.equals(jedis.set(key, value, 'NX', 'EX', lockExpirseTime))) { log.debug('Reids Lock key : ' + key + ',value : ' + value); return value; } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } } return null; }
public String tryLock(String key) {
Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); key = KEY_PRE + key; String value = fetchLockValue(); Long firstTryTime = new Date().getTime(); do { if (SET_SUCCESS.equals(jedis.set(key, value, 'NX', 'EX', lockExpirseTime))) { log.debug('Reids Lock key : ' + key + ',value : ' + value); return value; } log.info('Redis lock failure,waiting try next'); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } while ((new Date().getTime() - tryExpirseTime * 1000) < firstTryTime); } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } } return null; }
public boolean unLock(String key, String value) { Long RELEASE_SUCCESS = 1L; Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); key = KEY_PRE + key; String command = 'if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end'; if (RELEASE_SUCCESS.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) { return true; } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } } return false; }
/** * 生成加鎖的唯一字符串 * * @return 唯一字符串 */ private String fetchLockValue() { return UUID.randomUUID().toString() + '_' + df.format(new Date()); } }
測試代碼package com.x9710.common.redis.test;
import com.x9710.common.redis.RedisConnection; import com.x9710.common.redis.impl.LockServiceRedisImpl;
public class RedisLockTest {
public static void main(String[] args) { for (int i = 0; i < 9; i++) { new Thread(new Runnable() { public void run() { RedisConnection redisConnection = RedisConnectionUtil.create(); LockServiceRedisImpl lockServiceRedis = new LockServiceRedisImpl(); lockServiceRedis.setRedisConnection(redisConnection); lockServiceRedis.setDbIndex(15); lockServiceRedis.setLockExpirseTime(20); String key = '20171228'; String value = lockServiceRedis.lock(key); try { if (value != null) { System.out.println(Thread.currentThread().getName() + ' lock key = ' + key + ' success! '); Thread.sleep(25 * 1000); }else{ System.out.println(Thread.currentThread().getName() + ' lock key = ' + key + ' failure! '); } } catch (Exception e) { e.printStackTrace(); } finally { if (value == null) { value = ''; } System.out.println(Thread.currentThread().getName() + ' unlock key = ' + key + ' ' + lockServiceRedis.unLock(key, value));
} } }).start(); } } }
測試結(jié)果 Thread-1 lock key = 20171228 failure! Thread-2 lock key = 20171228 failure! Thread-4 lock key = 20171228 failure! Thread-8 lock key = 20171228 failure! Thread-7 lock key = 20171228 failure! Thread-3 lock key = 20171228 failure! Thread-5 lock key = 20171228 failure! Thread-0 lock key = 20171228 failure! Thread-6 lock key = 20171228 success! Thread-1 unlock key = 20171228 false Thread-2 unlock key = 20171228 false Thread-4 unlock key = 20171228 false Thread-8 unlock key = 20171228 false Thread-3 unlock key = 20171228 false Thread-5 unlock key = 20171228 false Thread-0 unlock key = 20171228 false Thread-7 unlock key = 20171228 false Thread-6 unlock key = 20171228 true
從測試結(jié)果來看可以看到,9個線程同時給一個 key 加鎖,只有一個能夠成功獲取到鎖,其余的客戶端都無法獲取到鎖。 后記這個代碼里面還實現(xiàn)了一個 tryLock 的接口。這個主要是客戶端無法獲取到鎖的時候會在一小段時間內(nèi)反復(fù)嘗試是否能夠獲取到鎖。 文中代碼鏈接: https://pan.baidu.com/s/1CQ_ZCG6Tfc-7z26JBggKRA
|