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

文章目錄

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

1.分布式鎖是什么?

1.1 分布式鎖設(shè)計(jì)目的

1.2 分布式鎖設(shè)計(jì)要求

1.3 分布式鎖設(shè)計(jì)思路

2.分布式鎖實(shí)現(xiàn)

3.分布式鎖實(shí)現(xiàn)過(guò)程中可能出現(xiàn)的問(wèn)題以及解決方案

3.1 服務(wù)宕機(jī)造成死鎖

3.1.1 Lua腳本命令連用

3.1.2 RedisConnection命令連用

3.1.3 升級(jí)高版本Redis

3.2 業(yè)務(wù)時(shí)間大于鎖超時(shí)時(shí)間

3.2.1 解鎖錯(cuò)位問(wèn)題

3.2.2 業(yè)務(wù)并發(fā)執(zhí)行問(wèn)題

3.2.3 可重入鎖

4.總結(jié)

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

1.分布式鎖是什么?

?分布式鎖是控制分布式系統(tǒng)或不同系統(tǒng)之間共同訪問(wèn)共享資源的一種鎖實(shí)現(xiàn)。如果不同的系統(tǒng)或同一個(gè)系統(tǒng)的不同主機(jī)之間共享了某個(gè)資源時(shí),往往通過(guò)互斥來(lái)防止彼此之間的干擾。實(shí)現(xiàn)分布式鎖的方式有很多,可以通過(guò)各種中間件來(lái)進(jìn)行分布式鎖的設(shè)計(jì),包括Redis、Zookeeper等,這里我們主要介紹Redis如何實(shí)現(xiàn)分布式鎖以及在整個(gè)過(guò)程中出現(xiàn)的問(wèn)題和優(yōu)化解決方案。

1.1 分布式鎖設(shè)計(jì)目的

?可以保證在分布式部署的應(yīng)用集群中,同一個(gè)方法的同一操作只能被一臺(tái)機(jī)器上的一個(gè)線程執(zhí)行。分布式鎖至少包含以下三點(diǎn):

具有互斥性。任意時(shí)刻只有一個(gè)服務(wù)持有鎖。

不會(huì)死鎖。即使持有鎖的服務(wù)異常崩潰沒(méi)有主動(dòng)解鎖后續(xù)也能夠保證其他服務(wù)可以拿到鎖。

加鎖和解鎖都需要是同一個(gè)服務(wù)。

1.2 分布式鎖設(shè)計(jì)要求

分布式鎖要是一把可重入鎖(同時(shí)需避免死鎖)。

分布式鎖有高可用的獲取鎖和釋放鎖功能。

分布式鎖獲取鎖和釋放鎖的性能要好

1.3 分布式鎖設(shè)計(jì)思路

使用SETNX命令獲取鎖(Key存在則返回0,不存在并且設(shè)置成功返回1)。

若返回0則不進(jìn)行業(yè)務(wù)操作,若返回1則設(shè)置鎖Value為當(dāng)前服務(wù)器IP + 業(yè)務(wù)標(biāo)識(shí),用于鎖釋放和鎖延期時(shí)判斷。同時(shí)使用EXPIRE命令給鎖設(shè)置一個(gè)合理的過(guò)期時(shí)間,避免當(dāng)前服務(wù)宕機(jī)鎖永久存在造成死鎖,并且設(shè)計(jì)需要保證可重入性。

執(zhí)行業(yè)務(wù),業(yè)務(wù)執(zhí)行完成判斷當(dāng)前鎖Value是否為當(dāng)前服務(wù)器IP + 業(yè)務(wù)標(biāo)識(shí),若相同則通過(guò)DEL或者EXPIRE設(shè)置為0釋放當(dāng)前鎖。

2.分布式鎖實(shí)現(xiàn)

?我們?cè)趯?shí)現(xiàn)分布式鎖的過(guò)程中大致思路就是上圖的整個(gè)流程,這里我們主要記住幾個(gè)要點(diǎn):

鎖一定要設(shè)置失效時(shí)間,否則服務(wù)宕機(jī)鎖就會(huì)永久性存在,整個(gè)業(yè)務(wù)體系死鎖。

