1、技術(shù)方案
1.1、redis的基本命令
1)SETNX命令(SET if Not eXists)
語(yǔ)法:SETNX key value
功能:當(dāng)且僅當(dāng) key 不存在,將 key 的值設(shè)為 value ,并返回1;若給定的 key 已經(jīng)存在,則 SETNX 不做任何動(dòng)作,并返回0。
2)expire命令
語(yǔ)法:expire KEY seconds
功能:設(shè)置key的過(guò)期時(shí)間。如果key已過(guò)期,將會(huì)被自動(dòng)刪除。
3)DEL命令
語(yǔ)法:DEL key [KEY …]
功能:刪除給定的一個(gè)或多個(gè) key ,不存在的 key 會(huì)被忽略。
1.2、實(shí)現(xiàn)同步鎖原理
1)加鎖:“鎖”就是一個(gè)存儲(chǔ)在redis里的key-value對(duì),key是把一組投資操作用字符串來(lái)形成唯一標(biāo)識(shí),value其實(shí)并不重要,因?yàn)橹灰@個(gè)唯一的key-value存在,就表示這個(gè)操作已經(jīng)上鎖。
2)解鎖:既然key-value對(duì)存在就表示上鎖,那么釋放鎖就自然是在redis里刪除key-value對(duì)。
3)阻塞、非阻塞:阻塞式的實(shí)現(xiàn),若線(xiàn)程發(fā)現(xiàn)已經(jīng)上鎖,會(huì)在特定時(shí)間內(nèi)輪詢(xún)鎖。非阻塞式的實(shí)現(xiàn),若發(fā)現(xiàn)線(xiàn)程已經(jīng)上鎖,則直接返回。
4)處理異常情況:假設(shè)當(dāng)投資操作調(diào)用其他平臺(tái)接口出現(xiàn)等待時(shí),自然沒(méi)有釋放鎖,這種情況下加入鎖超時(shí)機(jī)制,用redis的expire命令為key設(shè)置超時(shí)時(shí)長(zhǎng),過(guò)了超時(shí)時(shí)間redis就會(huì)將這個(gè)key自動(dòng)刪除,即強(qiáng)制釋放鎖
(此步驟需在JAVA內(nèi)部設(shè)置同樣的超時(shí)機(jī)制,內(nèi)部超時(shí)時(shí)長(zhǎng)應(yīng)小于或等于redis超時(shí)時(shí)長(zhǎng))。
1.3、處理流程圖
2、代碼實(shí)現(xiàn)
2.1、同步鎖工具類(lèi)
package com.mic.synchrolock.util;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.mic.constants.Constants;
import com.mic.constants.InvestType;
/**
* 分布式同步鎖工具類(lèi)
* @author Administrator
*
*/
public class SynchrolockUtil {
private final Log logger = LogFactory.getLog(getClass());
@Autowired
private RedisClientTemplate redisClientTemplate;
public final String RETRYTYPE_WAIT = "1"; //加鎖方法當(dāng)對(duì)象已加鎖時(shí),設(shè)置為等待并輪詢(xún)
public final String RETRYTYPE_NOWAIT = "0"; //加鎖方法當(dāng)對(duì)象已加鎖時(shí),設(shè)置為直接返回
private String requestTimeOutName = ""; //投資同步鎖請(qǐng)求超時(shí)時(shí)間
private String retryIntervalName = ""; //投資同步鎖輪詢(xún)間隔
private String keyTimeoutName = ""; //緩存中key的失效時(shí)間
private String investProductSn = ""; //產(chǎn)品Sn
private String uuid; //對(duì)象唯一標(biāo)識(shí)
private Long startTime = System.currentTimeMillis(); //首次調(diào)用時(shí)間
public Long getStartTime() {
return startTime;
}
ListString> keyList = new ArrayListString>(); //緩存key的保存集合
public ListString> getKeyList() {
return keyList;
}
public void setKeyList(ListString> keyList) {
this.keyList = keyList;
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
}
@PreDestroy
public void destroy() {
this.unlock();
}
/**
* 根據(jù)傳入key值,判斷緩存中是否存在該key
* 存在-已上鎖:判斷retryType,輪詢(xún)超時(shí),或直接返回,返回ture
* 不存在-未上鎖:將該放入緩存,返回false
* @param key
* @param retryType 當(dāng)遇到上鎖情況時(shí) 1:輪詢(xún);0:直接返回
* @return
*/
public boolean islocked(String key,String retryType){
boolean flag = true;
logger.info("====投資同步鎖設(shè)置輪詢(xún)間隔、請(qǐng)求超時(shí)時(shí)長(zhǎng)、緩存key失效時(shí)長(zhǎng)====");
//投資同步鎖輪詢(xún)間隔 毫秒
Long retryInterval = Long.parseLong(Constants.getProperty(retryIntervalName));
//投資同步鎖請(qǐng)求超時(shí)時(shí)間 毫秒
Long requestTimeOut = Long.parseLong(Constants.getProperty(requestTimeOutName));
//緩存中key的失效時(shí)間 秒
Integer keyTimeout = Integer.parseInt(Constants.getProperty(keyTimeoutName));
//調(diào)用緩存獲取當(dāng)前產(chǎn)品鎖
logger.info("====當(dāng)前產(chǎn)品key為:"+key+"====");
if(isLockedInRedis(key,keyTimeout)){
if("1".equals(retryType)){
//采用輪詢(xún)方式等待
while (true) {
logger.info("====產(chǎn)品已被占用,開(kāi)始輪詢(xún)====");
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
logger.error("線(xiàn)程睡眠異常:"+e.getMessage(), e);
return flag;
}
logger.info("====判斷請(qǐng)求是否超時(shí)====");
Long currentTime = System.currentTimeMillis(); //當(dāng)前調(diào)用時(shí)間
long Interval = currentTime - startTime;
if (Interval > requestTimeOut) {
logger.info("====請(qǐng)求超時(shí)====");
return flag;
}
if(!isLockedInRedis(key,keyTimeout)){
logger.info("====輪詢(xún)結(jié)束,添加同步鎖====");
flag = false;
keyList.add(key);
break;
}
}
}else{
//不等待,直接返回
logger.info("====產(chǎn)品已被占用,直接返回====");
return flag;
}
}else{
logger.info("====產(chǎn)品未被占用,添加同步鎖====");
flag = false;
keyList.add(key);
}
return flag;
}
/**
* 在緩存中查詢(xún)key是否存在
* 若存在則返回true;
* 若不存在則將key放入緩存,設(shè)置過(guò)期時(shí)間,返回false
* @param key
* @param keyTimeout key超時(shí)時(shí)間單位是秒
* @return
*/
boolean isLockedInRedis(String key,int keyTimeout){
logger.info("====在緩存中查詢(xún)key是否存在====");
boolean isExist = false;
//與redis交互,查詢(xún)對(duì)象是否上鎖
Long result = this.redisClientTemplate.setnx(key, uuid);
logger.info("====上鎖 result = "+result+"====");
if(null != result 1 == Integer.parseInt(result.toString())){
logger.info("====設(shè)置緩存失效時(shí)長(zhǎng) = "+keyTimeout+"秒====");
this.redisClientTemplate.expire(key, keyTimeout);
logger.info("====上鎖成功====");
isExist = false;
}else{
logger.info("====上鎖失敗====");
isExist = true;
}
return isExist;
}
/**
* 根據(jù)傳入key,對(duì)該產(chǎn)品進(jìn)行解鎖
* @param key
* @return
*/
public void unlock(){
//與redis交互,對(duì)產(chǎn)品解鎖
if(keyList.size()>0){
for(String key : this.keyList){
String value = this.redisClientTemplate.get(key);
if(null != value !"".equals(value)){
if(uuid.equals(value)){
logger.info("====解鎖key:"+key+" value="+value+"====");
this.redisClientTemplate.del(key);
}else{
logger.info("====待解鎖集合中key:"+key+" value="+value+"與uuid不匹配====");
}
}else{
logger.info("====待解鎖集合中key="+key+"的value為空====");
}
}
}else{
logger.info("====待解鎖集合為空====");
}
}
}
2.2、業(yè)務(wù)調(diào)用模擬樣例
//獲取同步鎖工具類(lèi)
SynchrolockUtil synchrolockUtil = SpringUtils.getBean("synchrolockUtil");
//獲取需上鎖資源的KEY
String key = "abc";
//查詢(xún)是否上鎖,上鎖輪詢(xún),未上鎖加鎖
boolean isLocked = synchrolockUtil.islocked(key,synchrolockUtil.RETRYTYPE_WAIT);
//判斷上鎖結(jié)果
if(isLocked){
logger.error("同步鎖請(qǐng)求超時(shí)并返回 key ="+key);
}else{
logger.info("====同步鎖加鎖陳功====");
}
try {
//執(zhí)行業(yè)務(wù)處理
} catch (Exception e) {
logger.error("業(yè)務(wù)異常:"+e.getMessage(), e);
}finally{
//解鎖
synchrolockUtil.unlock();
}
2.3、如果業(yè)務(wù)處理內(nèi)部,還有嵌套加鎖需求,只需將對(duì)象傳入方法內(nèi)部,加鎖成功后將key值追加到集合中即可
ps:實(shí)際實(shí)現(xiàn)中還需要jedis工具類(lèi),需額外添加調(diào)用
補(bǔ)充:使用redis鎖還是出現(xiàn)同步問(wèn)題
一種可能是,2臺(tái)機(jī)器同時(shí)訪(fǎng)問(wèn),一臺(tái)訪(fǎng)問(wèn),還沒(méi)有把鎖設(shè)置過(guò)去的時(shí)候,另一臺(tái)也查不到就會(huì)出現(xiàn)這個(gè)問(wèn)題。
解決方法
這我跟寫(xiě)代碼的方式有關(guān)。先查,如果不存在就set,這種方式有極微小的可能存在時(shí)間差,導(dǎo)致鎖set了2次。
推薦使用setIfAbsent 這樣在redis set的時(shí)候是單線(xiàn)程的。不會(huì)存在重復(fù)的問(wèn)題。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。
您可能感興趣的文章:- Redis的主從同步解析
- 簡(jiǎn)單注解實(shí)現(xiàn)集群同步鎖(spring+redis+注解)
- SpringBoot集成redis實(shí)現(xiàn)分布式鎖的示例代碼
- 基于redis setIfAbsent的使用說(shuō)明
- Redis實(shí)現(xiàn)分布式Session管理的機(jī)制詳解
- kubernetes環(huán)境部署單節(jié)點(diǎn)redis數(shù)據(jù)庫(kù)的方法