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