注:此文章,爲總結的學習筆記。redis
能夠保證在分佈式部署的應用集羣中,同一個方法在同一操做只能被一臺機器上的一個線程執行。spring
設計要求bash
分佈鎖實現方案分析服務器
分佈鎖知足兩個條件,一個是加有效時間的鎖,一個是高性能解鎖maven
採用redis命令setnx(set if not exist)、setex(set expire value)實現分佈式
【千萬記住】解鎖流程不能遺漏,不然致使任務執行一次就永不過時性能
將加鎖代碼和任務邏輯放在try,catch代碼塊,將解鎖流程放在finally學習
public void lockJob() {
String lock = LOCK_PREFIX + "LockNxExJob";
try{
//redistemplate setnx操做
boolean nxRet = redisTemplate.opsForValue().setIfAbsent(lock,"XXX");
Object lockValue = redisService.get(lock);
//獲取鎖失敗
if(!nxRet){
String value = (String)redisService.get(lock);
//打印當前佔用鎖的服務器IP
logger.info("get lock fail,lock belong to:{}",value);
return;
}else{
redisTemplate.opsForValue().set(lock,getHostIp(),3600);
//獲取鎖成功
logger.info("start lock lockNxExJob success");
Thread.sleep(5000);
}
}catch (Exception e){
logger.error("lock error",e);
}finally {
redisService.remove(lock);
}
}
複製代碼
value是由客戶端生成的一個隨機字符串,至關因而客戶端持有鎖的標誌優化
NX表示只有key值不存在的時候才能SET成功,至關於只有第一個請求的客戶端才能得到鎖ui
PX 30000表示這個鎖有一個30秒的自動過時時間。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
複製代碼
某線程成功獲得了鎖,而且設置的超時時間是30秒。 若是某些緣由致使線程B執行的很慢很慢,過了30秒都沒執行完,這時候鎖過時自動釋放,線程B獲得了鎖。
隨後,線程A執行完了任務,線程A接着執行del指令來釋放鎖。但這時候線程B還沒執行完,線程A實際上刪除的是線程B加的鎖。
怎麼避免這種狀況呢?能夠在del釋放鎖以前作一個判斷,驗證當前的鎖是否是本身加的鎖。
至於具體的實現,能夠在加鎖的時候把當前的線程ID當作value,並在刪除以前驗證key對應的value是否是本身線程的ID。
/**
* 獲取lua結果
* @param key
* @param value
* @return
*/
public Boolean luaExpress(String key,String value) {
DefaultRedisScript<Boolean> lockScript = new DefaultRedisScript<Boolean>();
lockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("add.lua")));
lockScript.setResultType(Boolean.class);
// 封裝參數
List<Object> keyList = new ArrayList<Object>();
keyList.add(key);
keyList.add(value);
Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
return result;
}
複製代碼
add.lua
local lockKey = KEYS[1]
local lockValue = KEYS[2]
-- setnx info
local result_1 = redis.call('SETNX', lockKey, lockValue)
if result_1 == true
then
local result_2= redis.call('SETEX', lockKey,3600, lockValue)
return result_1
else
return result_1
end
複製代碼
引發maven配置
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
複製代碼
代碼實現:
@SpringBootApplication
public class RedissonApplication {
public static void main(String[] args) {
SpringApplication.run(RedissonApplication.class, args);
}
@Bean
Redisson redissonSentinel() {
//支持單機,主從,哨兵,集羣等模式
//此爲哨兵模式
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://192.168.1.1:6379")
.setPassword("123456");
return (Redisson)Redisson.create(config);
}
}
複製代碼
使用:
String lockKey = "test";//分佈式鎖的key
RLock lock = redisson.getLock(lockKey);
lock.lock(60, TimeUnit.SECONDS); //設置60秒自動釋放鎖 (默認是30秒自動過時)
...業務代碼..
lock.unlock(); //釋放鎖
複製代碼
spring-data-redis的版本儘可能高版本,2.0如下的connection.set是沒有返回值的。
@Component
public class RedisLock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
//加鎖
public Boolean setNX(final String key, final String requestId, final long expirationTime, final TimeUnit timeUnit) {
return redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(key.getBytes(), (value == null ? "" : value).getBytes(),
Expiration.from(expirationTime, timeUnit),
RedisStringCommands.SetOption.ifAbsent()));
}
//釋放鎖
public Boolean releaseLock(String key, String requestId) {
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Boolean result = connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1, key.getBytes(), requestId.getBytes());
return result;
});
}
}
複製代碼