一,背景
項目中需要調用第三方接口,調用時需要攜帶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;
這樣就避免了請求丟失的情況.