在刷新微信公众号accessToken中使用读写锁

背景:
客户端要调用微信JS_ SDK,需要后端返回jsapi_ticket给前端进行签名,而jsapi_ticket又需要通过accessToken获取。
微信公众号的accessToken是有过期时间的,文档写是2个小时,我们可以通过指定api去刷新accessToken,但是有一点是,如果刷新了accessToken,那么原来的accessToken将会变得不可用,这就可能出现用一个不可用的accessToken来进行请求返回错误。
一般的做法是使用Redis作为全局缓存,存入有过期时间的key,然后定时去刷新这个Redis以及key过期自动去刷新。一般情况下,尽管可能还是会出现多个线程过来,其中有一个拿到的是过期的accessToken的情况,但是其实这种情况很少会发生,就算发生,再请求一次即可。

但是抱着学习成长的心态,决定使用读写锁来控制accessToken的操作,在刷新accessToken的时候加上写锁,此时其他线程不能去获取accessToken,等到写锁释放的时候,一定是拿到最新的。

/**
* 从redis中获取JsapiTicket
*/
public String getJsapiTicketCacheTest(){
        String xdlmJsapiTicket = null;
        if(env.equals(DEV_ENV)){
            // 测试环境逻辑
            System.out.println("线程:" + Thread.currentThread().getName() + "测试环境获取xdlmJsapiTicket数据结果为空");
        } else {
            // 正式环境逻辑
            System.out.println("正式环境获取xdlmJsapiTicket数据结果为空");
        }
        if(StringUtils.isNoneBlank(xdlmJsapiTicket)){
            return xdlmJsapiTicket;
        } else {
            return getJsapiTicketTest(xdzOpenWechatConfig);
        }
    }

    /**
     * accessToken只能在executeTest内部调用,以支撑锁体系
     * @return
     */
    private String getAccessTokenCacheTest(){
//        accessTokenRLock.lock();    // 获取读锁
//        System.out.println("线程:" + Thread.currentThread().getName() + "----------获取读锁成功");
        try {
            String accessToken = null;
            if(env.equals(DEV_ENV)){
                // 测试环境逻辑
                System.out.println("线程:" + Thread.currentThread().getName() + "测试环境获取accessToken数据结果为空");
            } else {
                // 正式环境逻辑
                System.out.println("正式环境获取accessToken数据结果为空");
            }
            if(StringUtils.isNoneBlank(accessToken)){
                return accessToken;
            } else {
                return getAccessTokenTest();
            }
        } finally {
            /**
             * 该方法是可以作为一个独立方法给外部调用,所以应该要释放全部读锁
             */
            /*int readHoldCount = accessTokenReadWriteLock.getReadHoldCount();
            System.out.println("线程:" + Thread.currentThread().getName() + "当前线程获取读锁的次数:" + readHoldCount);
            for(int i=0; i
        }
    }

    private String getJsapiTicketTest(WechatConfig xdzOpenWechatConfig) {
        System.out.println("线程:" + Thread.currentThread().getName() + "获取JsapiTicket");
        String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi";
        String resultContent = executeTest(url, WechatHttpUtil.RequestMethodEnum.GET, null,0);
        System.out.println("线程:" + Thread.currentThread().getName() + "jsapiTicket刷新成功:123456");
//        setJsapiTicketByConfig(wechatConfig,ticket);
        return resultContent;
    }
    private String getAccessTokenTest() {
        System.out.println("线程:" + Thread.currentThread().getName() + "获取accessToken");
        // 这里涉及到读锁线程中获取写锁,所以要完成锁降级;
        //1. 先释放读锁
        int readHoldCount = accessTokenReadWriteLock.getReadHoldCount();
        System.out.println("线程:" + Thread.currentThread().getName() + "当前线程获取读锁的次数:" + readHoldCount);
        for(int i=0; i<readHoldCount; i++){
            accessTokenRLock.unlock();  // 释放读锁
            System.out.println("----------释放读锁成功");
        }
        // 2.获取写锁
        accessTokenWLock.lock();    // 获取写锁
        System.out.println("线程:" + Thread.currentThread().getName() + "获取写锁成功");
        try {
            System.out.println("线程:" + Thread.currentThread().getName() + "正在刷新accessToken");
            TimeUnit.SECONDS.sleep(5);
            System.out.println("线程:" + Thread.currentThread().getName() + "accessToken刷新成功:" + "654321");
            /**
             * 刷新成功后进行锁降级,一直持有读锁
             */
            accessTokenRLock.lock();
//            log.error("accessToken刷新失败");
//            return null;
            return "654321";
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            accessTokenWLock.unlock();  // 释放写锁
            System.out.println("线程:" + Thread.currentThread().getName() + "----------释放写锁成功");
            System.out.println("线程:" + Thread.currentThread().getName() + "锁降级为读锁");
        }
        return null;
    }

    private String executeTest(String url, WechatHttpUtil.RequestMethodEnum get, Object o, int i) {
        System.out.println("线程:" + Thread.currentThread().getName() + "调用url获取JsapiTicket");
        accessTokenRLock.lock();    // 获取读锁
        System.out.println("线程:" + Thread.currentThread().getName() + "----------获取读锁成功");
        try{
            // 该方法也有加锁,所以最后要释放该线程的全部读锁
            String openAccessToken = getAccessTokenCacheTest();
            if (openAccessToken == null) {
                if(i < 10){
                    return executeTest(url, get, o, ++i);
                }
            }
            String randomStr = RandomUtils.getRandomStr(10);
            System.out.println("已获得JsapiTicket为:" + randomStr);
            return randomStr;
        } finally {
            int readHoldCount = accessTokenReadWriteLock.getReadHoldCount();
            System.out.println("线程:" + Thread.currentThread().getName() + "当前线程获取读锁的次数:" + readHoldCount);
            for(int k=0; k<readHoldCount; k++){
                accessTokenRLock.unlock();  // 释放读锁
                System.out.println("----------释放读锁成功");
            }
        }
    }

