通过自旋锁来解决多线程远程调用时会多次获取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;
这样就避免了请求丢失的情况.

你可能感兴趣的:(Java)