目前在做一個app的java后端開發(fā),有這樣一個需求,某一個用戶的某一種數(shù)據(jù)只能夠在數(shù)據(jù)庫表中出現(xiàn)唯一一條
有這個需求的話,很簡單的實(shí)現(xiàn)就是不用考慮太多東西,直接寫好邏輯:
如果數(shù)據(jù)庫中已經(jīng)存在那條數(shù)據(jù)了就把它刪掉,否則新插入一條數(shù)據(jù),在service層當(dāng)中就直接寫了這個邏輯,賊簡單,心中不經(jīng)暗喜,敲完部署就不管了.
然而,過了一段時間服務(wù)器崩了(相信這是大部分菜鳥程序員都會發(fā)生的事情,有自信的代碼居然會出現(xiàn)bug,啊啊啊淚奔怪自己年輕,對吧),關(guān)于那條數(shù)據(jù)的模塊都顯示不出數(shù)據(jù),我趕快看了一下日志發(fā)現(xiàn)數(shù)據(jù)庫中報了錯,大概的意思就是數(shù)據(jù)出現(xiàn)了3條,可是在dao層中僅獲取一條,問題來了,這多出來的數(shù)據(jù)是怎么回事?
冷靜下來想一想,應(yīng)該是多條請求在同一時刻內(nèi)發(fā)過來的,它們同時判斷出數(shù)據(jù)庫當(dāng)中沒有數(shù)據(jù),然后同時插入了進(jìn)去,噢,原來是這個樣子,那么這個問題該如何解決呢?
相信這種問題在后臺端開發(fā)是非常常見的,例如在web端,要提交一個表單數(shù)據(jù),由于服務(wù)器處理延遲,用戶看不到反饋,就心急地狂按鼠標(biāo)發(fā)送數(shù)據(jù);又或者是在下單的時候不小心多按了幾下鼠標(biāo),導(dǎo)致訂單下多了幾個,等等..
1.把問題扔給數(shù)據(jù)庫解決
可以在建表的時候,為相關(guān)的字段設(shè)置唯一索引(也可以設(shè)置聯(lián)合唯一索引),當(dāng)出現(xiàn)重復(fù)數(shù)據(jù)的時候,自然也就插不進(jìn)去了,這是保證數(shù)據(jù)安全的最可靠的方案,為保證安全,這個一定要設(shè)置
2.把問題扔給前端或者移動端解決
前端或者移動端可以在提交數(shù)據(jù)的時候加鎖,例如前端提交表單數(shù)據(jù)的時候,可以用JavaScript把submit設(shè)置為disable,直到后端返回數(shù)據(jù)的時候再設(shè)置為enable,等等
3.服務(wù)器端自己解決
其實(shí)解決方案也差不多,大致就是加鎖,問題出現(xiàn)的時候,我是直接在service層對應(yīng)的方法上面直接加上synchronized,然后把重復(fù)的數(shù)據(jù)從數(shù)據(jù)庫當(dāng)中刪掉,以解燃眉之急,但是這種方案加鎖的代碼太多了會降低性能,所以干脆寫一個不怎么影響性能的代碼,,接下來跟大伙分享一下吧!
想象一下,現(xiàn)在有個用戶對一個按鈕狂按,那么我們就對這個操作加鎖
加鎖的思路是這樣的:當(dāng)一條請求過來的時候,我們就做一個標(biāo)識,標(biāo)識當(dāng)前用戶的某一條請求正在被處理,當(dāng)這個用戶的其他請求進(jìn)來的時候,看到有標(biāo)識就對這些請求棄之不顧,然后這一條請求被處理之后,就把這個標(biāo)識拿掉.
看到上面的思路,大伙肯定想到用Spring的aop去實(shí)現(xiàn)這個想法,那么就用aop去實(shí)現(xiàn)它吧!
實(shí)現(xiàn)想法
非常值得注意的一點(diǎn)是,我們現(xiàn)在要實(shí)現(xiàn)的aop是在SpringMVC,而不是直接在Spring當(dāng)中,所以,按常理那樣在Spring的配置文件當(dāng)中配置<aop:aspectj-autoproxy />
和掃描對應(yīng)的aop類是行不通的,一定要在SpringMVC的配置文件當(dāng)中配置這兩樣?xùn)|西,當(dāng)我們是用注解去注冊標(biāo)識aop類的時候,一樣要這樣配置<aop:aspectj-autoproxy proxy-target-class="true" />
,否則會出現(xiàn)錯誤.
這個是注解類:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AvoidPostSameTime {
}
這個是aop類
@Aspect
@Component
public class AvoidPostSameTimeAdvice {
private static EhcacheUtil cache = EhcacheUtil.getInstance();
//與token拼接在一起組成一個叫做runningToken的東西,用來標(biāo)識當(dāng)前用戶的所有請求
private static final String suffix = "running";
@Around("com.cppteam.ulink.comm.aop.LoggerAdvice.executePointcut() && @annotation(AvoidPostSameTime)")
public Object aroundMethod(ProceedingJoinPoint process) {
String runningToken = getRunningToken(process.getArgs());
String runningTokenValue = runningToken+String.valueOf(Thread.currentThread().getId());
try {
synchronized (this) { //這里一定要用同步,同步里面的操作都是對緩存的存儲,所以對性能的影響不大
Object obj = cache.get(Project.ULINK.getValue(), runningToken);
if (obj == null) {
//把runningToken和runningTokenValue存進(jìn)緩存
cache.put(Project.ULINK.getValue(),runningToken,runningTokenValue);
}
}
//在這里再判斷當(dāng)前線程是不是當(dāng)前正在被處理的請求,如果是其他的請求.則不處理
String cacheRunningTokenValue = (String) cache.get(Project.ULINK.getValue(), runningToken);
if(cacheRunningTokenValue != null && cacheRunningTokenValue.equals(runningTokenValue))
return process.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
return BeforeSendJson.install(BeforeSendJson.ERROR,"服務(wù)器出現(xiàn)錯誤");
}
//最后,對于其他的請求就會反饋信息,操作過于頻繁
return BeforeSendJson.install(BeforeSendJson.FAIL, "操作過于頻繁");
}
//無論是正常返回還是拋出了異常,都會執(zhí)行
@After("com.cppteam.ulink.comm.aop.LoggerAdvice.executePointcut() && @annotation(AvoidPostSameTime)")
public void afterRun(JoinPoint point){
String runningToken = getRunningToken(point.getArgs());
String runningTokenValue = runningToken+String.valueOf(Thread.currentThread().getId());
String cacheRunningTokenValue = (String) cache.get(Project.ULINK.getValue(), runningToken);
if(cacheRunningTokenValue != null && cacheRunningTokenValue.equals(runningTokenValue)) {
//移走runningToken這一步非常關(guān)鍵,必須是判斷是當(dāng)前用戶的當(dāng)前可以被處理的請求才可以把它remove掉,因?yàn)閍fterRun方法是任何請求(包括不同用戶的請求)結(jié)束都會調(diào)用,
//所以這也是runningTokenValue這樣設(shè)計的原因,保證是同一個用戶的其中一個請求
cache.remove(Project.ULINK.getValue(),runningToken);
}
}
private String getRunningToken(Object[] args) {
return getUserToken(args) + suffix;
}
private String getUserToken(Object[] args) {
User cachUser = (User) Arrays.asList(args).stream().filter((object) -> object instanceof User && ((User) object).getUser_token() != null).findFirst().get();
return cachUser.getUser_token();
}
}
直接說一下怎么設(shè)置這把鎖吧,我們都知道app當(dāng)中,用戶登錄之后都會有一個token,這個token對應(yīng)的是某一個用戶,然后可以根據(jù)這個token生成一個叫runningToken的東西標(biāo)識當(dāng)前用戶的請求,具體是哪個線程在處理呢,所以就要以runningToken為key,runningTokenValue(runningToken與線程id拼接成的字符串)為值存進(jìn)緩存當(dāng)中,在aop的@After方法中remove掉runningToken的時候,一定要判斷線程是不是當(dāng)前用戶的正在被處理的請求,如果是的話,才可以remove掉它,如果不加限制,加鎖是失敗的.
另外另外,寫完代碼一定要測試,不要盲目自信,我們可以自己模擬一個高并發(fā),看看有沒有問題發(fā)生,模擬高并發(fā)的方法很多,自己搞定吧!