分佈式系統後臺如何防止重複提交

分佈式系統後臺如何防止重複提交

分佈式系統網絡拓撲結構

場景描述

秒殺系統提交訂單時,由於用戶連續快速點擊,並且前端沒有針對性處理,導致連續發送兩次請求,一次命中服務器A,另一次命中服務器B, 那麼就生成了兩個內容完全相同的訂單,只是訂單號不同而已.

重複提交的後果

  1. 用戶在界面看到兩個一模一樣的訂單,不知道應該支付哪個;
  2. 系統出現異常數據,影響正常的校驗.

解決方法

解決思路:相同的請求在同一時間只能被處理一次.
如果是單體服務器,我們可以通過多線程併發的方式解決,但是目前大部分系統,採用了多機負載均衡.

分佈式鎖

  1. 服務器A接收到請求之後,獲取鎖,獲取成功 ,
  2. 服務器A進行業務處理,訂單提交成功;
  3. 服務器B接收到相同的請求,獲取鎖,失敗,
    因爲鎖被服務器A獲取了,並且未釋放
  4. 服務器A處理完成,釋放鎖 實現採用redis ,

    參考:http://www.importnew.com/27477.html

利用數據庫唯一性約束

實現思路: 對請求信息進行hash運算,得到一個hash值,
相同的請求信息得到相同的hash值(換成md5也可以) 步驟:

  1. 接口A接收到請求之後,對請求信息hash運算,得到hash值hashCodeA;
  2. 保存hashCodeA 到數據庫,並且對應的數據庫的列(column)滿足unique約束;
  3. 保存成功之後,才進行正常業務邏輯處理,比如提交訂單;
  4. 服務器B接收到相同的請求後,也得到相同的hash值,hashCodeA,
  5. 服務器B 保存hashCodeA 到數據庫,肯定失敗,因爲相同的hash值已經存在;
  6. 因爲保存失敗,所以後面的業務邏輯不會執行.

示例代碼: 控制器中:


// 使用數據庫約束條件,防止重複提交
        try {
            Integer userId = getCurrentId();
            String hashSource = WebServletUtil.buildHashSource(request, userId);
            reqOrderLock(hashSource, houseInfo.getId());
        } catch (IOException e) {
            e.printStackTrace();
        }
		

 Service中:

/***
     * 利用數據庫的唯一性約束<br />
     * 防止重複提交
     * @param queryString
     */
    public void reqOrderLock(String queryString, Integer houseInfoId) {
        long crc32Long = EncryptionUtil.getHash(queryString);
        this.orderReqLockDao.addUnique(String.valueOf(crc32Long), houseInfoId, Constant2.Order_type_Label_VISIT_ORDER);
    }

 

dao 中:

public OrderReqLock addUnique(String crc32, Integer houseInfoId, String orderTypeLabel) {
        OrderReqLock orderReqLock = new OrderReqLock();
        orderReqLock.setCrc32(crc32);
        if (null != houseInfoId) {
            orderReqLock.setHouseInfoId(houseInfoId);
        }

        orderReqLock.setOrderTypeLabel(orderTypeLabel);
        CreateTimeDto createTimeDto = TimeHWUtil.getCreateTimeDao();
        orderReqLock.setCreateTime(createTimeDto.getCreateTime());
        orderReqLock.setUpdateTime(createTimeDto.getUpdateTime());
        try {
            add(orderReqLock);
        } catch (org.hibernate.exception.ConstraintViolationException e) {
            e.printStackTrace();
            LogicExc.throwEx(Constant2.ERROR_CODE_Repeat_Operation, "您重複提交了訂單,訂單類型" );
        } 
        return orderReqLock;
    }

 當然有個隱患: 在增加完鎖,即執行addUnique 方法之後,程序掛了,不管是網絡原因還是數據庫崩潰, 當服務恢復之後,相同的請求無法提交了,因爲數據庫已經保存了請求的hash(但是實際上,後面的業務邏輯還沒有來得及執行). 原因:鎖操作addUnique 和業務邏輯肯定不在同一個數據庫事務中

前端的解決方法

思路: 進入添加頁面時,獲取服務器端的token,
提交時把token提交過去,判斷token是否存在,
若存在,則進行後續正常業務邏輯,
如不存在,則報錯重複提交.

流程圖

添加頁面接口 使用註解:@RepeatToken(save = true) 提交接口 使用註解 :@RepeatToken(remove = true) token 攔截器代碼

package com.girltest.web.controller.intercept;

import com.common.util.WebServletUtil;
import org.apache.log4j.Logger;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.UUID;

/**
 * Created by 黃威 on 9/14/16.<br >
 */
public class TokenInterceptor extends HandlerInterceptorAdapter {
    private static final Logger LOG = Logger.getLogger(TokenInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatToken annotation = method.getAnnotation(RepeatToken.class);
            if (annotation != null) {
                boolean needSaveSession = annotation.save();
                if (needSaveSession) {
                    request.getSession(true).setAttribute("token", UUID.randomUUID().toString());
                }
                boolean needRemoveSession = annotation.remove();
                if (needRemoveSession) {
                    if (isRepeatSubmit(request)) {
                        LOG.warn("please don't repeat submit,url:" + request.getServletPath());
                        //如果重複提交,則重定向到列表頁面
                        response.sendRedirect(WebServletUtil.getBasePath(request) + "test/list");
                        return false;
                    }
                    request.getSession(true).removeAttribute("token");
                }
            }
            return true;
        } else {
            return super.preHandle(request, response, handler);
        }
    }

    /***
     *
     * @param request
     * @return : true:報錯需要重定向 <br />
     * false: 處理後續的正常業務邏輯
     */
    private boolean isRepeatSubmit(HttpServletRequest request) {
        String serverToken = (String) request.getSession(true).getAttribute("token");
        if (serverToken == null) {
            return true;
        }
        String clinetToken = request.getParameter("token");
        if (clinetToken == null) {
            return true;
        }
        if (!serverToken.equals(clinetToken)) {
            return true;
        }
        return false;
    }
}

 

總結

  1. 第二種方法(利用數據庫完整性約束)最簡便,但是會訪問(讀寫)數據庫,給數據庫造成一定的壓力;
    但也有個隱患,程序執行中途故障了(網絡垮了,服務宕了...),後面重複提交,就無法成功了.也有解決方法:定時器清理這個hash數據庫表
  2. 第一種方法最複雜,但是符合高性能,高可用

參考

https://my.oschina.net/huangweiindex/blog/1837706
參考:http://www.importnew.com/27477.html

https://my.oschina.net/huangweiindex/blog/1843927

 

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