業(yè)務(wù)執(zhí)行完必須解鎖,可將加鎖和業(yè)務(wù)代碼放置try/catch中,解鎖流程放置finally中。

?若要用jar包方式后臺(tái)啟動(dòng)服務(wù),可用 nohup java -jar jar包名稱(chēng) &命令。這里我們來(lái)看一下我們加解鎖的主要代碼。

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è)置失效時(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與本機(jī)Value一致,則當(dāng)前機(jī)器獲得鎖,進(jìn)行解鎖

? ? ? ? ? ? ? ? redisTemplate.delete(lockName);

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ? * 獲取本機(jī)內(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地址即本機(jī)地址,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è)定了一個(gè)和業(yè)務(wù)相關(guān)的唯一標(biāo)示,用于當(dāng)前業(yè)務(wù)分布式鎖的相關(guān)操作,首先我們通過(guò)setIfAbsent也就是SETNX命令去加鎖,若成功我們給鎖加上失效時(shí)間并執(zhí)行業(yè)務(wù)結(jié)束后解鎖,否則重試或者結(jié)束等待下一次任務(wù)周期。這里我們不將服務(wù)打包多個(gè)部署在服務(wù)器上,直接本地修改端口啟動(dòng)三個(gè)項(xiàng)目??聪陆Y(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)我們同時(shí)開(kāi)啟三個(gè)服務(wù),模擬分布式項(xiàng)目,可以看到當(dāng)我們執(zhí)行同一段業(yè)務(wù)代碼時(shí),通過(guò)分布式鎖的實(shí)現(xiàn)達(dá)到了我們預(yù)期的目的,同時(shí)只會(huì)有一個(gè)服務(wù)進(jìn)行業(yè)務(wù)處理。

3.分布式鎖實(shí)現(xiàn)過(guò)程中可能出現(xiàn)的問(wèn)題以及解決方案

3.1 服務(wù)宕機(jī)造成死鎖

?上面我們通過(guò)我們之前的設(shè)計(jì)思路,去構(gòu)建了一個(gè)分布式鎖的實(shí)現(xiàn),但是在真實(shí)的場(chǎng)景中我們需要考慮更多可能出現(xiàn)的一些問(wèn)題。上面我們實(shí)現(xiàn)的思路整體是沒(méi)有問(wèn)題的,但是還需要考慮一些特殊情況。

?通過(guò)以上兩張圖我們可以知道,當(dāng)我們某個(gè)服務(wù)在成功獲取鎖之后,在還沒(méi)有給當(dāng)前鎖設(shè)置失效時(shí)間之前服務(wù)宕機(jī),那么該鎖會(huì)永久存在,整個(gè)業(yè)務(wù)體系會(huì)形成死鎖。我們這里模擬這個(gè)業(yè)務(wù)場(chǎng)景,先同時(shí)開(kāi)啟三個(gè)服務(wù),然后當(dāng)某個(gè)服務(wù)設(shè)置鎖并未設(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è)置鎖失效時(shí)間時(shí)我們將其停止。觀察另外兩個(gè)服務(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

?果然另外兩個(gè)服務(wù)是會(huì)無(wú)止盡獲取鎖失敗,進(jìn)入一個(gè)無(wú)限循環(huán)拿不到鎖的情況,此時(shí)就出現(xiàn)了我們所說(shuō)的服務(wù)提供異常造成的死鎖問(wèn)題,這里我們有幾種解決辦法介紹給大家,主要的解決思路都是使SETNX和SETEX包裝成一個(gè)整體使其具有原子性來(lái)解決。

3.1.1 Lua腳本命令連用

?Redis2.6.0版本起,通過(guò)內(nèi)置的Lua解釋器,可以使用EVAL命令對(duì)Lua腳本進(jìn)行求值。關(guān)于Lua大家可以自己去了解,使用起來(lái)的話很簡(jiǎn)單。Redis使用單個(gè)Lua解釋器去運(yùn)行所有腳本,并且也保證腳本會(huì)以原子性的方式執(zhí)行,即當(dāng)某個(gè)腳本正在運(yùn)行時(shí)不會(huì)有其他腳本或Redis命令被執(zhí)行。這和使用MULTI/EXEC包圍的事務(wù)類(lèi)似。在其他客戶端看來(lái),腳本的效果要么是不可見(jiàn)的,要么是已完成的。關(guān)于EVAL命令使用可以參考Redis 命令參考 ? Script(腳本)。

?通過(guò)Redis對(duì)Lua腳本保持原子性的支持,我們可以利用此特性去實(shí)現(xiàn)SETNX和SETEX并用,包裝成一個(gè)整體執(zhí)行。這里我們主要有以下幾個(gè)步驟:

資源文件目錄新建.lua文件并且編寫(xiě)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è)置失效時(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與本機(jī)Value一致,則當(dāng)前機(jī)器獲得鎖,進(jìn)行解鎖

? ? ? ? ? ? ? ? redisTemplate.delete(lockName);

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ? * 獲取本機(jī)內(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地址即本機(jī)地址,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);

? ? }

}

