基於分佈式鎖的防止重複請求解決方案

1、前言

        關於重複請求,指的是咱們服務端接收到很短的時間內的多個相同內容的重複請求。而這樣的重複請求若是是冪等的(每次請求的結果都相同,如查詢請求),那其實對於咱們沒有什麼影響,但若是是非冪等的(每次請求都會對關鍵數據形成影響,如刪除關係、創建關係等),那就會輕則產生髒數據,重則致使系統錯誤。前端

        所以,在當前廣泛分佈式服務的狀況下,如何避免和解決重複請求給咱們帶來的數據異常成爲了亟待解決的問題。而避免重複請求,最好的作法是先後端共同去作。java

        1. 前端或客戶端在非冪等的按鈕上直接作禁止提交重複請求的操做。git

        2. 後端在接收到請求時加鎖,完成後解鎖。redis

        這篇博客主要講的是在後端基於分佈式鎖的概念去出一個關於解決重複請求的通用解決方案。算法

2、正文

        爲什麼要使用分佈式鎖來解決呢?由於咱們當前廣泛的架構都是分佈式的服務端,前端請求經過網關層轉發至後端,以下圖所示,所以若是隻在一個單獨的服務器上作限制,就沒法在分佈式的服務中完成應對高頻次的重複請求了。spring

              

image

基本思路

        思路基本上是對須要作防止重複請求的接口加上分佈式鎖,步驟以下:後端

  1. 在接收到請求後,根據方法名+參數取md5值,獲取該方法及該參數的惟一標識;
  2. 獲取標識後設置分佈式鎖,而且設置過時時間;
  3. 在請求結束後,釋放分佈式鎖。

        便可完成對當前請求的重複請求禁止。若是想作通用的解決方案,那就須要把上述步驟作出一個小功能出來,因爲本人對java、spring框架比較熟悉,就拿這個來作個示例。bash

基於spring切面、redis的實現

        想必一些熟悉spring的同窗已經知道我想採用什麼方式了,作通用型的,確定要用到spring的aop特性,註解+切面+md5key+反射+redis實現,具體以下:服務器

  1. 定義一個分佈式鎖註解,註解包含過時時間設置、忽略參數;
  2. 定義一個切面,切點爲分佈式鎖註解,在切面中獲取須要使用分佈式鎖的方法名、參數、過時時間,而且將方法名及未被忽略參數作md5取惟一標識;
  3. 再根據上述惟一標識設置redsis分佈式鎖;
  4. 方法結束後解鎖。

        代碼以下:架構

註解

        定義名稱爲RepeatOperationLock的註解,參數有鎖過時時間及忽略屬性(即不參與分佈式鎖標識MD5計算的屬性)。

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Component
public @interface RepeatOperationLock {
    /**
     * 鎖時長,默認500ms
     * @return
     */
    long timeOut() default 500;

    /**
     * 忽略上鎖參數位置,從0開始
     * @return
     */
    int[] ignoreIndex();
}
複製代碼

切面

        切點爲上述註解,切面中作了如下幾件事,獲取方法名、獲取註解屬性(過時時間、忽略屬性)、計算方法+屬性的md5值、調用外部分佈式鎖的方法。

@Aspect
@Slf4j
@Component
public class LockAspect {

    @Autowired
    RepeatLockService repeatLockService;

    @Pointcut("@annotation(com.ls.javabase.aspect.annotation.RepeatOperationLock)")
    public void serviceAspect() {
    }

    @Before("serviceAspect()")
    public void setLock(JoinPoint point) {
        log.info("防止方法重複調用接口鎖,上鎖,point:{}", point);
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
        if (Objects.isNull(repeatOperationLock)) {
            log.warn("---repeatOperationLock is null---");
            return;
        }
        long timeOut = repeatOperationLock.timeOut();
        int [] ignoreIndex = repeatOperationLock.ignoreIndex();
        log.info("lockTime——{}", timeOut);
        if (Objects.isNull(timeOut)) {
            log.warn("---timeOut is null");
            return;
        }
        String methodName = method.getName();
        Object[] args = point.getArgs();


        repeatLockService.setRepeatLock(methodName, args, timeOut);
    }

    @After("serviceAspect()")
    public void removeLock(JoinPoint point) {
        log.info("防止方法重複調用接口鎖,解鎖,point:{}",point);
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
        if (Objects.isNull(repeatOperationLock)) {
            log.warn("---repeatOperationLock is null---");
            return;
        }
        long timeOut = repeatOperationLock.timeOut();
        log.info("lockTime——{}", timeOut);
        if (Objects.isNull(timeOut)) {
            log.warn("---timeOut is null");
            return;
        }
        String methodName = method.getName();
        Object[] args = point.getArgs();
        repeatLockService.removeRepeatLock(methodName, args);
    }

