Redis分散式鎖的try-with-resources實現
一、簡介
在當今這個時代,單體應用(standalone)已經很少了,java提供的synchronized已經不能滿足需求,大家自然 而然的想到了分散式鎖。談到分散式鎖,比較流行的方法有3中:
- 基於資料庫實現的
- 基於redis實現的
- 基於zookeeper實現的
今天我們重點說一下基於redis的分散式鎖,redis分散式鎖的實現我們可以參照redis的官方文件。 實現Redis分散式鎖的最簡單的方法就是在Redis中建立一個key,這個key有一個失效時間(TTL),以保證鎖最終會被自動釋放掉。當客戶端釋放資源(解鎖)的時候,會刪除掉這個key。
獲取鎖使用命令:
SET resource_name my_random_value NX PX 30000
這個命令僅在不存在key的時候才能被執行成功(NX選項),並且這個key有一個30秒的自動失效時間(PX屬性)。這個key的值是“my_random_value”(一個隨機值), 這個值在所有的客戶端必須是唯一的,所有同一key的獲取者(競爭者)這個值都不能一樣。
value的值必須是隨機數主要是為了更安全的釋放鎖,釋放鎖的時候使用指令碼告訴Redis:只有key存在並且儲存的值和我指定的值一樣才能告訴我刪除成功。
可以通過以下Lua指令碼實現:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
使用這種方式釋放鎖可以避免刪除別的客戶端獲取成功的鎖。舉個例子:客戶端A取得資源鎖,但是緊接著被一個其他操作阻塞了,當客戶端A執行完畢其他操作後要釋放鎖時, 原來的鎖早已超時並且被Redis自動釋放,並且在這期間資源鎖又被客戶端B再次獲取到。如果僅使用DEL命令將key刪除,那麼這種情況就會把客戶端B的鎖給刪除掉。 使用Lua指令碼就不會存在這種情況,因為指令碼僅會刪除value等於客戶端A的value的key(value相當於客戶端的一個簽名)。
這種方法已經足夠安全,如果擔心redis故障轉移時,鎖失效的問題,請參照Redis官方文件中的RedLock,這裡不做具體討論。
二、try-with-resources的實現
知道了Redis鎖的實現原理,我們再來看看如何實現。其實關鍵的步驟只有兩步:
- 獲取鎖;
- 釋放鎖;
大家在寫程式的時候是不是總忘記釋放鎖呢?就像以前對流操作時,忘記了關閉流。從java7開始,加入了try-with-resources的方式,它可以 自動的執行close()方法,釋放資源,再也不用寫finally塊了。我們就按照這種思路編寫Redis鎖,在具體寫程式碼之前,我們先談談 Redis的客戶端,Redis的客戶端官方推薦有3種:
- Jedis;
- Lecttuce;
- Redisson;
Redis官方比較推薦Redisson,但是Spring-data中並沒有這種方式,Spring-Data-Redis支援Jedis和Lecttuce兩種方式。 國內用的比較多的是Jedis,但是Spring-Data預設用Lecttuce。不管那麼多了,直接用Spring-Boot,配置好連線,直接使用就好了。
Redis鎖的try-with-resources實現:
public class RedisLock implements Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisLock.class);
private RedisTemplate redisTemplate;
private String lockKey;
private String lockValue;
private int expireTime;
public RedisLock(RedisTemplate redisTemplate,String lockKey,String lockValue,int expireTime){
this.redisTemplate = redisTemplate;
//redis key
this.lockKey = lockKey;
//redis value
this.lockValue = lockValue;
//過期時間 單位:s
this.expireTime = expireTime;
}
/**
* 獲取分散式鎖
*/
public boolean getLock(){
//獲取鎖的操作
return (boolean) redisTemplate.execute((RedisCallback) connection -> {
//過期時間 單位:s
Expiration expiration = Expiration.seconds(expireTime);
//執行NX操作
SetOption setOption = SetOption.ifAbsent();
//序列化key
byte[] serializeKey = redisTemplate.getKeySerializer().serialize(lockKey);
//序列化value
byte[] serializeVal = redisTemplate.getValueSerializer().serialize(lockValue);
//獲取鎖
boolean result = connection.set(serializeKey, serializeVal, expiration, setOption);
LOGGER.info("獲取redis鎖結果:" + result);
return result;
});
}
/**
* 自動釋放鎖
* @throws IOException
*/
@Override
public void close() throws IOException {
//釋放鎖的lua指令碼
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
RedisScript<String> redisScript = RedisScript.of(script,Boolean.class);
//是否redis鎖
Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(lockKey), lockValue);
LOGGER.info("釋放redis鎖結果:"+result);
}
}
只要實現了Closeable
介面,並重寫了close()
方法,就可以使用try-with-resources的方式了。
具體的使用程式碼如下:
@SpringBootApplication
public class Application {
private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class, args);
RedisTemplate redisTemplate = applicationContext.getBean("redisTemplate",RedisTemplate.class);
try (RedisLock lock = new RedisLock(redisTemplate,"test_key","test_val",60)){
//獲取鎖
if (lock.getLock()){
//模擬執行業務
Thread.sleep(5*1000);
LOGGER.info("獲取到鎖,執行業務操作耗時5s");
}
}catch (Exception e){
LOGGER.error(e.getMessage(),e);
}
}
}
這樣我們就不用關心鎖的釋放問題了。