经过一个下午的编码和调试,做出了下面的总结:

  1. 关于获得微信公众号access_token加读写锁的原因:因为微信的accessToken有一个特点,就是重复获取access_token将导致上次获取的失效,也就是说,如果刚好在刷新access_token的时候,有个线程先拿到了access_token,随后这个token马上被刷新了,那么这个token就用不了了,所以这里需要加锁控制,但是加synchronize同步锁很大情况下会影响并发性,不可能每次读都要加锁,所以这里直接加上读写锁,读asseccToken的时候,不能去刷新数据,去刷新数据的时候,不能去读数据

  2. 当存在读锁在不同方法中获取,而一个线程中又可能经过这两个方法时,需要判断只有获取了读锁才去释放读锁,否则会报错没有可释放的读锁

  3. 如果更新的数据在这个线程中后面额还会被使用到,此时一定要进行锁降级,以免让其他线程更新了这个数据,同时,后面业务如果是最后一步操作,需要注意的是释放这个线程的全部读锁

  4. 对于写锁的获取,如果说在同一个线程中,可能会获取读锁,然后又获取写锁,一定注意要先释放全部的读锁,再获取写锁,因为读锁不能升级为写锁,换句话说,就是先获取了读锁后,再获取写锁会造成死锁。也就是读写锁不能升级,只能锁降级

  5. 读写的方法一定要逻辑清晰,比如像getAccessTokenForCache这个方法,乍一看好像就应该是个公有方法,但是并不可以是,因为在这个方法中,我们进行了如果没有则去刷新accessToken的行为,这个过程是加了写锁。如果这是个公有方法,意味着这个方法在最后要释放全部的读锁,而getAccessTokenForCache中是调用了getAccessToken方法而且保留了一个读锁的,这个读锁不能被释放。所以解决方案是要么将方法抽离,要么这个方法就不要是一个公有方法

p.s. 这里的代码还不是很严谨,应该要使用tryLock来避免获取锁太长时间。
补充知识点:为了避免饥饿竞争,导致写线程获取不到写锁,JVM 会把写锁后面发起的读锁获取操作排在获取写锁操作的后面,也就会 读 – 写 – 读

知识点:https://my.oschina.net/meandme/blog/1839265

你可能感兴趣的:(多线程,JVM,锁,多线程)