Redis - 分布式鎖實現(xiàn)以及相關(guān)問題解決方案

文章目錄

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沐批,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蝎亚,老刑警劉巖九孩,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異发框,居然都是意外死亡躺彬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門梅惯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宪拥,“玉大人,你說我怎么就攤上這事铣减∷” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵葫哗,是天一觀的道長缔刹。 經(jīng)常有香客問我球涛,道長,這世上最難降的妖魔是什么校镐? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任亿扁,我火速辦了婚禮,結(jié)果婚禮上鸟廓,老公的妹妹穿的比我還像新娘从祝。我一直安慰自己,他們只是感情好引谜,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布哄褒。 她就那樣靜靜地躺著,像睡著了一般煌张。 火紅的嫁衣襯著肌膚如雪呐赡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天骏融,我揣著相機與錄音链嘀,去河邊找鬼。 笑死档玻,一個胖子當(dāng)著我的面吹牛怀泊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播误趴,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼霹琼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凉当?” 一聲冷哼從身側(cè)響起枣申,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎看杭,沒想到半個月后忠藤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡楼雹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年模孩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贮缅。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡榨咐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谴供,到底是詐尸還是另有隱情块茁,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布憔鬼,位于F島的核電站龟劲,受9級特大地震影響胃夏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昌跌,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一仰禀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚕愤,春花似錦答恶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至裕坊,卻和暖如春包竹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背籍凝。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工周瞎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饵蒂。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓声诸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親退盯。 傳聞我的和親對象是個殘疾皇子彼乌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容