目錄
- 分佈式鎖場景
- Redis分佈式鎖的簡易實現
- Redis分佈式鎖存在的問題
- Lua腳本實現分佈式鎖一致性
- RedisConnection實現分佈式鎖一致性
- 分佈式鎖優化分析建議
分佈式鎖場景
-
分佈式鎖是什麼
分佈式鎖是控制分佈式系統或不同系統之間共同訪問共享資源的一種鎖實現
如果不同的系統或同一個系統的不同主機之間共享了某個資源時,往往通過互斥來防止彼此干擾。 -
分佈鎖設計目的
可以保證在分佈式部署的應用集羣中,同一個方法在同一操作只能被一臺機器上的一個線程執行。 -
設計要求
- 這把鎖要是一把可重入鎖(避免死鎖)
- 這把鎖有高可用的獲取鎖和釋放鎖功能
- 這把鎖獲取鎖和釋放鎖的性能要好…
-
分佈鎖實現方案分析
- 獲取鎖的時候,使用 setnx(SETNX key val:當且僅當 key 不存在時,set 一個 key 爲 val 的字符串,返回 1;
- 若 key 存在,則什麼都不做,返回 【0】加鎖,鎖的 value 值爲當前佔有鎖服務器內網IP編號拼接任務標識
- 在釋放鎖的時候進行判斷。並使用 expire 命令爲鎖添 加一個超時時間,超過該時間則自動釋放鎖。
- 返回1則成功獲取鎖。還設置一個獲取的超時時間, 若超過這個時間則放棄獲取鎖。setex(key,value,expire)過期以秒爲單位
- 釋放鎖的時候,判斷是不是該鎖(即Value爲當前服務器內網IP編號拼接任務標識),若是該鎖,則執行 delete 進行鎖釋放
Redis分佈式鎖的簡易實現
setnx獲取鎖成功後設置setex超時時間,在沒有任何意外宕機的情況下看起來很完美,下文講解下突然宕機的情況會產生什麼問題
代碼演練
本文項目環境基於Springboot與Redis Cache深度整合
1.再主啓動類RedisApplication上添加定時器註解 @EnableScheduling
package com.gcxzflgl.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
2.再工程結構中新建schedule包,創建定時任務類演練分佈式鎖初步實現
package com.gcxzflgl.redis.schedule;
/**
* @author gcxzf$
* @version : ClusterLockJob$, v 0.1 2020/6/14$ 9:03$ gcxzf$ Exp$
*/
@Service
public class ClusterLockJob {
private static final Logger logger = LoggerFactory.getLogger(ClusterLockJob.class);
@Autowired
private RedisService redisService;
@Autowired
private RedisTemplate redisTemplate;
private static String LOCK_PREFIX = "prefix_";
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "ClusterLockJob";
boolean nxRet = false;
try{
//redistemplate setnx操作
nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
Object lockValue = redisService.get(lock);
//獲取鎖失敗
if(!nxRet){
String value = (String)redisService.get(lock);
//打印當前佔用鎖的服務器IP
logger.info("get lock fail,lock belong to:{}",value);
return;
}else{
redisTemplate.opsForValue().set(lock,getHostIp(),3600);
//獲取鎖成功
logger.info("start lock lockNxExJob success");
Thread.sleep(5000);
}
}catch (Exception e){
logger.error("lock error",e);
}finally {
if(nxRet){
logger.info("release lock success");
redisService.remove(lock);
}
}
}
/**
* 獲取本機內網IP地址方法
* @return
*/
private static String getHostIp(){
try{
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()){
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本機地址,IPv4的loopback範圍是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":")==-1){
return ip.getHostAddress();
}
}
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
String localIP = "";
try {
localIP = getHostIp();
} catch (Exception e) {
e.printStackTrace();
}
//獲取本機IP
System.out.println(localIP);
}
}
3.演練方式,本地工程啓動和jar包啓動查看演示效果
Redis分佈式鎖存在的問題
上文中用setnx 和 setex實現了簡易分佈式鎖,我們剖析分佈式鎖setnx、setex的缺陷,在setnx和setex中間發生了服務down機的情況,第一臺服務器加鎖成功,突如其來的服務宕機導致redis鎖無法釋放,其他服務一直拿不到鎖,也就是如何保證一致性問題。
Lua腳本實現分佈式鎖一致性
- Lua簡介
- 從 Redis 2.6.0 版本開始,通過內置的 Lua 解釋器,可以使用 EVAL 命令對 Lua 腳本進行求值。
- Redis 使用單個 Lua 解釋器去運行所有腳本,並且, Redis 也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很類似。在其他別的客戶端看來,腳本的效果(effect)要麼是不可見的(not visible),要麼就是已完成的(already completed)。
- Lua腳本配置流程
- 1、在resource目錄下面新增一個後綴名爲.lua結尾的文件
- 2、編寫lua腳本
- 3、傳入lua腳本的key和arg
- 4、調用redisTemplate.execute方法執行腳本
代碼演練:
1.再/resources新建add.lua的腳本
-- local定義變量 KEYS[N] 是由代碼傳遞過來的key1 key2 key3
local lockKey = KEYS[1]
local lockValue = KEYS[2]
-- 保證一致性,要麼全部成功,要麼全部失敗 redis.call 以命令方式執行
local result_1 = redis.call('SETNX', lockKey, lockValue)
if result_1 == true
then
local result_2= redis.call('SETEX', lockKey,3600, lockValue)
return result_1
else
return result_1
end
2.RedisService 新增工具方法
/**
* 讀取緩存
*
* @param key
* @return
*/
public Object genValue(final String key) {
Object result = null;
ValueOperations<String, String> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
3.再schedule中新增luaDistributeLock.java
package com.gcxzflgl.redis.schedule;
import com.gcxzflgl.redis.biz.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@Service
public class LuaDistributeLock {
private static final Logger logger = LoggerFactory.getLogger(LuaDistributeLock.class);
@Autowired
private RedisService redisService;
@Autowired
private RedisTemplate redisTemplate;
private static String LOCK_PREFIX = "lua_";
private DefaultRedisScript<Boolean> lockScript;
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "LockNxExJob";
boolean luaRet = false;
try {
luaRet = luaExpress(lock,getHostIp());
//獲取鎖失敗
if (!luaRet) {
String value = (String) redisService.genValue(lock);
//打印當前佔用鎖的服務器IP
logger.info("lua get lock fail,lock belong to:{}", value);
return;
} else {
//獲取鎖成功
logger.info("lua start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("lock error", e);
} finally {
if (luaRet) {
logger.info("release lock success");
redisService.remove(lock);
}
}
}
/**
* 獲取lua結果
* @param key
* @param value
* @return
*/
public Boolean luaExpress(String key,String value) {
//獲取操作lua腳本的類
lockScript = new DefaultRedisScript<Boolean>();
//設置腳本的存放地址
lockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("add.lua")));
lockScript.setResultType(Boolean.class);
// 封裝參數
List<Object> keyList = new ArrayList<Object>();
keyList.add(key); //對應Lua腳本 KEYS[1]
keyList.add(value); //對應Lua腳本 KEYS[2]
Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
return result;
}
/**
* 獲取本機內網IP地址方法
*
* @return
*/
private static String getHostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本機地址,IPv4的loopback範圍是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":") == -1) {
return ip.getHostAddress();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4.用IDEA 啓動端口 8080,8081模擬一致性
RedisConnection實現分佈式鎖一致性
RedisConnection實現分佈鎖的方式,採用redisTemplate操作redisConnection
實現setnx和setex兩個命令連用,這種操作方式很常見。
代碼演練:
1.再pom.xml引入依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.shedule包下新增redisTmplate實現RedisConnection保證一致性
package com.gcxzflgl.redis.schedule;
@Component
public class JedisDistributedLock {
private final Logger logger = LoggerFactory.getLogger(JedisDistributedLock.class);
private static String LOCK_PREFIX = "lua_";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisService redisService;
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "JedisNxExJob";
boolean lockRet = false;
try {
lockRet = this.setLock(lock, 600);
//獲取鎖失敗
if (!lockRet) {
String value = (String) redisService.genValue(lock);
//打印當前佔用鎖的服務器IP
logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
return;
} else {
//獲取鎖成功
logger.info("jedisLockJob start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("jedisLockJob lock error", e);
} finally {
if (lockRet) {
logger.info("jedisLockJob release lock success");
redisService.remove(lock);
}
}
}
public boolean setLock(String key, long expire) {
try {
Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.set(key.getBytes(), "鎖定的資源".getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
}
});
return result;
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
public String get(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
logger.error("get redis occured an exception", e);
}
return "";
}
/**
* 獲取本機內網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;
}
}
分佈式鎖優化建議
上圖中的方式會存在一種無鎖狀態
假如:A獲得鎖,鎖超時時間是1個小時,再任務沒有執行完時不會釋放鎖,但是超時時間到了釋放了鎖,此時B操作判斷當前是無鎖獲取了鎖,然而A執行完畢了,釋放了鎖,其實釋放的是B的鎖
爲了解決這種問題發生,再次引入Lua腳本來執行,我們改造下RedisConnection的方法
package com.gcxzflgl.redis.schedule;
@Component
public class JedisDistributedLock {
private final Logger logger = LoggerFactory.getLogger(JedisDistributedLock.class);
private static String LOCK_PREFIX = "JedisDistributedLock_";
private DefaultRedisScript<Boolean> lockScript;
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private RedisService redisService;
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "JedisNxExJob";
boolean lockRet = false;
try {
lockRet = this.setLock(lock, 600);
//獲取鎖失敗
if (!lockRet) {
String value = (String) redisService.genValue(lock);
//打印當前佔用鎖的服務器IP
logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
return;
} else {
//獲取鎖成功
logger.info("jedisLockJob start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("jedisLockJob lock error", e);
} finally {
if (lockRet) {
logger.info("jedisLockJob release lock success");
releaseLock(lock,getHostIp());
}
}
}
public boolean setLock(String key, long expire) {
try {
Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.set(key.getBytes(), getHostIp().getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
}
});
return result;
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
public String get(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
logger.error("get redis occured an exception", e);
}
return "";
}
/**
* 釋放鎖操作
* @param key
* @param value
* @return
*/
private boolean releaseLock(String key, String value) {
lockScript = new DefaultRedisScript<Boolean>();
lockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("unlock.lua")));
lockScript.setResultType(Boolean.class);
// 封裝參數
List<Object> keyList = new ArrayList<Object>();
keyList.add(key);
keyList.add(value);
Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
return result;
}
/**
* 獲取本機內網IP地址方法
*
* @return
*/
private static String getHostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本機地址,IPv4的loopback範圍是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":") == -1) {
return ip.getHostAddress();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
新增unlock.lua腳本
當key的value與傳遞進來的value相同時進行釋放鎖,不會影響其他服務正常執行
local lockKey = KEYS[1]
local lockValue = KEYS[2]
-- get key
local result_1 = redis.call('get', lockKey)
if result_1 == lockValue
then
local result_2= redis.call('del', lockKey)
return result_2
else
return false
end
至此,分佈式鎖的講解到此完成!