?這里我們通過(guò)DefaultRedisScript去執(zhí)行我們編寫(xiě)的Lua腳本,達(dá)到了NX和EX連用保證原子性的目的。

3.1.2 RedisConnection命令連用

?由于redisTemplate本身通過(guò)valueOperation無(wú)法實(shí)現(xiàn)命令連用,但是我們可以通過(guò)RedisConnection這種方式去實(shí)現(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 實(shí)現(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è)置失效時(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與本機(jī)Value一致,則當(dāng)前機(jī)器獲得鎖,進(jìn)行解鎖

? ? ? ? ? ? ? ? redisTemplate.delete(lockName);

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ? * 獲取本機(jī)內(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地址即本機(jī)地址,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很輕松地實(shí)現(xiàn)了命令連用的功能。這里我們主要還是要多參考官方文檔,查看當(dāng)前版本支持哪些方法調(diào)用。

3.1.3 升級(jí)高版本Redis

?其實(shí)在提供Redis整合的團(tuán)隊(duì)里,由于分布式鎖頻繁的應(yīng)用也有所改進(jìn),在高版本中通過(guò)RedisTemplate我們就可以實(shí)現(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è)置失效時(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與本機(jī)Value一致,則當(dāng)前機(jī)器獲得鎖,進(jìn)行解鎖

? ? ? ? ? ? ? ? redisTemplate.delete(lockName);

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ? * 獲取本機(jī)內(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地址即本機(jī)地址,IPv4的loopback范圍是127.0.0.0 ~ 127.255.255.255

? ? ? ? ? ? ? ? ? ? ? ? ? ? && ip.getHostAddress().indexOf(":")==-1){

? ? ? ? ? ? ? ? ? ? ? ? return ip.getHostAddress();

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }catch(Exception e){

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? ? ? return null;

? ? }

}

?在高版本的使用中,我們更方便快捷就可以避免命令無(wú)法連用造成的問(wèn)題。

3.2 業(yè)務(wù)時(shí)間大于鎖超時(shí)時(shí)間

3.2.1 解鎖錯(cuò)位問(wèn)題

?我們?cè)囅胍粋€(gè)場(chǎng)景,當(dāng)我們A線程加鎖成功執(zhí)行業(yè)務(wù),但是由于業(yè)務(wù)時(shí)間大于鎖超時(shí)時(shí)間,當(dāng)鎖超時(shí)之后B線程加鎖成功開(kāi)始執(zhí)行業(yè)務(wù),此時(shí)A線程業(yè)務(wù)執(zhí)行結(jié)束,進(jìn)行解鎖操作。很多同學(xué)此時(shí)是沒(méi)有考慮這種情況的,這種情況下就會(huì)造成B線程加的鎖被A線程錯(cuò)位解掉,造成一種無(wú)鎖的情況,另外的線程再競(jìng)爭(zhēng)鎖發(fā)現(xiàn)無(wú)鎖又可以進(jìn)行業(yè)務(wù)操作。

?這里我們主要提供幾個(gè)思路。第一個(gè)思路就是在我們解鎖時(shí)我們需要比對(duì)當(dāng)前鎖的內(nèi)容是否屬于當(dāng)前線程鎖加的鎖,若是才進(jìn)行解鎖操作。第二個(gè)思路就是我們?cè)阪i內(nèi)容比較時(shí)需要先從Redis中取出當(dāng)前鎖內(nèi)容,如果此時(shí)取值仍然為A線程占用,當(dāng)前取值就是A線程的鎖內(nèi)容,但是在下一刻鎖超時(shí)導(dǎo)致B線程拿到了鎖,此時(shí)A鎖取到的值就是一個(gè)臟數(shù)據(jù),所以我們要通過(guò)之前我們解決問(wèn)題的思想,將取值和比較以及解鎖封裝成一個(gè)原子性操作。這里我們依然通過(guò)Lua腳本可以實(shí)現(xiàn),來(lái)看下如何達(dá)到這種目的的吧。

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è)置失效時(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與本機(jī)Value一致,則當(dāng)前機(jī)器獲得鎖,進(jìn)行解鎖

//? ? ? ? ? ? ? ? redisTemplate.delete(lockName);

? ? ? ? ? ? ? ? Boolean releaseLock = luaScriptReleaseLock(lockName, currentValue);

if(releaseLock){

? ? ? ? ? ? ? ? ? ? System.out.println("release lock success");

? ? ? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? ? ? System.out.println("release lock fail");

? ? ? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ? * 獲取本機(jī)內(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地址即本機(jī)地址,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);

? ? }

}

