-
分佈式鎖是什麼?
分佈式鎖是控制分佈式系統或不同系統之間共同訪問共享資源的一種鎖實現,如果不同的系統或同一個系統的不同主機之間共享了某個資源時,往往通過互斥來防止彼此干擾。
-
分佈鎖設計目的?
可以保證在分佈式部署的應用集羣中,同一個方法在同一操作只能被一臺機器上的一個線程執行。
-
分佈式鎖設計要求
1、這是一把可重入鎖(避免死鎖)
2、這把鎖具備高性能獲取鎖和釋放鎖的功能
3、這把鎖具有高可用獲取鎖和釋放鎖的功能
- 分佈鎖實現方案分析
-
加鎖的時候,使用 setnx(SETNX 當且僅當 key 不存在時,set 一個 key 爲 val 的字符串才成功)
-
鎖的 value 值可以爲當前佔有鎖服務器內網IP編號拼接任務標識,如:lock1_127.0.0.1
-
使用 expire 命令爲鎖添 加一個超時時間,超過該時間則自動釋放鎖
-
釋放鎖的時候,判斷是不是該鎖(即Value爲當前服務器內網IP編號拼接任務標識),若是該鎖,則執行 delete 進行鎖釋放
-
Redis分佈式鎖可能出現的問題
基於上面的分析,我們很容易能想到採用setnx進行鎖獨佔性保證,此外,除了獨佔性,我們還要保證這把鎖的有效期,如果鎖長期不過期將導致某個系統執行一次任務之後,其他任務都無法被當前系統或其他系統執行,所以我們將採用setex來保證這把鎖的有效期。但在系統設計的時候我們得爲百分之一的可能性作爲百分之百的設計,當我們執行完setnx之後,此時,如果出現Redis宕機或者我們的Server服務宕機情況,我們的setex命令將無法執行。宕機情況如圖
- 基於故障服務機故障和Redis故障,我們必須要保證setnx和setex這兩個命令的原子性, 那麼redis保證這兩個命令原子性的方式主要有兩種,一種是採用lua腳本(此種方式是保證所有命令原子性的方法),一種是基於官方的setnx和setex命令連用。
- 我們先來講一下Lua腳本整合springboot的redisTemplate這種實現流程步驟
- 在resource目錄下面新增一個後綴名爲.lua結尾的文件
- 編寫lua腳本
- 傳入lua腳本的key和arg
- 調用redisTemplate.execute方法執行腳本
下面我們講一下Lua腳本整合springboot的redisTemplate這種實現的代碼
引入maven pom.xml文件依賴
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
注入redisTemplate的bean
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, String> redisTemplate = new RedisTemplate<String,String>();
redisTemplate.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerialize 替換默認序列化
/**Jackson序列化 json佔用的內存最小 */
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
/**Jdk序列化 JdkSerializationRedisSerializer是最高效的*/
// JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
/**String序列化*/
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
/**將key value 進行stringRedisSerializer序列化*/
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
/**將HashKey HashValue 進行序列化*/
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
分佈式鎖代碼實現類:
package com.xdclass.mobile.xdclassmobileredis.schedule;
import com.xdclass.mobile.xdclassmobileredis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@Service
public class LuaDistributeLock {
private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);
@Autowired
private RedisTemplate redisTemplate;
private static String LOCK_PREFIX = "lua_";
private DefaultRedisScript<Boolean> lockScript;
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "LockNxExJob";
boolean luaRet = false;
try {
luaRet = luaExpress(lock,getHostIp());
//獲取鎖失敗
if (!luaRet) {
String value = (String)genValue(lock);
//打印當前佔用鎖的服務器IP
//logger.info("lua get lock fail,lock belong to:{}", value);
return;
} else {
//獲取鎖成功
//logger.info("lua start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("lock error", e);
} finally {
if (luaRet) {
//logger.info("release lock success");
redisService.remove(lock);
}
}
}
/**
* 獲取lua結果
* @param key
* @param value
* @return
*/
public Boolean luaExpress(String key,String value) {
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;
}
/**
* 獲取本機內網IP地址方法
*
* @return
*/
private static String getHostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本機地址,IPv4的loopback範圍是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":") == -1) {
return ip.getHostAddress();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static Object genValue(final String key){
Object result = null;
ValueOperations<String, String> operations = redisTemplate.opsForValue();
result = operations.get(key);
}
}
lua腳本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
- 下面我們來講解基於springboot的整合官方提供的setnx和setex命令連用的方式實戰方案,此處我們採用的是springboot2.X裏面spring-data-redis提供的redisTemplate的實現方案,此方案和lua腳本一樣,需要引入上面的pom.xml和redisTemplate這兩個步
package com.xdclass.mobile.xdclassmobileredis.schedule;
import com.xdclass.mobile.xdclassmobileredis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Enumeration;
@Service
public class LockNxExJob {
private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);
@Autowired
private RedisTemplate redisTemplate;
private static String LOCK_PREFIX = "prefix_";
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "LockNxExJob";
boolean nxRet = false;
try{
//redistemplate setnx操作 setex
nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
Object lockValue = redisService.genValue(lock);
//獲取鎖失敗
if(!nxRet){
String value = (String)genValue(lock);
//打印當前佔用鎖的服務器IP
//logger.info("get lock fail,lock belong to:{}",value);
return;
}else{
redisTemplate.opsForValue().set(lock,getHostIp(),3600);
//Thread.sleep(30)
//獲取鎖成功
//logger.info("start lock lockNxExJob success");
Thread.sleep(5000);
}
}catch (Exception e){
logger.error("lock error",e);
}finally {
if(nxRet){
//logger.info("release lock success");
redisService.remove(lock);
}
}
}
/**
* 獲取本機內網IP地址方法
* @return
*/
private static String getHostIp(){
try{
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()){
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本機地址,IPv4的loopback範圍是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":")==-1){
return ip.getHostAddress();
}
}
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
private static Object genValue(final String key){
Object result = null;
ValueOperations<String, String> operations = redisTemplate.opsForValue();
result = operations.get(key);
}
public static void main(String[] args) {
String localIP = "";
try {
localIP = getHostIp();
} catch (Exception e) {
e.printStackTrace();
}
//獲取本機IP
System.out.println(localIP);
}
}
至此,我們採用Lua腳本和setIfAbsent 實現了redis分佈式鎖,大家如果對這兩種實現方式有疑問的話可以歡迎和我私聊 ,QQ 1151427440,這篇文章出自小D課堂,轉載註明出處。