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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容