導讀
大家都聽過1萬小時定律,可事實真的是這樣嗎?做了1萬小時的CRUD,不還只會CRUD嗎,這年頭不適當的更新自身下技術棧,出門和別人聊天吹牛的時候,都沒拿不出手,(⊙o⊙)…Redis沒入門的童鞋不推薦往下看,先去腦補下Redis入門(點我直達),SpringBoot整合Redis的教程(點我直達),這篇不會講淺的知識點!!!!
面試專題
什麼是分散式鎖?
首先,為了確保分散式鎖可用,至少要滿足以下三個條件
- 互斥性。在任意時刻,只有一個客戶端能持有鎖
- 不會發生死鎖。即便有一個客戶端在持有鎖的期間奔潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖
- 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了
實現分散式鎖方式?
兩種實現,下面都會有講到
- 採用lua指令碼操作分散式鎖
- 採用setnx、setex命令連用的方式實現分散式鎖
分散式鎖的場景
什麼是分散式鎖?
- 分散式鎖是控制分散式系統或不同系統之間共同訪問共享資源的一種鎖實現
- 如果不同的系統或同一個系統的不同主機之間共享了某個資源時,往往通過互斥來防止彼此干擾
為什麼要有分散式鎖?
可以保證在分散式部署的應用叢集中,同一個方法在同一操作只能被一臺機器上的一個執行緒執行。
設計要求
- 可重入鎖(避免死鎖)
- 獲取鎖和釋放鎖高可用
- 獲取鎖和釋放鎖高效能
實現方案
- 獲取鎖,使用setnx():SETNX key val:當且僅當key不存在時,set一個key為val的字串,返回1
- 若key存在,則什麼都不做,返回【0】加鎖,鎖的value值為當前佔有鎖伺服器內網IP編號拼接任務標識
- 在釋放鎖的時候進行判斷。並使用expire命令為鎖新增一個超時時間,超過該時間則自動釋放鎖
- 返回1則成功獲取鎖。還設定一個獲取的超時時間,若超過這個時間則放棄獲取鎖,setex(key,value,expire)過期以秒為單位
- 釋放鎖的時候,判斷是不是該鎖(即value為當前伺服器內網IP編號拼接任務標識),若是該鎖,則執行delete進行鎖釋放
Redis分散式鎖的實現
建立一個SpringBoot工程
網址:https://start.spring.io/
步驟
1、啟動類上加上註解@EnableScheduling
2、執行方法上加上註解@Scheduled
打包並上傳至Linux伺服器中啟動
準備3臺Linux伺服器,並將打好的jar包,上傳至3臺伺服器中,然後啟動
nohub之持久化啟動方式
nohup java -jar jar名稱 &
檢視叢集裡面所有叢集是否啟動成功
1、先安裝lsof:yum install lsof
2、驗證:lsof -i:8080
TCP三次握手
檢視本機TCP連線狀態
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
為什麼要三次握手?
主要是為了防止已失效的連線請求報文段突然又傳到了B,因而報文錯亂問題,假定A發出的第一個連線請求報文段並沒有丟失,而是在某些網路節點長時間滯留,一直延遲到連線釋放以後的某個時間才到達B,本來這是一個早已經失效的報文段。但B收到失效的連線請求報文段後,就誤認為是A又發出一個新的連線請求,於是就向A發出確認報文段,同意建立連線。
假定不採用三次握手,那麼只要B發出確認,新的連線就建立,這樣一直等待A發來資料,B的許多資源就這樣白白浪費了。
圖解
有3次握手了,為啥還有4次揮手?
第一次揮手:主動關閉方傳送一個FIN,用來關閉主動方到被動關閉方的資料傳送,也就是主動關閉方告訴被動關閉方:我已經不 會再給你發資料了(當然,在fin包之前傳送出去的資料,如果沒有收到對應的ack確認報文,主動關閉方依然會重發這些資料),但是,此時主動關閉方還可 以接受資料。
第二次揮手:被動關閉方收到FIN包後,傳送一個ACK給對方,確認序號為收到序號+1(與SYN相同,一個FIN佔用一個序號)。
第三次揮手:被動關閉方傳送一個FIN,用來關閉被動關閉方到主動關閉方的資料傳送,也就是告訴主動關閉方,我的資料也傳送完了,不會再給你發資料了。
第四次揮手:主動關閉方收到FIN後,傳送一個ACK給被動關閉方,確認序號為收到序號+1,至此,完成四次揮手。
作用
確保資料能夠完整傳輸
Redis分散式鎖實現原始碼講解
圖文講解
步驟
- 分散式鎖滿足兩個條件,一個是加有效時間的鎖,一個是高效能解鎖
- 採用redis命令setnx (set if not exist)、setex(set expire value)實現
- 解鎖流程不能遺漏,否則導致任務執行一次就永不過期
- 將加鎖程式碼和任務邏輯放到try catch程式碼塊,解鎖流程放到finally程式碼塊
專案結構
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cyb</groupId> <artifactId>yb-mobile-redis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>yb-mobile-redis</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties
spring.redis.database=0
spring.redis.host=192.168.199.142
spring.redis.port=6379
spring.redis.password=12345678
server.port=9001
RedisService.java
package com.cyb.ybmobileredis.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Service; import java.io.Serializable; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @ClassName:RedisService * @Description:TODO * @Author:chenyb * @Date:2020/8/16 5:39 下午 * @Versiion:1.0 */ @Service public class RedisService { @Autowired private RedisTemplate redisTemplate; private static double size = Math.pow(2, 32); /** * 寫入快取 * * @param key * @param offset 位 8Bit=1Byte * @return */ public boolean setBit(String key, long offset, boolean isShow) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.setBit(key, offset, isShow); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入快取 * * @param key * @param offset * @return */ public boolean getBit(String key, long offset) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.getBit(key, offset); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入快取 * * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入快取設定時效時間 * * @param key * @param value * @return */ public boolean set(final String key, Object value, Long expireTime) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 批量刪除對應的value * * @param keys */ public void remove(final String... keys) { for (String key : keys) { remove(key); } } /** * 刪除對應的value * * @param key */ public void remove(final String key) { if (exists(key)) { redisTemplate.delete(key); } } /** * 判斷快取中是否有對應的value * * @param key * @return */ public boolean exists(final String key) { return redisTemplate.hasKey(key); } /** * 讀取快取 * * @param key * @return */ public Object get(final String key) { Object result = null; ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.get(key); return result; } /** * 雜湊 新增 * * @param key * @param hashKey * @param value */ public void hmSet(String key, Object hashKey, Object value) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); hash.put(key, hashKey, value); } /** * 雜湊獲取資料 * * @param key * @param hashKey * @return */ public Object hmGet(String key, Object hashKey) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); return hash.get(key, hashKey); } /** * 列表新增 * * @param k * @param v */ public void lPush(String k, Object v) { ListOperations<String, Object> list = redisTemplate.opsForList(); list.rightPush(k, v); } /** * 列表獲取 * * @param k * @param l * @param l1 * @return */ public List<Object> lRange(String k, long l, long l1) { ListOperations<String, Object> list = redisTemplate.opsForList(); return list.range(k, l, l1); } /** * 集合新增 * * @param key * @param value */ public void add(String key, Object value) { SetOperations<String, Object> set = redisTemplate.opsForSet(); set.add(key, value); } /** * 集合獲取 * * @param key * @return */ public Set<Object> setMembers(String key) { SetOperations<String, Object> set = redisTemplate.opsForSet(); return set.members(key); } /** * 有序集合新增 * * @param key * @param value * @param scoure */ public void zAdd(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.add(key, value, scoure); } /** * 有序集合獲取 * * @param key * @param scoure * @param scoure1 * @return */ public Set<Object> rangeByScore(String key, double scoure, double scoure1) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); redisTemplate.opsForValue(); return zset.rangeByScore(key, scoure, scoure1); } //第一次載入的時候將資料載入到redis中 public void saveDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); boolean availableUsers = setBit("availableUsers", indexLong, true); } //第一次載入的時候將資料載入到redis中 public boolean getDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); return getBit("availableUsers", indexLong); } /** * 有序集合獲取排名 * * @param key 集合名稱 * @param value 值 */ public Long zRank(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.rank(key,value); } /** * 有序集合獲取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start,long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key,start,end); return ret; } /** * 有序集合新增 * * @param key * @param value */ public Double zSetScore(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.score(key,value); } /** * 有序集合新增分數 * * @param key * @param value * @param scoure */ public void incrementScore(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.incrementScore(key, value, scoure); } /** * 有序集合獲取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start,long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key,start,end); return ret; } /** * 有序集合獲取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end); return ret; } }
LockNxExJob.java
package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.RedisService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.Enumeration; /** * @ClassName:LockNxExJob * @Description:分散式獲取鎖和釋放鎖 * @Author:chenyb * @Date:2020/8/16 5:44 下午 * @Versiion:1.0 */ @Service public class LockNxExJob { private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class); @Autowired private RedisService redisService; @Autowired private RedisTemplate redisTemplate; private static String LOCK_PREFIX = "prefix_"; @Scheduled(fixedRate = 8000) public void lockJob() { String lock = LOCK_PREFIX + "LockNxExJob"; boolean nxRet=false; try{ //redistemplate setnx操作 nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp()); Object lockValue = redisService.get(lock); System.out.println(lockValue); //獲取鎖失敗 if(!nxRet){ String value = (String)redisService.get(lock); //列印當前佔用鎖的伺服器IP logger.info(System.currentTimeMillis()+" get lock fail,lock belong to:{}",value); return; }else{ redisTemplate.opsForValue().set(lock,getHostIp(),3600); //獲取鎖成功 logger.info(System.currentTimeMillis()+" start lock lockNxExJob success"); Thread.sleep(4000); } }catch (Exception e){ logger.error("lock error",e); }finally { if (nxRet){ System.out.println("釋放鎖成功"); 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; } }
驗證
打個jar執行在Linux上,一個在本地執行,一個獲取鎖成功,一個獲取鎖失敗
Redis分散式鎖可能出現的問題
上面我們已經使用程式碼實現了分散式鎖的功能,同一時刻只能一把鎖獲取成功。從上圖可以看出,極端情況下,第一個Server獲取鎖成功後,服務或者Redis當機了,會導致Redis鎖無法釋放的問題,其他的Server就一直獲取鎖失敗。
模擬server獲取鎖當機
先把專案跑起來,獲取鎖之後,立馬kill -9 程式id,殺掉當前程式,然後在執行專案,控制檯就會一直提示,獲取鎖失敗了。
解決方案(重點)
- 一次性執行一條命令就不會出現該情況發生,採用Lua指令碼
- Redis從2.6之後,支援setnx、setex連用
lua指令碼
- 在redource目錄下新增一個字尾名為.lua結尾的檔案
- 編寫lua指令碼
- 傳入lua指令碼的key和arg
- 呼叫redisTemplate.execute方法執行指令碼
編寫lua指令碼
local lockKey = KEYS[1] local lockValue = KEYS[2] -- setnx info local result_1 = redis.call('SETNX',lockKey,lockValue) if result_1 == true then local result_2 = redis.call('SETEX',lockKey,3600,lockValue) return result_1 else return result_1 end
封裝呼叫lua指令碼方法
@Autowired private RedisTemplate redisTemplate; private DefaultRedisScript<Boolean> lockScript; /** * 獲取lua結果 * * @param key 鍵 * @param value 值 * @return */ public Boolean luaExpress(String key, String value) { lockScript = new DefaultRedisScript<>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("add.lua")) ); //設定返回值 lockScript.setResultType(Boolean.class); //封裝引數 List<Object> keyList = new ArrayList<>(); keyList.add(key); keyList.add(value); Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList); return result; }
改造之前的分散式鎖方法
package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.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; /** * @ClassName:LockNxExJob * @Description:分散式獲取鎖和釋放鎖 * @Author:chenyb * @Date:2020/8/16 5:44 下午 * @Versiion:1.0 */ @Service public class LockNxExJob { private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class); @Autowired private RedisService redisService; @Autowired private RedisTemplate redisTemplate; private static String LOCK_PREFIX = "prefix_"; private DefaultRedisScript<Boolean> lockScript; //一般分散式鎖 // @Scheduled(fixedRate = 8000) // public void lockJob() { // String lock = LOCK_PREFIX + "LockNxExJob"; // boolean nxRet = false; // try { // //redistemplate setnx操作 // nxRet = redisTemplate.opsForValue().setIfAbsent(lock, getHostIp()); // Object lockValue = redisService.get(lock); // System.out.println(lockValue); // //獲取鎖失敗 // if (!nxRet) { // String value = (String) redisService.get(lock); // //列印當前佔用鎖的伺服器IP // logger.info(System.currentTimeMillis() + " get lock fail,lock belong to:{}", value); // return; // } else { // redisTemplate.opsForValue().set(lock, getHostIp(), 3600000); // // //獲取鎖成功 // logger.info(System.currentTimeMillis() + " start lock lockNxExJob success"); // Thread.sleep(4000); // } // } catch (Exception e) { // logger.error("lock error", e); // // } finally { // if (nxRet) { // System.out.println("釋放鎖成功"); // redisService.remove(lock); // } // } // } /** * lua指令碼方式分散式鎖 */ @Scheduled(fixedRate = 8000) public void luaLockJob() { String lock = LOCK_PREFIX + "LockNxExJob"; boolean nxRet = false; try { //redistemplate setnx操作 nxRet = luaExpress(lock,getHostIp()); Object lockValue = redisService.get(lock); System.out.println(lockValue); //獲取鎖失敗 if (!nxRet) { String value = (String) redisService.get(lock); //列印當前佔用鎖的伺服器IP logger.info(System.currentTimeMillis() + " lua get lock fail,lock belong to:{}", value); return; } else { redisTemplate.opsForValue().set(lock, getHostIp(), 3600000); //獲取鎖成功 logger.info(System.currentTimeMillis() + " lua start lock lockNxExJob success"); Thread.sleep(4000); } } catch (Exception e) { logger.error("lua lock error", e); } finally { if (nxRet) { System.out.println("lua 釋放鎖成功"); redisService.remove(lock); } } } /** * 獲取lua結果 * * @param key 鍵 * @param value 值 * @return */ public Boolean luaExpress(String key, String value) { lockScript = new DefaultRedisScript<>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("add.lua")) ); //設定返回值 lockScript.setResultType(Boolean.class); //封裝引數 List<Object> keyList = new ArrayList<>(); 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; } }
驗證
補充解決Redis中的key亂碼問題
只需要新增RedisConfig.java配置檔案即可
package com.cyb.ybmobileredis.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @ClassName:RedisConfig * @Description:Redis配置類 * @Author:chenyb * @Date:2020/8/16 11:48 下午 * @Versiion:1.0 */ @Configuration //當前類為配置類 public class RedisConfig { @Bean //redisTemplate注入到Spring容器 public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory){ RedisTemplate<String,String> redisTemplate=new RedisTemplate<>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); redisTemplate.setConnectionFactory(factory); //key序列化 redisTemplate.setKeySerializer(redisSerializer); //value序列化 redisTemplate.setValueSerializer(redisSerializer); //value hashmap序列化 redisTemplate.setHashKeySerializer(redisSerializer); //key hashmap序列化 redisTemplate.setHashValueSerializer(redisSerializer); return redisTemplate; } }
RedisConnection實現分散式鎖
簡介
RedisConnection實現分散式鎖的方式,採用redisTemplate操作redisConnection實現setnx和setex兩個命令連用
程式碼實現
package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.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.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.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; /** * @ClassName:LockNxExJob * @Description:分散式獲取鎖和釋放鎖 * @Author:chenyb * @Date:2020/8/16 5:44 下午 * @Versiion:1.0 */ @Service public class LockNxExJob { private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class); @Autowired private RedisService redisService; @Autowired private RedisTemplate redisTemplate; private static String LOCK_PREFIX = "prefix_"; private DefaultRedisScript<Boolean> lockScript; //一般分散式鎖 // @Scheduled(fixedRate = 8000) // public void lockJob() { // String lock = LOCK_PREFIX + "LockNxExJob"; // boolean nxRet = false; // try { // //redistemplate setnx操作 // nxRet = redisTemplate.opsForValue().setIfAbsent(lock, getHostIp()); // Object lockValue = redisService.get(lock); // System.out.println(lockValue); // //獲取鎖失敗 // if (!nxRet) { // String value = (String) redisService.get(lock); // //列印當前佔用鎖的伺服器IP // logger.info(System.currentTimeMillis() + " get lock fail,lock belong to:{}", value); // return; // } else { // redisTemplate.opsForValue().set(lock, getHostIp(), 3600000); // // //獲取鎖成功 // logger.info(System.currentTimeMillis() + " start lock lockNxExJob success"); // Thread.sleep(4000); // } // } catch (Exception e) { // logger.error("lock error", e); // // } finally { // if (nxRet) { // System.out.println("釋放鎖成功"); // redisService.remove(lock); // } // } // } /** * lua指令碼方式分散式鎖 */ // @Scheduled(fixedRate = 8000) // public void luaLockJob() { // String lock = LOCK_PREFIX + "LockNxExJob"; // boolean nxRet = false; // try { // //redistemplate setnx操作 // //nxRet = luaExpress(lock,getHostIp()); // nxRet = setLock(lock,600); // Object lockValue = redisService.get(lock); // System.out.println(lockValue); // //獲取鎖失敗 // if (!nxRet) { // String value = (String) redisService.get(lock); // //列印當前佔用鎖的伺服器IP // logger.info(System.currentTimeMillis() + " lua get lock fail,lock belong to:{}", value); // return; // } else { // redisTemplate.opsForValue().set(lock, getHostIp(), 3600000); // // //獲取鎖成功 // logger.info(System.currentTimeMillis() + " lua start lock lockNxExJob success"); // Thread.sleep(4000); // } // } catch (Exception e) { // logger.error("lua lock error", e); // // } finally { // if (nxRet) { // System.out.println("lua 釋放鎖成功"); // redisService.remove(lock); // } // } // } /** * setnx和setex連用分散式鎖 */ @Scheduled(fixedRate = 8000) public void setLockJob() { String lock = LOCK_PREFIX + "LockNxExJob"; boolean nxRet = false; try { //redistemplate setnx操作 //nxRet = luaExpress(lock,getHostIp()); nxRet = setLock(lock,getHostIp(),3); Object lockValue = redisService.get(lock); System.out.println(lockValue); //獲取鎖失敗 if (!nxRet) { String value = (String) redisService.get(lock); //列印當前佔用鎖的伺服器IP logger.info(System.currentTimeMillis() + " setnx and setex get lock fail,lock belong to:{}", value); return; } else { redisTemplate.opsForValue().set(lock, getHostIp(), 3600000); //獲取鎖成功 logger.info(System.currentTimeMillis() + " setnx and setex start lock lockNxExJob success"); Thread.sleep(4000); } } catch (Exception e) { logger.error(" setnx and setex lock error", e); } finally { if (nxRet) { System.out.println(" setnx and setex 釋放鎖成功"); redisService.remove(lock); } } } /** * setnx和setex連用 * @param key 鍵 * @param value 值 * @param expire 超時時間 * @return */ public boolean setLock(String key,String value,long expire){ try{ Boolean result=(boolean)redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.set(key.getBytes(),value.getBytes(),Expiration.seconds(expire),RedisStringCommands.SetOption.ifAbsent()); } }); return result; }catch (Exception e){ logger.error("set redis occured an exception",e); } return false; } /** * 獲取lua結果 * * @param key 鍵 * @param value 值 * @return */ public Boolean luaExpress(String key, String value) { lockScript = new DefaultRedisScript<>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("add.lua")) ); //設定返回值 lockScript.setResultType(Boolean.class); //封裝引數 List<Object> keyList = new ArrayList<>(); 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; } }
測試
分散式鎖優化細節(重點)
上面幾個案例,已經實現了分散式鎖的功能,但是極端情況下,ServerA程式還沒執行完,ServerB程式執行完,把鎖釋放掉了,就會造成A的鎖釋放掉了,這不是扯嘛,ServerA還沒執行完,鎖就被其他人釋放了。解決方案:釋放的時候,使用lua,通過get方法獲取value,判斷value是否等於本機ip,是自己的才能釋放
package com.cyb.ybmobileredis.schedule; import com.cyb.ybmobileredis.service.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.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.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; /** * @ClassName:LockNxExJob * @Description:分散式獲取鎖和釋放鎖 * @Author:chenyb * @Date:2020/8/16 5:44 下午 * @Versiion:1.0 */ @Service public class LockNxExJob { private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class); @Autowired private RedisService redisService; @Autowired private RedisTemplate redisTemplate; private static String LOCK_PREFIX = "prefix_"; private DefaultRedisScript<Boolean> lockScript; //一般分散式鎖 // @Scheduled(fixedRate = 8000) // public void lockJob() { // String lock = LOCK_PREFIX + "LockNxExJob"; // boolean nxRet = false; // try { // //redistemplate setnx操作 // nxRet = redisTemplate.opsForValue().setIfAbsent(lock, getHostIp()); // Object lockValue = redisService.get(lock); // System.out.println(lockValue); // //獲取鎖失敗 // if (!nxRet) { // String value = (String) redisService.get(lock); // //列印當前佔用鎖的伺服器IP // logger.info(System.currentTimeMillis() + " get lock fail,lock belong to:{}", value); // return; // } else { // redisTemplate.opsForValue().set(lock, getHostIp(), 3600000); // // //獲取鎖成功 // logger.info(System.currentTimeMillis() + " start lock lockNxExJob success"); // Thread.sleep(4000); // } // } catch (Exception e) { // logger.error("lock error", e); // // } finally { // if (nxRet) { // System.out.println("釋放鎖成功"); // redisService.remove(lock); // } // } // } /** * lua指令碼方式分散式鎖 */ // @Scheduled(fixedRate = 8000) // public void luaLockJob() { // String lock = LOCK_PREFIX + "LockNxExJob"; // boolean nxRet = false; // try { // //redistemplate setnx操作 // //nxRet = luaExpress(lock,getHostIp()); // nxRet = setLock(lock,600); // Object lockValue = redisService.get(lock); // System.out.println(lockValue); // //獲取鎖失敗 // if (!nxRet) { // String value = (String) redisService.get(lock); // //列印當前佔用鎖的伺服器IP // logger.info(System.currentTimeMillis() + " lua get lock fail,lock belong to:{}", value); // return; // } else { // redisTemplate.opsForValue().set(lock, getHostIp(), 3600000); // // //獲取鎖成功 // logger.info(System.currentTimeMillis() + " lua start lock lockNxExJob success"); // Thread.sleep(4000); // } // } catch (Exception e) { // logger.error("lua lock error", e); // // } finally { // if (nxRet) { // System.out.println("lua 釋放鎖成功"); // redisService.remove(lock); // } // } // } /** * setnx和setex連用分散式鎖 */ @Scheduled(fixedRate = 8000) public void setLockJob() { String lock = LOCK_PREFIX + "LockNxExJob"; boolean nxRet = false; try { //redistemplate setnx操作 //nxRet = luaExpress(lock,getHostIp()); System.out.println("hostIp1="+getHostIp()); nxRet = setLock(lock, getHostIp(), 30); Object lockValue = redisService.get(lock); System.out.println(lockValue); //獲取鎖失敗 if (!nxRet) { String value = (String) redisService.get(lock); //列印當前佔用鎖的伺服器IP logger.info(System.currentTimeMillis() + " setnx and setex get lock fail,lock belong to:{}", value); return; } else { //獲取鎖成功 logger.info(System.currentTimeMillis() + " setnx and setex start lock lockNxExJob success"); Thread.sleep(4000); } } catch (Exception e) { logger.error(" setnx and setex lock error", e); } finally { if (nxRet) { System.out.println(" setnx and setex 釋放鎖成功"); //redisService.remove(lock); //使用lua指令碼釋放鎖 System.out.println("hostIp2="+getHostIp()); Boolean result = releaseLock(lock, getHostIp()); System.out.println("狀態:"+result); } } } /** * 釋放鎖操作 * * @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<>(); keyList.add(key); keyList.add(value); Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList); return result; } /** * setnx和setex連用 * * @param key 鍵 * @param value 值 * @param expire 超時時間 * @return */ public boolean setLock(String key, String value, long expire) { try { Boolean result = (boolean) redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.set(key.getBytes(), value.getBytes(), Expiration.seconds(expire), RedisStringCommands.SetOption.ifAbsent()); } }); return result; } catch (Exception e) { logger.error("set redis occured an exception", e); } return false; } /** * 獲取lua結果 * * @param key 鍵 * @param value 值 * @return */ public Boolean luaExpress(String key, String value) { lockScript = new DefaultRedisScript<>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("add.lua")) ); //設定返回值 lockScript.setResultType(Boolean.class); //封裝引數 List<Object> keyList = new ArrayList<>(); 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指令碼
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
演示
為了演示方便,我把失效時間設定短一點,8秒
尾聲
嫖都嫖完了,難道你忍心不點贊關注嘛,O(∩_∩)O哈哈~~~~~今天,先到這,後續繼續寫Redis秒殺系統的設計。
案例原始碼下載
連結: https://pan.baidu.com/s/1uVoRQs8K3_zTfHXSTeP0uA 密碼: k9sv