通過自旋鎖來解決多線程遠程調用時會多次獲取token的問題

一,背景

項目中需要調用第三方接口,調用時需要攜帶token;而token會兩個小時失效一次.
原有的邏輯是調用三方接口時,如果返回token失效就先獲取token後再調用三方接口;

  • 問題點
    假設當線程A在獲取token時,線程B也在訪問第三方接口此時token是失效的,於是線程B也會去獲取token,假如線程一多就會造成重複獲取的問題;
    而當第三方接口對token獲取次數限制時,就很容易超過限制次數.

二,解決方式

對獲取token操作進行加鎖

import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author kismet
 * @since 2019-10-30 16:32
 */
public class HttpOperate {

    private static String token;

    private static ReentrantLock lock = new ReentrantLock();


    public static void sendHttp() {
        // 獲取token
        getToken();
        System.out.printf("執行http請求操作,token:%s%n", token);
    }

    private static void getToken() {
        // 模擬token失效情況
        if (token == null) {
            // 加鎖  如果是集羣模式則要使用分佈式鎖
            if (!lock.tryLock()) {
                return;
            }
            try {
                getTokenFromHttp();
            } catch (Exception e) {
                System.out.println("獲取token異常" + Thread.currentThread().getName());
                throw new RuntimeException("獲取token異常" + Thread.currentThread().getName());
            } finally {
                System.out.println("釋放鎖" + Thread.currentThread().getName());
                // 避免遠程調用失敗鎖一直不釋放導致自旋一直執行
                lock.unlock();
            }
        }

    }

    // 模擬遠程調用獲取token
    private static void getTokenFromHttp() {
        try {
            // 模擬網絡延時
            Thread.sleep(1L);
            // 模擬網絡故障
//            int a = 1 / 0;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s線程獲取token中%n", Thread.currentThread().getName());
        token = UUID.randomUUID().toString();
    }
}

測試類

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 *
 * @author kismet
 * @since 2019-10-30 16:49
 */
public class LockTest {

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(10, 10,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>());

        threadPool.submit(HttpOperate::sendHttp);
        threadPool.submit(HttpOperate::sendHttp);
        threadPool.submit(HttpOperate::sendHttp);
        threadPool.submit(HttpOperate::sendHttp);
        threadPool.submit(HttpOperate::sendHttp);
        threadPool.submit(HttpOperate::sendHttp);
        threadPool.submit(HttpOperate::sendHttp);
        threadPool.submit(HttpOperate::sendHttp);

    }
}

測試結果::

執行http請求操作,token:null
執行http請求操作,token:null
執行http請求操作,token:null
執行http請求操作,token:null
執行http請求操作,token:null
pool-1-thread-1線程獲取token中
執行http請求操作,token:null
執行http請求操作,token:null
釋放鎖pool-1-thread-1
執行http請求操作,token:ffa8e8a4-ab87-4f01-abbc-1249182ae180

三,使用自旋鎖改善

如上面測試結果所示,在獲取token期間的其他請求都會是操作失敗;
假如要確保請求不丟失,同時允許線程適當等待,就可以使用自旋鎖的方式來解決

import org.apache.commons.lang3.StringUtils;

import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author kismet
 * @since 2019-10-30 16:32
 */
public class HttpOperate {

    private static String token;

    private static ReentrantLock lock = new ReentrantLock();


    public static void sendHttp() {
        // 獲取token
        getToken();
        System.out.printf("執行http請求操作,token:%s%n", token);
    }

    private static void getToken() {
        // 模擬token失效情況
        if (token == null) {
            // 加鎖  如果是集羣模式則要使用分佈式鎖
            if (!lock.tryLock()) {
                // 當其他線程在獲取token時該線程自旋
                while (StringUtils.isBlank(token) && lock.isLocked()) {
                    System.out.println("自旋中" + Thread.currentThread().getName());
                }
                System.out.println("自旋完成" + Thread.currentThread().getName());
                if (token != null) {
                    return;
                }
            }
            try {
                getTokenFromHttp();
            } catch (Exception e) {
                System.out.println("獲取token異常" + Thread.currentThread().getName());
                throw new RuntimeException("獲取token異常" + Thread.currentThread().getName());
            } finally {
                System.out.println("釋放鎖" + Thread.currentThread().getName());
                // 避免遠程調用失敗鎖一直不釋放導致自旋一直執行
                lock.unlock();
            }
        }

    }

    // 模擬遠程調用獲取token
    private static void getTokenFromHttp() {
        try {
            // 模擬網絡延時
            Thread.sleep(1L);
            // 模擬網絡故障
//            int a = 1 / 0;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s線程獲取token中%n", Thread.currentThread().getName());
        token = UUID.randomUUID().toString();
    }
}

測試結果:

自旋中pool-1-thread-3
自旋中pool-1-thread-3
自旋中pool-1-thread-3
自旋中pool-1-thread-3
自旋中pool-1-thread-3
自旋中pool-1-thread-4
自旋中pool-1-thread-8
自旋中pool-1-thread-8
自旋中pool-1-thread-8
自旋中pool-1-thread-7
自旋中pool-1-thread-6
自旋中pool-1-thread-2
自旋中pool-1-thread-5
自旋完成pool-1-thread-2
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01
釋放鎖pool-1-thread-1
自旋中pool-1-thread-4
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01
自旋中pool-1-thread-3
自旋完成pool-1-thread-3
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01
自旋完成pool-1-thread-4
自旋完成pool-1-thread-7
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01
自旋完成pool-1-thread-6
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01
自旋完成pool-1-thread-5
自旋完成pool-1-thread-8
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01
執行http請求操作,token:6a908de8-8e43-47ca-804b-c4564661ee01

小結

如代碼所示,在獲取token期間;其他調用第三方的請求就是自旋等待,
直到鎖釋放或者獲取到token;
這樣就避免了請求丟失的情況.

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