?通過(guò)Lua腳本我們解決了這個(gè)問(wèn)題,大家可以動(dòng)手試試。

3.2.2 業(yè)務(wù)并發(fā)執(zhí)行問(wèn)題

?我們大家在使用分布式鎖的時(shí)候需要思考一個(gè)問(wèn)題,那就是鎖超時(shí)時(shí)間如何設(shè)置?如果業(yè)務(wù)中包含了一些請(qǐng)求或者排隊(duì)操作,都可能會(huì)導(dǎo)致業(yè)務(wù)時(shí)間被大幅拉長(zhǎng),業(yè)務(wù)并未執(zhí)行完成鎖就已經(jīng)失效,此時(shí)就可能會(huì)出現(xiàn)多個(gè)業(yè)務(wù)同時(shí)在執(zhí)行的情況。如果對(duì)并發(fā)要求嚴(yán)格的業(yè)務(wù),那這就是不可接受的,所以我們就需要去思考如何才能避免這種情況。

?在整個(gè)設(shè)計(jì)中,我們的思路主要是通過(guò)開(kāi)啟一個(gè)守護(hù)線程去周期性進(jìn)行檢測(cè)續(xù)時(shí),直接上代碼更直觀,這里有幾個(gè)需要注意的地方:

在續(xù)時(shí)鎖的時(shí)候,我們需要檢測(cè)當(dāng)前鎖需要續(xù)時(shí)的鎖是否是當(dāng)前線程鎖占有,此時(shí)涉及取值和設(shè)時(shí)兩個(gè)操作,考慮到之前的并發(fā)情況,我們?nèi)匀徊捎肔ua腳本去實(shí)現(xiàn)續(xù)時(shí)。

開(kāi)啟的守護(hù)線程執(zhí)行頻率需要控制,不可頻繁執(zhí)行造成資源浪費(fèi),我們這里以2/3過(guò)期時(shí)間周期去檢測(cè)執(zhí)行。

當(dāng)我們業(yè)務(wù)執(zhí)行完成,該守護(hù)線程需要被銷(xiāo)毀,不可無(wú)限制執(zhí)行。

ExpandLockExpireTask .java

package com.springboot.task;

import com.springboot.schedule.LuaClusterLockJob2;

