服務(wù)器端如何防止在同一時刻接收多個請求?

目前在做一個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ā)的方法很多,自己搞定吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贸典,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡缭付,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門循未,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陷猫,“玉大人,你說我怎么就攤上這事的妖⌒迕剩” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵羔味,是天一觀的道長。 經(jīng)常有香客問我钠右,道長赋元,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任飒房,我火速辦了婚禮搁凸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狠毯。我一直安慰自己护糖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布嚼松。 她就那樣靜靜地躺著嫡良,像睡著了一般。 火紅的嫁衣襯著肌膚如雪献酗。 梳的紋絲不亂的頭發(fā)上寝受,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音罕偎,去河邊找鬼很澄。 笑死,一個胖子當(dāng)著我的面吹牛颜及,可吹牛的內(nèi)容都是我干的甩苛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼俏站,長吁一口氣:“原來是場噩夢啊……” “哼讯蒲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起肄扎,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤爱葵,失蹤者是張志新(化名)和其女友劉穎施戴,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萌丈,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赞哗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辆雾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肪笋。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖度迂,靈堂內(nèi)的尸體忽然破棺而出藤乙,到底是詐尸還是另有隱情,我是刑警寧澤惭墓,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布坛梁,位于F島的核電站,受9級特大地震影響腊凶,放射性物質(zhì)發(fā)生泄漏划咐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一钧萍、第九天 我趴在偏房一處隱蔽的房頂上張望褐缠。 院中可真熱鬧,春花似錦风瘦、人聲如沸队魏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胡桨。三九已至,卻和暖如春瞬雹,著一層夾襖步出監(jiān)牢的瞬間登失,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工挖炬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揽浙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓意敛,卻偏偏與公主長得像馅巷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子草姻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理钓猬,服務(wù)發(fā)現(xiàn),斷路器撩独,智...
    卡卡羅2017閱讀 134,601評論 18 139
  • 從三月份找實(shí)習(xí)到現(xiàn)在敞曹,面了一些公司账月,掛了不少,但最終還是拿到小米澳迫、百度局齿、阿里、京東橄登、新浪抓歼、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,192評論 11 349
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,520評論 25 707
  • 明天拢锹,是你我的初遇 我告訴你 我心中的歡喜 明月谣妻,是我對你的期許 我告訴她 我心中的猶豫 明年,是你我的重逢 但 ...
    啟之明星閱讀 137評論 1 2
  • 記得在《繼承者們》的 I love California ! 一件普通不可在普通的T恤充坑,總在恩尚情淡的時...
    其其愛飛行閱讀 224評論 0 1