spring層面處理網絡抖動導致的重複寫入數據,實現請求的冪等性

問題描述

在用戶使用網站創建新聞時,由於網絡波動,導致發送了多個創建新聞的請求,使得系統中存在了冗餘的數據。

問題分析

  1. 看過很多文章,大部分思路都是:如果同一用戶在很短時間內發送了重複的post請求,那麼後臺只處理第一個請求,後面的請求則過濾掉。
  2. 具體實現方式則是添加一個springmvc的攔截器,當一個post請求過來時,首先去緩存中查詢短時間內有沒有相同請求到來,如果沒有,則把該請求放入緩存,並將請求傳遞下去;如果有,則不再執行後續操作。
  3. 但是這種方式也會面臨一個問題:如果兩個請求同時到來,同時讀緩存,這樣兩個請求都不會讀到記錄,導致重複執行了兩個請求。
  4. 於是我們想到給緩存加鎖,這樣就可以避免3中出現的情況。但是衆所周知,普通的加鎖是很影響效率的,我們也不能捨棄效率來保證冪等性。爲了提升加鎖後的效率,我採用了DCL(雙重檢查鎖,沒有學過的小夥伴可以去查一查)來進行緩存的加鎖讀寫。

代碼

由於我的應用是單例的,所以採用guava來做緩存。

import com.csdc.cett.exception.RequestException;
import com.csdc.cett.util.MD5;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;

/**
 * 處理重複請求,保持請求的冪等性
 * 使用雙重檢查鎖定(DCL)
 *
 * @author ksyzz
 * @since <pre>2019/06/20</pre>
 */
@Component
public class RequestInterceptor implements HandlerInterceptor {

    private static volatile Cache<String, String> loadingCache = CacheBuilder.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .build();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 只需要保持用戶登錄後的POST請求的冪等性,此處要排除文件分片上傳
        if ("POST".equals(request.getMethod()) && request.getHeader("Auth-Token") != null && !request.getRequestURI().contains("/upload/files")) {
            // 要求:用戶5秒內不能重複提交相同url,相同參數的請求
            // 存儲方式爲 md5(URI+Auth-Token+RequestParams+InputStream)
            StringBuilder md5 = new StringBuilder();
            // Auth-Token爲用戶身份標識
            String token = request.getHeader("Auth-Token");
            md5.append(request.getRequestURI());
            md5.append(token);
            Enumeration<String> parameterNames = request.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String key = parameterNames.nextElement();
                String parameter = request.getParameter(key);
                md5.append("&").append(key).append("=").append(parameter);
            }
            String cacheKey = MD5.getHashString(md5.toString());

            // 校驗是否已經提交過請求

            if (loadingCache.getIfPresent(cacheKey) == null) {
                synchronized (loadingCache) {
                    if (loadingCache.getIfPresent(cacheKey) == null) {
                        // 此處value可不設置
                        loadingCache.put(cacheKey, "");
                        return true;
                    }
                }
            }
            throw new RequestException("請勿重複提交");
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章