文章目錄
Redis - 分布式鎖實現(xiàn)以及相關(guān)問題解決方案
1.分布式鎖是什么?
1.1 分布式鎖設(shè)計目的
1.2 分布式鎖設(shè)計要求
1.3 分布式鎖設(shè)計思路
2.分布式鎖實現(xiàn)
3.分布式鎖實現(xiàn)過程中可能出現(xiàn)的問題以及解決方案
3.1 服務(wù)宕機造成死鎖
3.1.1 Lua腳本命令連用
3.1.2 RedisConnection命令連用
3.1.3 升級高版本Redis
3.2 業(yè)務(wù)時間大于鎖超時時間
3.2.1 解鎖錯位問題
3.2.2 業(yè)務(wù)并發(fā)執(zhí)行問題
3.2.3 可重入鎖
4.總結(jié)
Redis - 分布式鎖實現(xiàn)以及相關(guān)問題解決方案
1.分布式鎖是什么冈爹?
?分布式鎖是控制分布式系統(tǒng)或不同系統(tǒng)之間共同訪問共享資源的一種鎖實現(xiàn)。如果不同的系統(tǒng)或同一個系統(tǒng)的不同主機之間共享了某個資源時,往往通過互斥來防止彼此之間的干擾。實現(xiàn)分布式鎖的方式有很多哆窿,可以通過各種中間件來進行分布式鎖的設(shè)計囚聚,包括Redis鸦难、Zookeeper等,這里我們主要介紹Redis如何實現(xiàn)分布式鎖以及在整個過程中出現(xiàn)的問題和優(yōu)化解決方案片林。
1.1 分布式鎖設(shè)計目的
?可以保證在分布式部署的應(yīng)用集群中端盆,同一個方法的同一操作只能被一臺機器上的一個線程執(zhí)行。分布式鎖至少包含以下三點:
具有互斥性费封。任意時刻只有一個服務(wù)持有鎖焕妙。
不會死鎖。即使持有鎖的服務(wù)異常崩潰沒有主動解鎖后續(xù)也能夠保證其他服務(wù)可以拿到鎖弓摘。
加鎖和解鎖都需要是同一個服務(wù)焚鹊。
1.2 分布式鎖設(shè)計要求
分布式鎖要是一把可重入鎖(同時需避免死鎖)。
分布式鎖有高可用的獲取鎖和釋放鎖功能韧献。
分布式鎖獲取鎖和釋放鎖的性能要好
1.3 分布式鎖設(shè)計思路
使用SETNX命令獲取鎖(Key存在則返回0末患,不存在并且設(shè)置成功返回1)。
若返回0則不進行業(yè)務(wù)操作锤窑,若返回1則設(shè)置鎖Value為當(dāng)前服務(wù)器IP + 業(yè)務(wù)標(biāo)識璧针,用于鎖釋放和鎖延期時判斷。同時使用EXPIRE命令給鎖設(shè)置一個合理的過期時間渊啰,避免當(dāng)前服務(wù)宕機鎖永久存在造成死鎖探橱,并且設(shè)計需要保證可重入性申屹。
執(zhí)行業(yè)務(wù),業(yè)務(wù)執(zhí)行完成判斷當(dāng)前鎖Value是否為當(dāng)前服務(wù)器IP + 業(yè)務(wù)標(biāo)識隧膏,若相同則通過DEL或者EXPIRE設(shè)置為0釋放當(dāng)前鎖哗讥。
2.分布式鎖實現(xiàn)
?我們在實現(xiàn)分布式鎖的過程中大致思路就是上圖的整個流程,這里我們主要記住幾個要點:
鎖一定要設(shè)置失效時間胞枕,否則服務(wù)宕機鎖就會永久性存在杆煞,整個業(yè)務(wù)體系死鎖。
業(yè)務(wù)執(zhí)行完必須解鎖曲稼,可將加鎖和業(yè)務(wù)代碼放置try/catch中索绪,解鎖流程放置finally中湖员。
?若要用jar包方式后臺啟動服務(wù)贫悄,可用 nohup java -jar jar包名稱 &命令。這里我們來看一下我們加解鎖的主要代碼娘摔。
ClusterLockJob.java
package com.springboot.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
/**
* @author hzk
* @date 2019/7/2
*/
@Component
public class ClusterLockJob {
? ? @Autowired
? ? private RedisTemplate redisTemplate;
? ? @Value("${server.port}")
? ? private String port;
? ? public static final String LOCK_PRE = "lock_prefix_";
? ? @Scheduled(cron = "0/5 * * * * *")
? ? public void lock(){
? ? ? ? String lockName = LOCK_PRE + "ClusterLockJob";
? ? ? ? String currentValue = getHostIp() + ":" + port;
? ? ? ? Boolean ifAbsent = false;
? ? ? ? try {
? ? ? ? ? ? //設(shè)置鎖
? ? ? ? ? ? //Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockName, currentValue,3600, TimeUnit.SECONDS);
? ? ? ? ? ? ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockName, currentValue);
? ? ? ? ? ? if(ifAbsent){
? ? ? ? ? ? ? //獲取鎖成功窄坦,設(shè)置失效時間
? ? ? ? ? ? ? ? redisTemplate.expire(lockName,3600,TimeUnit.SECONDS);
? ? ? ? ? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? Thread.sleep(3000);
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? //獲取鎖失敗
? ? ? ? ? ? ? ? String value = (String) redisTemplate.opsForValue().get(lockName);
? ? ? ? ? ? ? ? System.out.println("Lock fail,current lock belong to:" + value);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("ClusterLockJob exception:" + e);
? ? ? ? }finally {
? ? ? ? ? ? if(ifAbsent){
? ? ? ? ? ? ? ? //若分布式鎖Value與本機Value一致,則當(dāng)前機器獲得鎖凳寺,進行解鎖
? ? ? ? ? ? ? ? redisTemplate.delete(lockName);
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /**
? ? * 獲取本機內(nèi)網(wǎng)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;
? ? }
}
?這里我們給鎖Key設(shè)定了一個和業(yè)務(wù)相關(guān)的唯一標(biāo)示,用于當(dāng)前業(yè)務(wù)分布式鎖的相關(guān)操作肠缨,首先我們通過setIfAbsent也就是SETNX命令去加鎖逆趋,若成功我們給鎖加上失效時間并執(zhí)行業(yè)務(wù)結(jié)束后解鎖,否則重試或者結(jié)束等待下一次任務(wù)周期晒奕。這里我們不將服務(wù)打包多個部署在服務(wù)器上闻书,直接本地修改端口啟動三個項目∧曰郏看下結(jié)果是否和我們預(yù)想一致魄眉。
port:8080
Lock fail,current lock belong to:192.168.126.1:8081
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8081
Lock success,execute business,current time:1562122895372
Lock fail,current lock belong to:192.168.126.1:8082
Lock success,execute business,current time:1562122905350
Lock success,execute business,current time:1562122910334
Lock success,execute business,current time:1562122915340
Lock fail,current lock belong to:192.168.126.1:8082
port:8081
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock success,execute business,current time:1562122940330
port:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock success,execute business,current time:1562122920341
Lock success,execute business,current time:1562122925392
Lock success,execute business,current time:1562122930407
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8081
Lock success,execute business,current time:1562122945340
Lock fail,current lock belong to:192.168.126.1:8080
Lock success,execute business,current time:1562122955339
?當(dāng)我們同時開啟三個服務(wù),模擬分布式項目闷袒,可以看到當(dāng)我們執(zhí)行同一段業(yè)務(wù)代碼時坑律,通過分布式鎖的實現(xiàn)達到了我們預(yù)期的目的,同時只會有一個服務(wù)進行業(yè)務(wù)處理囊骤。
3.分布式鎖實現(xiàn)過程中可能出現(xiàn)的問題以及解決方案
3.1 服務(wù)宕機造成死鎖
?上面我們通過我們之前的設(shè)計思路晃择,去構(gòu)建了一個分布式鎖的實現(xiàn),但是在真實的場景中我們需要考慮更多可能出現(xiàn)的一些問題也物。上面我們實現(xiàn)的思路整體是沒有問題的宫屠,但是還需要考慮一些特殊情況。
?通過以上兩張圖我們可以知道焦除,當(dāng)我們某個服務(wù)在成功獲取鎖之后激况,在還沒有給當(dāng)前鎖設(shè)置失效時間之前服務(wù)宕機,那么該鎖會永久存在,整個業(yè)務(wù)體系會形成死鎖乌逐。我們這里模擬這個業(yè)務(wù)場景竭讳,先同時開啟三個服務(wù),然后當(dāng)某個服務(wù)設(shè)置鎖并未設(shè)置失效時間前我們把他給停止浙踢。
port:8080
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Disconnected from the target VM, address: '127.0.0.1:8606', transport: 'socket'
Lock success,execute business,current time:1562124770986
?當(dāng)8080端口服務(wù)獲取到鎖未設(shè)置鎖失效時間時我們將其停止绢慢。觀察另外兩個服務(wù)獲取鎖情況。
port:8081
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
port:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
?果然另外兩個服務(wù)是會無止盡獲取鎖失敗洛波,進入一個無限循環(huán)拿不到鎖的情況胰舆,此時就出現(xiàn)了我們所說的服務(wù)提供異常造成的死鎖問題,這里我們有幾種解決辦法介紹給大家蹬挤,主要的解決思路都是使SETNX和SETEX包裝成一個整體使其具有原子性來解決缚窿。
3.1.1 Lua腳本命令連用
?Redis2.6.0版本起,通過內(nèi)置的Lua解釋器焰扳,可以使用EVAL命令對Lua腳本進行求值倦零。關(guān)于Lua大家可以自己去了解,使用起來的話很簡單吨悍。Redis使用單個Lua解釋器去運行所有腳本扫茅,并且也保證腳本會以原子性的方式執(zhí)行,即當(dāng)某個腳本正在運行時不會有其他腳本或Redis命令被執(zhí)行育瓜。這和使用MULTI/EXEC包圍的事務(wù)類似葫隙。在其他客戶端看來,腳本的效果要么是不可見的躏仇,要么是已完成的恋脚。關(guān)于EVAL命令使用可以參考Redis 命令參考 ? Script(腳本)。
?通過Redis對Lua腳本保持原子性的支持钙态,我們可以利用此特性去實現(xiàn)SETNX和SETEX并用慧起,包裝成一個整體執(zhí)行。這里我們主要有以下幾個步驟:
資源文件目錄新建.lua文件并且編寫lua腳本
代碼中傳遞參數(shù)執(zhí)行腳本
setnx_ex.lua
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local result_nx = redis.call('SETNX',lockKey,lockValue)
if result_nx == 1 then
? ? local result_ex = redis.call('EXPIRE',lockKey,3600)
? ? return result_ex
else
? ? return result_nx
end
LuaClusterLockJob.java
package com.springboot.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
/**
* lua腳本命令連用 保證原子性
* @author hzk
* @date 2019/7/2
*/
@Component
public class LuaClusterLockJob {
? ? @Autowired
? ? private RedisTemplate redisTemplate;
? ? @Value("${server.port}")
? ? private String port;
? ? public static final String LOCK_PRE = "lock_prefix_lua_";
? ? private DefaultRedisScript<Boolean> lockLuaScript;
? ? @Scheduled(cron = "0/5 * * * * *")
? ? public void lock(){
? ? ? ? String lockName = LOCK_PRE + "LuaClusterLockJob";
? ? ? ? String currentValue = getHostIp() + ":" + port;
? ? ? ? Boolean luaResult = false;
? ? ? ? try {
? ? ? ? ? ? //設(shè)置鎖
? ? ? ? ? ? luaResult = luaScript(lockName,currentValue);
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? //獲取鎖成功册倒,設(shè)置失效時間
? ? ? ? ? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? Thread.sleep(3000);
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? //獲取鎖失敗
? ? ? ? ? ? ? ? String value = (String) redisTemplate.opsForValue().get(lockName);
? ? ? ? ? ? ? ? System.out.println("Lock fail,current lock belong to:" + value);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("ClusterLockJob exception:" + e);
? ? ? ? }finally {
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? ? //若分布式鎖Value與本機Value一致蚓挤,則當(dāng)前機器獲得鎖,進行解鎖
? ? ? ? ? ? ? ? redisTemplate.delete(lockName);
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /**
? ? * 獲取本機內(nèi)網(wǎng)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;
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ? public Boolean luaScript(String key,String value){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("setnx_ex.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
}
?這里我們通過DefaultRedisScript去執(zhí)行我們編寫的Lua腳本灿意,達到了NX和EX連用保證原子性的目的。
3.1.2 RedisConnection命令連用
?由于redisTemplate本身通過valueOperation無法實現(xiàn)命令連用崇呵,但是我們可以通過RedisConnection這種方式去實現(xiàn)命令連用缤剧。
RedisConnectionLockJob .java
package com.springboot.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
/**
* RedisConnection 實現(xiàn)命令連用
* @author hzk
* @date 2019/7/2
*/
@Component
public class RedisConnectionLockJob {
? ? @Autowired
? ? private RedisTemplate redisTemplate;
? ? @Value("${server.port}")
? ? private String port;
? ? public static final String LOCK_PRE = "lock_prefix_redisconnection_";
? ? @Scheduled(cron = "0/5 * * * * *")
? ? public void lock(){
? ? ? ? String lockName = LOCK_PRE + "RedisConnectionLockJob";
? ? ? ? String currentValue = getHostIp() + ":" + port;
? ? ? ? Long timeout = 3600L;
? ? ? ? Boolean result = false;
? ? ? ? try {
? ? ? ? ? ? //設(shè)置鎖
? ? ? ? ? ? result = setLock(lockName,currentValue,timeout);
? ? ? ? ? ? if(result){
? ? ? ? ? ? ? //獲取鎖成功,設(shè)置失效時間
? ? ? ? ? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? Thread.sleep(3000);
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? //獲取鎖失敗
? ? ? ? ? ? ? ? String value = get(lockName);
? ? ? ? ? ? ? ? System.out.println("Lock fail,current lock belong to:" + value);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("ClusterLockJob exception:" + e);
? ? ? ? }finally {
? ? ? ? ? ? if(result){
? ? ? ? ? ? ? ? //若分布式鎖Value與本機Value一致域慷,則當(dāng)前機器獲得鎖荒辕,進行解鎖
? ? ? ? ? ? ? ? redisTemplate.delete(lockName);
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /**
? ? * 獲取本機內(nèi)網(wǎng)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;
? ? }
? ? /**
? ? * 設(shè)置鎖
? ? * @param key
? ? * @param value
? ? * @param timeout
? ? * @return
? ? */
? ? public Boolean setLock(String key,String value,Long timeout){
? ? ? ? try{
? ? ? ? ? ? return (Boolean)redisTemplate.execute(new RedisCallback<Boolean>() {
? ? ? ? ? ? ? ? @Override
? ? ? ? ? ? ? ? public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
? ? ? ? ? ? ? ? ? ? return connection.set(key.getBytes(), value.getBytes(), Expiration.seconds(timeout), RedisStringCommands.SetOption.ifAbsent());
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("setLock Exception:" + e);
? ? ? ? }
? ? ? ? return false;
? ? }
? ? /**
? ? * 獲取Key->Value
? ? * @param key
? ? * @return
? ? */
? ? public String get(String key){
? ? ? ? try{
? ? ? ? ? ? byte[] result = (byte[]) redisTemplate.execute(new RedisCallback<byte[]>() {
? ? ? ? ? ? ? ? @Override
? ? ? ? ? ? ? ? public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
? ? ? ? ? ? ? ? ? ? return connection.get(key.getBytes());
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
? ? ? ? ? ? if(result.length > 0){
? ? ? ? ? ? ? ? return new String(result);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("setLock Exception:" + e);
? ? ? ? }
? ? ? ? return null;
? ? }
}
?我們借助RedisConnection很輕松地實現(xiàn)了命令連用的功能。這里我們主要還是要多參考官方文檔抵窒,查看當(dāng)前版本支持哪些方法調(diào)用弛针。
3.1.3 升級高版本Redis
?其實在提供Redis整合的團隊里,由于分布式鎖頻繁的應(yīng)用也有所改進李皇,在高版本中通過RedisTemplate我們就可以實現(xiàn)NX和EX的連用削茁。
package com.springboot.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
/**
* @author hzk
* @date 2019/7/2
*/
@Component
public class ClusterLockJob {
? ? @Autowired
? ? private RedisTemplate redisTemplate;
? ? @Value("${server.port}")
? ? private String port;
? ? public static final String LOCK_PRE = "lock_prefix_";
? ? @Scheduled(cron = "0/5 * * * * *")
? ? public void lock(){
? ? ? ? String lockName = LOCK_PRE + "ClusterLockJob";
? ? ? ? String currentValue = getHostIp() + ":" + port;
? ? ? ? Boolean ifAbsent = false;
? ? ? ? try {
? ? ? ? ? ? //設(shè)置鎖
? ? ? ? ? ? ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockName, currentValue,3600, TimeUnit.SECONDS);
? ? ? ? ? ? if(ifAbsent){
? ? ? ? ? ? ? //獲取鎖成功,設(shè)置失效時間
? ? ? ? ? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? Thread.sleep(3000);
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? //獲取鎖失敗
? ? ? ? ? ? ? ? String value = (String) redisTemplate.opsForValue().get(lockName);
? ? ? ? ? ? ? ? System.out.println("Lock fail,current lock belong to:" + value);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("ClusterLockJob exception:" + e);
? ? ? ? }finally {
? ? ? ? ? ? if(ifAbsent){
? ? ? ? ? ? ? ? //若分布式鎖Value與本機Value一致掉房,則當(dāng)前機器獲得鎖茧跋,進行解鎖
? ? ? ? ? ? ? ? redisTemplate.delete(lockName);
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /**
? ? * 獲取本機內(nèi)網(wǎng)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;
? ? }
}
?在高版本的使用中卓囚,我們更方便快捷就可以避免命令無法連用造成的問題瘾杭。
3.2 業(yè)務(wù)時間大于鎖超時時間
3.2.1 解鎖錯位問題
?我們試想一個場景,當(dāng)我們A線程加鎖成功執(zhí)行業(yè)務(wù)捍岳,但是由于業(yè)務(wù)時間大于鎖超時時間富寿,當(dāng)鎖超時之后B線程加鎖成功開始執(zhí)行業(yè)務(wù)睬隶,此時A線程業(yè)務(wù)執(zhí)行結(jié)束锣夹,進行解鎖操作。很多同學(xué)此時是沒有考慮這種情況的苏潜,這種情況下就會造成B線程加的鎖被A線程錯位解掉银萍,造成一種無鎖的情況,另外的線程再競爭鎖發(fā)現(xiàn)無鎖又可以進行業(yè)務(wù)操作恤左。
?這里我們主要提供幾個思路贴唇。第一個思路就是在我們解鎖時我們需要比對當(dāng)前鎖的內(nèi)容是否屬于當(dāng)前線程鎖加的鎖,若是才進行解鎖操作飞袋。第二個思路就是我們在鎖內(nèi)容比較時需要先從Redis中取出當(dāng)前鎖內(nèi)容戳气,如果此時取值仍然為A線程占用,當(dāng)前取值就是A線程的鎖內(nèi)容巧鸭,但是在下一刻鎖超時導(dǎo)致B線程拿到了鎖瓶您,此時A鎖取到的值就是一個臟數(shù)據(jù),所以我們要通過之前我們解決問題的思想纲仍,將取值和比較以及解鎖封裝成一個原子性操作呀袱。這里我們依然通過Lua腳本可以實現(xiàn),來看下如何達到這種目的的吧郑叠。
release_lock.lua
--
-- Created by IntelliJ IDEA.
-- User: hzk
-- Date: 2019/7/3
-- Time: 18:31
-- To change this template use File | Settings | File Templates.
--
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local result_get = redis.call('get',lockKey);
if lockValue == result_get then
? ? local result_del = redis.call('del',lockKey)
? ? return result_del
else
? ? return false;
end
LuaClusterLockJob2.java
package com.springboot.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
/**
* lua腳本命令連用 保證原子性
* @author hzk
* @date 2019/7/2
*/
@Component
public class LuaClusterLockJob2 {
? ? @Autowired
? ? private RedisTemplate redisTemplate;
? ? @Value("${server.port}")
? ? private String port;
? ? public static final String LOCK_PRE = "lock_prefix_lua_";
? ? private DefaultRedisScript<Boolean> lockLuaScript;
? ? @Scheduled(cron = "0/5 * * * * *")
? ? public void lock(){
? ? ? ? String lockName = LOCK_PRE + "LuaClusterLockJob2";
? ? ? ? String currentValue = getHostIp() + ":" + port;
? ? ? ? Boolean luaResult = false;
? ? ? ? try {
? ? ? ? ? ? //設(shè)置鎖
? ? ? ? ? ? luaResult = luaScript(lockName,currentValue);
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? //獲取鎖成功夜赵,設(shè)置失效時間
? ? ? ? ? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? Thread.sleep(3000);
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? //獲取鎖失敗
? ? ? ? ? ? ? ? String value = (String) redisTemplate.opsForValue().get(lockName);
? ? ? ? ? ? ? ? System.out.println("Lock fail,current lock belong to:" + value);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("LuaClusterLockJob2 exception:" + e);
? ? ? ? }finally {
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? ? //若分布式鎖Value與本機Value一致,則當(dāng)前機器獲得鎖乡革,進行解鎖
//? ? ? ? ? ? ? ? redisTemplate.delete(lockName);
? ? ? ? ? ? ? ? Boolean releaseLock = luaScriptReleaseLock(lockName, currentValue);
if(releaseLock){
? ? ? ? ? ? ? ? ? ? System.out.println("release lock success");
? ? ? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? ? ? System.out.println("release lock fail");
? ? ? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /**
? ? * 獲取本機內(nèi)網(wǎng)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;
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ? public Boolean luaScript(String key,String value){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("setnx_ex.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本(釋放鎖)
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ? public Boolean luaScriptReleaseLock(String key,String value){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("release_lock.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
}
?通過Lua腳本我們解決了這個問題摊腋,大家可以動手試試。
3.2.2 業(yè)務(wù)并發(fā)執(zhí)行問題
?我們大家在使用分布式鎖的時候需要思考一個問題嘁傀,那就是鎖超時時間如何設(shè)置歌豺?如果業(yè)務(wù)中包含了一些請求或者排隊操作,都可能會導(dǎo)致業(yè)務(wù)時間被大幅拉長心包,業(yè)務(wù)并未執(zhí)行完成鎖就已經(jīng)失效类咧,此時就可能會出現(xiàn)多個業(yè)務(wù)同時在執(zhí)行的情況。如果對并發(fā)要求嚴(yán)格的業(yè)務(wù)蟹腾,那這就是不可接受的痕惋,所以我們就需要去思考如何才能避免這種情況。
?在整個設(shè)計中娃殖,我們的思路主要是通過開啟一個守護線程去周期性進行檢測續(xù)時值戳,直接上代碼更直觀,這里有幾個需要注意的地方:
在續(xù)時鎖的時候炉爆,我們需要檢測當(dāng)前鎖需要續(xù)時的鎖是否是當(dāng)前線程鎖占有堕虹,此時涉及取值和設(shè)時兩個操作,考慮到之前的并發(fā)情況芬首,我們?nèi)匀徊捎肔ua腳本去實現(xiàn)續(xù)時赴捞。
開啟的守護線程執(zhí)行頻率需要控制,不可頻繁執(zhí)行造成資源浪費郁稍,我們這里以2/3過期時間周期去檢測執(zhí)行赦政。
當(dāng)我們業(yè)務(wù)執(zhí)行完成,該守護線程需要被銷毀耀怜,不可無限制執(zhí)行恢着。
ExpandLockExpireTask .java
package com.springboot.task;
import com.springboot.schedule.LuaClusterLockJob2;
/**
* 鎖續(xù)時任務(wù)
* @author hzk
* @date 2019/7/4
*/
public class ExpandLockExpireTask implements Runnable {
? ? private String key;
? ? private String value;
? ? private long expire;
? ? private boolean isRunning;
? ? private LuaClusterLockJob2 luaClusterLockJob2;
? ? public ExpandLockExpireTask(String key, String value, long expire, LuaClusterLockJob2 luaClusterLockJob2) {
? ? ? ? this.key = key;
? ? ? ? this.value = value;
? ? ? ? this.expire = expire;
? ? ? ? this.luaClusterLockJob2 = luaClusterLockJob2;
? ? ? ? this.isRunning = true;
? ? }
? ? @Override
? ? public void run() {
? ? ? ? //任務(wù)執(zhí)行周期
? ? ? ? long waitTime = expire * 1000 * 2 / 3;
? ? ? ? while (isRunning){
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? Thread.sleep(waitTime);
? ? ? ? ? ? ? ? if(luaClusterLockJob2.luaScriptExpandLockExpire(key,value,expire)){
? ? ? ? ? ? ? ? ? ? System.out.println("Lock expand expire success! " + value);
? ? ? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? ? ? stopTask();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? private void stopTask(){
? ? ? ? isRunning = false;
? ? }
}
LuaClusterLockJob2.java
package com.springboot.schedule;
import com.springboot.task.ExpandLockExpireTask;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
/**
* lua腳本命令連用 保證原子性
* @author hzk
* @date 2019/7/2
*/
@Component
public class LuaClusterLockJob2 {
? ? @Autowired
? ? private RedisTemplate redisTemplate;
? ? @Value("${server.port}")
? ? private String port;
? ? public static final String LOCK_PRE = "lock_prefix_lua_";
? ? private DefaultRedisScript<Boolean> lockLuaScript;
? ? @Scheduled(cron = "0/5 * * * * *")
? ? public void lock(){
? ? ? ? String lockName = LOCK_PRE + "LuaClusterLockJob2";
? ? ? ? String currentValue = getHostIp() + ":" + port;
? ? ? ? Boolean luaResult = false;
? ? ? ? Long expire = 30L;
? ? ? ? try {
? ? ? ? ? ? //設(shè)置鎖
? ? ? ? ? ? luaResult = luaScript(lockName,currentValue,expire);
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? //獲取鎖成功,設(shè)置失效時間
? ? ? ? ? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? //開啟守護線程 定期檢測 續(xù)鎖
? ? ? ? ? ? ? ? ExpandLockExpireTask expandLockExpireTask = new ExpandLockExpireTask(lockName,currentValue,expire,this);
? ? ? ? ? ? ? ? Thread thread = new Thread(expandLockExpireTask);
? ? ? ? ? ? ? ? thread.setDaemon(true);
? ? ? ? ? ? ? ? thread.start();
? ? ? ? ? ? ? ? Thread.sleep(600 * 1000);
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? //獲取鎖失敗
? ? ? ? ? ? ? ? String value = (String) redisTemplate.opsForValue().get(lockName);
? ? ? ? ? ? ? ? System.out.println("Lock fail,current lock belong to:" + value);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("LuaClusterLockJob2 exception:" + e);
? ? ? ? }finally {
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? ? //若分布式鎖Value與本機Value一致财破,則當(dāng)前機器獲得鎖掰派,進行解鎖
//? ? ? ? ? ? ? ? redisTemplate.delete(lockName);
? ? ? ? ? ? ? ? Boolean releaseLock = luaScriptReleaseLock(lockName, currentValue);
? ? ? ? ? ? ? ? if(releaseLock){
? ? ? ? ? ? ? ? ? ? System.out.println("release lock success");
? ? ? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? ? ? System.out.println("release lock fail");
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /**
? ? * 獲取本機內(nèi)網(wǎng)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;
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ? public Boolean luaScript(String key,String value,Long expire){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("setnx_ex.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? params.add(String.valueOf(expire));
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本(釋放鎖)
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ? public Boolean luaScriptReleaseLock(String key,String value){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("release_lock.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本(鎖續(xù)時)
? ? * @param key
? ? * @param value
? ? * @param expire
? ? * @return
? ? */
? ? public Boolean luaScriptExpandLockExpire(String key,String value,Long expire){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("expand_lock_expire.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? params.add(String.valueOf(expire));
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
}
setnx_ex.lua
--
-- Created by IntelliJ IDEA.
-- User: hzk
-- Date: 2019/7/4
-- Time: 15:19
-- To change this template use File | Settings | File Templates.
--
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local expire = KEYS[3]
local result_nx = redis.call('SETNX',lockKey,lockValue)
if result_nx == 1 then
? ? local result_ex = redis.call('EXPIRE',lockKey,expire)
? ? return result_ex
else
? ? return result_nx
end
release_lock.lua
--
-- Created by IntelliJ IDEA.
-- User: hzk
-- Date: 2019/7/3
-- Time: 18:31
-- To change this template use File | Settings | File Templates.
--
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local result_get = redis.call('get',lockKey);
if lockValue == result_get then
? ? local result_del = redis.call('del',lockKey)
? ? return result_del
else
? ? return false;
end
expand_lock_expire.lua
--
-- Created by IntelliJ IDEA.
-- User: hzk
-- Date: 2019/7/4
-- Time: 15:19
-- To change this template use File | Settings | File Templates.
--
local lockKey = KEYS[1]
local lockValue = KEYS[2]
local expire = KEYS[3]
local result_get = redis.call('GET',lockKey);
if lockValue == result_get then
? ? local result_expire = redis.call('EXPIRE',lockKey,expire)
? ? return result_expire
else
? ? return false;
end
?這里大家可以自己動手去實現(xiàn)驗證左痢,當(dāng)我們設(shè)置30s過期時間靡羡,業(yè)務(wù)執(zhí)行時間設(shè)置遠大于30s時,是否每20s會進行一次續(xù)時操作抖锥。
3.2.3 可重入鎖
?我們之前在考慮服務(wù)崩潰或者服務(wù)器宕機時亿眠,想到了鎖會變成永久性質(zhì),造成死鎖的情況以及如何去解決磅废。這里我們再細想一下纳像,如果我們A服務(wù)獲取到鎖并且設(shè)置成功失效時間,此時服務(wù)宕機拯勉,那么其他所有服務(wù)都需要等待一個周期之后才會有新的業(yè)務(wù)可以獲取鎖去執(zhí)行竟趾。這里我們就要考慮一個可重入性憔购,若我們當(dāng)前A服務(wù)崩潰之后立刻恢復(fù),那么我們是否需要允許該服務(wù)可以重新獲取該鎖權(quán)限岔帽,實現(xiàn)起來很簡單玫鸟,只需要在加鎖失敗之后驗證當(dāng)前鎖內(nèi)容是否和當(dāng)前服務(wù)所匹配即可。
LuaClusterLockJob2 .java
package com.springboot.schedule;
import com.springboot.task.ExpandLockExpireTask;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
/**
* lua腳本命令連用 保證原子性
* @author hzk
* @date 2019/7/2
*/
@Component
public class LuaClusterLockJob2 {
? ? @Autowired
? ? private RedisTemplate redisTemplate;
? ? @Value("${server.port}")
? ? private String port;
? ? public static final String LOCK_PRE = "lock_prefix_lua_";
? ? private DefaultRedisScript<Boolean> lockLuaScript;
? ? @Scheduled(cron = "0/5 * * * * *")
? ? public void lock(){
? ? ? ? String lockName = LOCK_PRE + "LuaClusterLockJob2";
? ? ? ? String currentValue = getHostIp() + ":" + port;
? ? ? ? Boolean luaResult = false;
? ? ? ? Long expire = 60L;
? ? ? ? try {
? ? ? ? ? ? //設(shè)置鎖
? ? ? ? ? ? luaResult = luaScript(lockName,currentValue,expire);
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? ? //開啟守護線程 定期檢測 續(xù)鎖
? ? ? ? ? ? ? ? executeBusiness(lockName,currentValue,expire);
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? //獲取鎖失敗
? ? ? ? ? ? ? ? String value = (String) redisTemplate.opsForValue().get(lockName);
? ? ? ? ? ? ? ? //校驗鎖內(nèi)容 支持可重入性
? ? ? ? ? ? ? ? if(currentValue.equals(value)){
? ? ? ? ? ? ? ? ? ? Boolean expireResult = redisTemplate.expire(lockName, expire, TimeUnit.SECONDS);
? ? ? ? ? ? ? ? ? ? if(expireResult){
? ? ? ? ? ? ? ? ? ? ? ? executeBusiness(lockName,currentValue,expire);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? System.out.println("Lock fail,current lock belong to:" + value);
? ? ? ? ? ? }
? ? ? ? }catch (Exception e){
? ? ? ? ? ? System.out.println("LuaClusterLockJob2 exception:" + e);
? ? ? ? }finally {
? ? ? ? ? ? if(luaResult){
? ? ? ? ? ? ? ? //若分布式鎖Value與本機Value一致犀勒,則當(dāng)前機器獲得鎖屎飘,進行解鎖
//? ? ? ? ? ? ? ? redisTemplate.delete(lockName);
? ? ? ? ? ? ? ? Boolean releaseLock = luaScriptReleaseLock(lockName, currentValue);
? ? ? ? ? ? ? ? if(releaseLock){
? ? ? ? ? ? ? ? ? ? System.out.println("release lock success");
? ? ? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? ? ? System.out.println("release lock fail");
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /**
? ? * 獲取本機內(nèi)網(wǎng)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;
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ? public Boolean luaScript(String key,String value,Long expire){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("setnx_ex.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? params.add(String.valueOf(expire));
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本(釋放鎖)
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ? public Boolean luaScriptReleaseLock(String key,String value){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("release_lock.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
? ? /**
? ? * 執(zhí)行l(wèi)ua腳本(鎖續(xù)時)
? ? * @param key
? ? * @param value
? ? * @param expire
? ? * @return
? ? */
? ? public Boolean luaScriptExpandLockExpire(String key,String value,Long expire){
? ? ? ? lockLuaScript = new DefaultRedisScript<Boolean>();
? ? ? ? lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("expand_lock_expire.lua")));
? ? ? ? lockLuaScript.setResultType(Boolean.class);
? ? ? ? //封裝傳遞腳本參數(shù)
? ? ? ? ArrayList<Object> params = new ArrayList<>();
? ? ? ? params.add(key);
? ? ? ? params.add(value);
? ? ? ? params.add(String.valueOf(expire));
? ? ? ? return (Boolean) redisTemplate.execute(lockLuaScript, params);
? ? }
? ? /**
? ? * 執(zhí)行業(yè)務(wù)
? ? * @param lockName
? ? * @param currentValue
? ? * @param expire
? ? */
? ? private void executeBusiness(String lockName,String currentValue,Long expire) throws InterruptedException {
? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
? ? ? ? //開啟守護線程 定期檢測 續(xù)鎖
? ? ? ? ExpandLockExpireTask expandLockExpireTask = new ExpandLockExpireTask(lockName,currentValue,expire,this);
? ? ? ? Thread thread = new Thread(expandLockExpireTask);
? ? ? ? thread.setDaemon(true);
? ? ? ? thread.start();
? ? ? ? Thread.sleep(600 * 1000);
? ? }
}
4.總結(jié)
?通過循序漸進對分布式鎖的了解以及如果動手去實現(xiàn)贾费,想必大家都有了一個比較清晰的了解钦购。這里我們還針對在整個分布式鎖應(yīng)用中可能存在的一些問題進行了分析以及解決。其實關(guān)于分布式鎖的實現(xiàn)方式還有很多褂萧,這里我們只是針對Redis實現(xiàn)了分布式鎖押桃,并且可能還有一些我們沒有考慮到的問題,只有在實際應(yīng)用中才會深入去研究探索导犹。近年來分布式系統(tǒng)越來越流行的情況下唱凯,分布式鎖出現(xiàn)頻率已經(jīng)十分頻繁,所以大家有精力還是可以去補充這方面的知識谎痢,之后我可能會介紹一下其他實現(xiàn)分布式鎖的方式磕昼,如果有問題還希望大家提出,我也可以學(xué)習(xí)改正舶得,共同進步掰烟。
原文鏈接:https://blog.csdn.net/u013985664/article/details/94459529