問題描述
在用戶使用網站創建新聞時,由於網絡波動,導致發送了多個創建新聞的請求,使得系統中存在了冗餘的數據。
問題分析
- 看過很多文章,大部分思路都是:如果同一用戶在很短時間內發送了重複的post請求,那麼後臺只處理第一個請求,後面的請求則過濾掉。
- 具體實現方式則是添加一個springmvc的攔截器,當一個post請求過來時,首先去緩存中查詢短時間內有沒有相同請求到來,如果沒有,則把該請求放入緩存,並將請求傳遞下去;如果有,則不再執行後續操作。
- 但是這種方式也會面臨一個問題:如果兩個請求同時到來,同時讀緩存,這樣兩個請求都不會讀到記錄,導致重複執行了兩個請求。
- 於是我們想到給緩存加鎖,這樣就可以避免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 {
}
}