    /**
     *
     * @param args
     * @param ignoreIndex
     * @return
     */
    private Object [] getEffectiveArgs(Object[] args,int [] ignoreIndex) {
        for (int i:ignoreIndex){
            args[i] = null;
        }
        for (Object obj:args){
            if (obj==null){

            }
        }
        return args;
    }
}
複製代碼

md5方法

public class Md5Encode {

    /**
     * constructors
     */
    private Md5Encode() {

    }

    /**
     * @param s 須要hash的字符串
     * @return hash以後的字符串
     */
    public static final String md5(final String s) {
        char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        try {
            byte[] btInput = s.getBytes(Charset.defaultCharset());
            // 得到MD5摘要算法的 MessageDigest 對象
            MessageDigest mdInst = MessageDigest.getInstance("MD5");
            // 使用指定的字節更新摘要
            mdInst.update(btInput);
            // 得到密文
            byte[] md = mdInst.digest();
            // 把密文轉換成十六進制的字符串形式
            int j = md.length;
            char[] str = new char[j * 2];
            int k = 0;
            for (int i = 0; i < j; i++) {
                byte byte0 = md[i];
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
複製代碼

分佈式鎖

        這裏的分佈式鎖使用redis,好比鎖繪本誤解,後續會作出改進,實現一個完整的分佈式鎖方案,寫到博客裏。

@Slf4j
@Service
public class RepeatLockService {

    @Autowired
    RepeatRedisUtil repeatRedisUtil;

    public void setRepeatLock(String methodName, Object[] args, Long expireTime) {
        for (Object obj : args) {
            log.info("方法名:{},對象:{},對象hashcode:{}", methodName, obj, obj.hashCode());
        }
        Boolean lock = repeatRedisUtil.setRepeatLock(methodName, args, expireTime);
        if (!lock) {
            log.info("已有相同請求");
        }
    }

    public void removeRepeatLock(String methodName, Object[] args) {
        repeatRedisUtil.removeRepeatLock(methodName, args);
    }
}

@Component
public class RepeatRedisUtil {
    @Autowired
    RedisTemplate redisTemplate;

    private static final String repeatLockPrefix = "repeat_lock_";

    /**
     * 設置重複請求鎖,這一塊的分佈式鎖的加與釋放有問題,後續會專門出個文章總結redis分佈式鎖
     * @param methodName
     * @param args
     * @param expireTime 過時時間ms
     * @return
     */
    public boolean setRepeatLock(String methodName, Object[] args,long expireTime) {
        String key = getRepeatLockKey(methodName, args);
        try {
            boolean b = (boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    Jedis jedis = null;
                    try {
                        jedis = (Jedis) connection.getNativeConnection();
                        String status = jedis.set(key, "1", NX, EX, expireTime);
                        if (setNXStatus.equals(status)) {
                            return Boolean.TRUE;
                        }
                        return Boolean.FALSE;
                    }finally {
                        connection.close();
                    }

                }
            });
            return b;
        } catch (Exception e) {
            log.error("redis操做異常:{}",e);
            return Boolean.FALSE;
        }
    }

    /**
     * 刪除重複請求鎖
     * @param methodName
     * @param args
     */
    public void removeRepeatLock(String methodName, Object[] args){
        String key = getRepeatLockKey(methodName, args);
        redisTemplate.delete(key);
    }

    /**
     * 獲取重複請求鎖Key
     *
     * @param methodName
     * @param args
     * @return
     */
    public String getRepeatLockKey(String methodName, Object[] args) {
        String repeatLockKey = repeatLockPrefix + methodName;
        for (Object obj : args) {
            repeatLockKey = repeatLockKey+"_"+ obj.hashCode();
        }
        return repeatLockKey;
    }
}
複製代碼

測試service接口

        即在方法上使用註解便可,表明過時時間爲200000ms,忽略第二個參數。

@Slf4j
@Service
public class TestLockService {

    @RepeatOperationLock(timeOut = 200000, ignoreIndex = 1)
    public void testLock(UserDto userDto,int i){
        log.info("service中屬性:{},{}",userDto,i);
        log.info("service中hashcode,userDto:{},i:{}",userDto.hashCode(),i);
    }
}
複製代碼

結語

        這樣一個基於spring的通用分佈式鎖解決方案就分享完畢了,確實還存在着一些瑕疵,好比解鎖時沒有判斷是否會被誤解等等,後續會專門做出分佈式鎖的總結並改進,上面也只是提出了一個基於分佈式鎖解決重複請求的思想,也但願能多多交流。

相關文章
相關標籤/搜索