導讀
大家都聽過1萬小時定律,可事實真的是這樣嗎?做了1萬小時的CRUD,不還只會CRUD嗎,這年頭不適當的更新自身下技術棧,出門和別人聊天吹牛的時候,都沒拿的出手的,(⊙o⊙)…Redis沒入門的童鞋不推薦往下看,先去腦補下Redis入門(點我直達),SpringBoot整合Redis的教程(點我直達),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