/**

* 鎖續(xù)時(shí)任務(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è)置失效時(shí)間

? ? ? ? ? ? ? ? System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());

? ? ? ? ? ? ? ? //開(kāi)啟守護(hù)線程 定期檢測(cè) 續(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與本機(jī)Value一致,則當(dāng)前機(jī)器獲得鎖,進(jìn)行解鎖

//? ? ? ? ? ? ? ? redisTemplate.delete(lockName);

? ? ? ? ? ? ? ? Boolean releaseLock = luaScriptReleaseLock(lockName, currentValue);

? ? ? ? ? ? ? ? if(releaseLock){

? ? ? ? ? ? ? ? ? ? System.out.println("release lock success");

? ? ? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? ? ? System.out.println("release lock fail");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ? * 獲取本機(jī)內(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地址即本機(jī)地址,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ù)時(shí))

? ? * @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

?這里大家可以自己動(dòng)手去實(shí)現(xiàn)驗(yàn)證,當(dāng)我們?cè)O(shè)置30s過(guò)期時(shí)間,業(yè)務(wù)執(zhí)行時(shí)間設(shè)置遠(yuǎn)大于30s時(shí),是否每20s會(huì)進(jìn)行一次續(xù)時(shí)操作。

3.2.3 可重入鎖

?我們之前在考慮服務(wù)崩潰或者服務(wù)器宕機(jī)時(shí),想到了鎖會(huì)變成永久性質(zhì),造成死鎖的情況以及如何去解決。這里我們?cè)偌?xì)想一下,如果我們A服務(wù)獲取到鎖并且設(shè)置成功失效時(shí)間,此時(shí)服務(wù)宕機(jī),那么其他所有服務(wù)都需要等待一個(gè)周期之后才會(huì)有新的業(yè)務(wù)可以獲取鎖去執(zhí)行。這里我們就要考慮一個(gè)可重入性,若我們當(dāng)前A服務(wù)崩潰之后立刻恢復(fù),那么我們是否需要允許該服務(wù)可以重新獲取該鎖權(quán)限,實(shí)現(xiàn)起來(lái)很簡(jiǎn)單,只需要在加鎖失敗之后驗(yà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){

? ? ? ? ? ? ? ? //開(kāi)啟守護(hù)線程 定期檢測(cè) 續(xù)鎖

? ? ? ? ? ? ? ? executeBusiness(lockName,currentValue,expire);

? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? //獲取鎖失敗

? ? ? ? ? ? ? ? String value = (String) redisTemplate.opsForValue().get(lockName);

? ? ? ? ? ? ? ? //校驗(yàn)鎖內(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與本機(jī)Value一致,則當(dāng)前機(jī)器獲得鎖,進(jìn)行解鎖

//? ? ? ? ? ? ? ? redisTemplate.delete(lockName);

? ? ? ? ? ? ? ? Boolean releaseLock = luaScriptReleaseLock(lockName, currentValue);

? ? ? ? ? ? ? ? if(releaseLock){

? ? ? ? ? ? ? ? ? ? System.out.println("release lock success");

? ? ? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? ? ? System.out.println("release lock fail");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ? * 獲取本機(jī)內(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地址即本機(jī)地址,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ù)時(shí))

? ? * @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());

? ? ? ? //開(kāi)啟守護(hù)線程 定期檢測(cè) 續(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é)

?通過(guò)循序漸進(jìn)對(duì)分布式鎖的了解以及如果動(dòng)手去實(shí)現(xiàn),想必大家都有了一個(gè)比較清晰的了解。這里我們還針對(duì)在整個(gè)分布式鎖應(yīng)用中可能存在的一些問(wèn)題進(jìn)行了分析以及解決。其實(shí)關(guān)于分布式鎖的實(shí)現(xiàn)方式還有很多,這里我們只是針對(duì)Redis實(shí)現(xiàn)了分布式鎖,并且可能還有一些我們沒(méi)有考慮到的問(wèn)題,只有在實(shí)際應(yīng)用中才會(huì)深入去研究探索。近年來(lái)分布式系統(tǒng)越來(lái)越流行的情況下,分布式鎖出現(xiàn)頻率已經(jīng)十分頻繁,所以大家有精力還是可以去補(bǔ)充這方面的知識(shí),之后我可能會(huì)介紹一下其他實(shí)現(xiàn)分布式鎖的方式,如果有問(wèn)題還希望大家提出,我也可以學(xué)習(xí)改正,共同進(jìn)步。

原文鏈接:https://blog.csdn.net/u013985664/article/details/94459529

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容