基於AOP和Redis實現的簡易版分布式鎖

2021-09-11 13:26:48 字數 4169 閱讀 1839

所以我準備通過分布式鎖去解決這個問題。

分布式鎖一般解決如下兩類問題:

1、效率性問題,比如重**簡訊,重複生成同樣的訂單等。

2、正確性問題,比如在某乙個請求扣款的同時不允許其他請求同時去扣款。

當然,題目中寫了簡易版是因為,這次的分布式鎖只準備解決效率性的問題,而不解決正確性的問題。

之所以基於redis,是因為redis作為一款基本上網際網路公司都會必備的快取資料庫,簡單好用,入門門檻較低。

而基於aop實現,是因為用註解方式侵入性更低,基本上不需要修改原生**,只需要加註解就行。

如下是簡易版的分布式鎖的實現方式:

首先我們需要1個註解,用於方法的lockannotation

@retention(retentionpolicy.runtime)

@target()

public @inte***ce lockannotation

複製**

lockannotation用於加在需要分布式鎖的方法上,表明這是乙個需要加分布式鎖的方法,來引入切面,lockfield用於redis的key的字首識別,lockkey是用於明確redis的key,通過spel語法實現,locktime是防止死鎖而新增的鎖的預設失效時間,waittime是為了讓鎖獲取失敗時可以進行等待則不僅僅是快速失敗。

最後是切面的實現lockaspect

@component

@aspect

public class lockaspect

expression expression = parser.parseexpression(lockannotation.lockkey());

string lockkey = expression.getvalue(context, string.class);

int locktime = lockannotation.locktime() > 1 ? lockannotation.locktime() : 1;

int waittime = lockannotation.waittime() > 0 ? lockannotation.waittime() : 0;

int locktime = lockannotation.locktime();

string randomvalue = uuid.randomuuid().tostring();

long starttime = system.currenttimemillis();

long endtime = system.currenttimemillis() + waittime * 1000;

try ,引數為{}", joinpoint.getsignature(),

string.join("-", lists.newarraylist(args).stream().map(obj -> jsonobject.tojsonstring(objectutils.defaultifnull(obj, "null")))

.collect(collectors.tolist())));

}object returnobject = joinpoint.proceed(args);

return

returnobject;

}int sleeptime = math.min(1000, waittime * 100);

if (logger.isdebugenabled()) ms,方法名為{},引數為{}", sleeptime, joinpoint.getsignature(),

string.join("-", lists.newarraylist(args).stream().map(obj -> jsonobject.tojsonstring(objectutils.defaultifnull(obj, "null")))

.collect(collectors.tolist())));

}thread.sleep(sleeptime);

} while (system.currenttimemillis() <= endtime);

if (logger.isinfoenabled()) ms,方法將不執行,方法名為{},引數為{}", system.currenttimemillis() - starttime, joinpoint.getsignature()

, string.join("-", lists.newarraylist(args).stream().map(object::tostring)

.collect(collectors.tolist())));

}return null;

} finally

}}複製**

其中setlock是通過redis的set指令實現的,其中[nx|xx]選擇nx,然後設定過期時間。 而dellock是通過redis的eval指令去執行lua指令碼實現的,其中script**如下:

string script = "if redis.call('get', keys[1]) == ar**[1] then return redis.call('del', keys[1]) else return 0 end";

複製**

以上獲取鎖和釋放鎖的過程經過一次改良,原本不太完美的的**如下:

try ,引數為{}", joinpoint.getsignature(), string.join("-", lists.newarraylist(args).stream().map(object::tostring).collect(collectors.tolist())));

}return null;

}cacheutils.expirekey(lockfield, lockkey, locktime);

if (logger.isdebugenabled()) ,引數為{}", joinpoint.getsignature(), string.join("-", lists.newarraylist(args).stream().map(object::tostring).collect(collectors.tolist())));

} returnobject = joinpoint.proceed(args);

} finally

}複製**

主要區別在於有兩點:

1.之前的建立鎖和給鎖設定有效時間是分二步進行的,存在風險。萬一在建立鎖之後宕機了,那麼將徹底死鎖。改良後,將建立鎖和給鎖設定有效時間合併為一步setnx,就避免了這個問題。

2.之前的釋放鎖的過程,只判斷了ttl,但沒法保證釋放鎖的時候,自己依然是鎖的持有者。如果不加以判斷直接釋放鎖,就會出現誤刪除其它請求建立的鎖的情況。改良後,通過生成乙個隨機數,然後在釋放鎖的時候通過lua指令碼去執行,先獲取鎖的值,然後判斷釋放的鎖是否是當初該請求建立的鎖,如果是,則釋放鎖。

if (randomvalue.equals(cacheutils.get(lockfield, lockkey))) 

複製**

之所以沒有採用上述的先get再del的操作,而是採用lua指令碼是因為需要保持操作的原子性。 假設採用上述的方式,則get和del是分步執行的。那麼如果要求a在執行del操作之前,萬一因為其他原因導致沒有及時del,此時鎖過期自動釋放了,這時請求b發現可以建立鎖,就建立了鎖。然後請求a突然又恢復正常去釋放鎖,但此時鎖的持有者是請求b,請求a誤刪了請求b持有的鎖。 這個問題比較複雜,因為redis沒有get和del合二為一的操作,要解決該問題只能通過lua指令碼將這兩個操作合二為一,一起執行才行。

原理介紹完了,接下來介紹一下如何使用。

lockkey的命名盡量保持'x='+#x的方式。 如果變數是個物件,需要獲取到物件中的值,例如是order物件中的ordersn,可以通過order.ordersn方式獲取。

@lockannotation(lockfield = "lock", locktime = 10, lockkey = "'ordersn='+#ordersn")

public integer lock(string action, string ordersn)

複製**

需要注意的是,常量'ordersn='需要用單引號',否則會被當做是賦值語法,而#後面的ordersn只要和方法中的變數名ordersn保持一直就可以了

如有更好的建議,歡迎一起來溝通。

spring的AOP 基於XML實現AOP的過程

參考對應的 logaspect bean 或者 logaspect 類。logaspect類如下 package org.zttc.itat.spring.proxy import org.aspectj.lang.joinpoint import org.aspectj.lang.proceedi...

AOP基於註解實現

切面類 component 控制反轉 aspect 宣告切面類 public class forumadvisor after execution com.mitu.aspect.宣告後置增強 public void after around execution com.mitu.aspect.宣告...

AspectJ基於註解的AOP 實現

配置檔案 demo aspect public class myaspect signature signature joinpoint.getsignature system.out.println 方法的定義簽名signature signature string name joinpoint....