digdeep

凡是过去,皆是序幕。Read the fucking manual and source code.

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  366 随笔 :: 0 文章 :: 225 评论 :: 300万 阅读
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

背景

      access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

1、为了保密appsecrect,第三方需要一个access_token获取和刷新的中控服务器。而其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则会造成access_token覆盖而影响业务;
2、目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器对外输出的依然是老access_token,此时公众平台后台会保证在刷新短时间内,新老access_token都可用,这保证了第三方业务的平滑过渡;
3、access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。

简单起见,使用一个随servlet容器一起启动的servlet来实现获取access_token的功能,具体为:因为该servlet随着web容器而启动,在该servlet的init方法中触发一个线程来获得access_token,该线程是一个无线循环的线程,每隔2个小时刷新一次access_token。相关代码如下:
1)servlet代码
1
2
3
4
5
6
7
8
9
10
public class InitServlet extends HttpServlet
{
    private static final long serialVersionUID = 1L;
 
    public void init(ServletConfig config) throws ServletException
    {
        new Thread(new AccessTokenThread()).start(); 
    }
 
}

 2)线程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class AccessTokenThread implements Runnable
{
    public static AccessToken accessToken;
     
    @Override
    public void run()
    {
        while(true)
        {
            try{
                AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
                if(token != null){
                    accessToken = token;
                }else{
                    System.out.println("get access_token failed------------------------------");
                }
            }catch(IOException e){
                e.printStackTrace();
            }
             
            try{
                if(null != accessToken){
                    Thread.sleep((accessToken.getExpire_in() - 200) * 1000);    // 休眠7000秒
                }else{
                    Thread.sleep(60 * 1000);    // 如果access_token为null,60秒后再获取
                }
            }catch(InterruptedException e){
                try{
                    Thread.sleep(60 * 1000);
                }catch(InterruptedException e1){
                    e1.printStackTrace();
                }
            }
        }
    }
}

  3)AccessToken代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AccessToken
{
    private String access_token;
    private long expire_in;     // access_token有效时间,单位为妙
     
    public String getAccess_token() {
        return access_token;
    }
    public void setAccess_token(String access_token) {
        this.access_token = access_token;
    }
    public long getExpire_in() {
        return expire_in;
    }
    public void setExpire_in(long expire_in) {
        this.expire_in = expire_in;
    }
}

 4)servlet在web.xml中的配置

  <servlet>
    <servlet-name>initServlet</servlet-name>
    <servlet-class>com.sinaapp.wx.servlet.InitServlet</servlet-class>
    <load-on-startup>0</load-on-startup>
  </servlet>

因为initServlet设置了load-on-startup=0,所以保证了在所有其它servlet之前启动。

其它servlet要使用access_token的只需要调用 AccessTokenThread.accessToken即可。

引出多线程并发问题

1)上面的实现似乎没有什么问题,但是仔细一想,AccessTokenThread类中的accessToken,它存在并发访问的问题,它仅仅由AccessTokenThread每隔2小时更新一次,但是会有很多线程来读取它,它是一个典型的读多写少的场景,而且只有一个线程写。既然存在并发的读写,那么上面的代码肯定是存在问题的。

     一般想到的最简单的方法是使用synchronized来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class AccessTokenThread implements Runnable
{
    private static AccessToken accessToken;
     
    @Override
    public void run()
    {
        while(true)
        {
            try{
                AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
                if(token != null){
                    AccessTokenThread.setAccessToken(token);
                }else{
                    System.out.println("get access_token failed");
                }
            }catch(IOException e){
                e.printStackTrace();
            }
             
            try{
                if(null != accessToken){
                    Thread.sleep((accessToken.getExpire_in() - 200) * 1000);    // 休眠7000秒
                }else{
                    Thread.sleep(60 * 1000);    // 如果access_token为null,60秒后再获取
                }
            }catch(InterruptedException e){
                try{
                    Thread.sleep(60 * 1000);
                }catch(InterruptedException e1){
                    e1.printStackTrace();
                }
            }
        }
    }
 
    public synchronized static AccessToken getAccessToken() {
        return accessToken;
    }
 
    private synchronized static void setAccessToken(AccessToken accessToken) {
        AccessTokenThread2.accessToken = accessToken;
    }
}

 accessToken变成了private,setAccessToken也变成了private,增加了同步synchronized访问accessToken的方法。

那么到这里是不是就完美了呢?就没有问题了呢?仔细想想,还是有问题,问题出在AccessToken类的定义上,它提供了public的set方法,那么所有的线程都在使用AccessTokenThread.getAccessToken()获得了所有线程共享的accessToken之后,任何线程都可以修改它的属性!!!!而这肯定是不对的,不应该的。

2)解决方法一

    我们让AccessTokenThread.getAccessToken()方法返回一个accessToken对象的copy,副本,这样其它的线程就无法修改AccessTokenThread类中的accessToken了。如下修改AccessTokenThread.getAccessToken()方法即可:

1
2
3
4
5
6
public synchronized static AccessToken getAccessToken() {
    AccessToken at = new AccessToken();
    at.setAccess_token(accessToken.getAccess_token());     
    at.setExpire_in(accessToken.getExpire_in());
    return at;
}

 也可以在AccessToken类中实现clone方法,原理都是一样的。当然setAccessToken也变成了private。

3)解决方法二

    既然我们不应该让AccessToken的对象被修改,那么我们为什么不将accessToken定义成一个“不可变对象”?相关修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AccessToken
{
    private final String access_token;
    private final long expire_in;       // access_token有效时间,单位为妙
     
    public AccessToken(String access_token, long expire_in)
    {
        this.access_token = access_token;
        this.expire_in = expire_in;
    }
     
    public String getAccess_token() {
        return access_token;
    }
     
    public long getExpire_in() {
        return expire_in;
    }
}

 如上所示,AccessToken所有的属性都定义成了final类型了,只提供构造函数和get方法。这样的话,其他的线程在获得了AccessToken的对象之后,就无法修改了。改修改要求AccessTokenUtil.freshAccessToken()中返回的AccessToken的对象只能通过有参的构造函数来创建。同时AccessTokenThread的setAccessToken也要修改成private,getAccessToken无须返回一个副本了。

注意不可变对象必须满足下面的三个条件:

a) 对象创建之后其状态就不能修改;

b) 对象的所有域都是final类型;

c) 对象是正确创建的(即在对象的构造函数中,this引用没有发生逸出);

4)解决方法三

    还有没有其他更加好,更加完美,更加高效的方法呢?我们分析一下,在解决方法二中,AccessTokenUtil.freshAccessToken()返回的是一个不可变对象,然后调用private的AccessTokenThread.setAccessToken(AccessToken accessToken)方法来进行赋值。这个方法上的synchronized同步起到了什么作用呢?因为对象时不可变的,而且只有一个线程可以调用setAccessToken方法,那么这里的synchronized没有起到"互斥"的作用(因为只有一个线程修改),而仅仅是起到了保证“可见性”的作用,让修改对其它的线程可见,也就是让其他线程访问到的都是最新的accessToken对象。而保证“可见性”是可以使用volatile来进行的,所以这里的synchronized应该是没有必要的,我们使用volatile来替代它。相关修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class AccessTokenThread implements Runnable
{
    private static volatile AccessToken accessToken;
     
    @Override
    public void run()
    {
        while(true)
        {
            try{
                AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
                if(token != null){
                    AccessTokenThread2.setAccessToken(token);
                }else{
                    System.out.println("get access_token failed");
                }
            }catch(IOException e){
                e.printStackTrace();
            }
             
            try{
                if(null != accessToken){
                    Thread.sleep((accessToken.getExpire_in() - 200) * 1000);    // 休眠7000秒
                }else{
                    Thread.sleep(60 * 1000);    // 如果access_token为null,60秒后再获取
                }
            }catch(InterruptedException e){
                try{
                    Thread.sleep(60 * 1000);
                }catch(InterruptedException e1){
                    e1.printStackTrace();
                }
            }
        }
    }
 
    private static void setAccessToken(AccessToken accessToken) {
        AccessTokenThread2.accessToken = accessToken;
    }<br>        public static AccessToken getAccessToken() {<br>               return accessToken;<br>        }
}

 也可以这样改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class AccessTokenThread implements Runnable
{
    private static volatile AccessToken accessToken;
     
    @Override
    public void run()
    {
        while(true)
        {
            try{
                AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
                if(token != null){
                    accessToken = token;
                }else{
                    System.out.println("get access_token failed");
                }
            }catch(IOException e){
                e.printStackTrace();
            }
             
            try{
                if(null != accessToken){
                    Thread.sleep((accessToken.getExpire_in() - 200) * 1000);    // 休眠7000秒
                }else{
                    Thread.sleep(60 * 1000);    // 如果access_token为null,60秒后再获取
                }
            }catch(InterruptedException e){
                try{
                    Thread.sleep(60 * 1000);
                }catch(InterruptedException e1){
                    e1.printStackTrace();
                }
            }
        }
    }
 
    public static AccessToken getAccessToken() {
        return accessToken;
    }
}

 还可以这样改:

复制代码
public class AccessTokenThread implements Runnable 
{
    public static volatile AccessToken accessToken;
    
    @Override
    public void run() 
    {
        while(true) 
        {
            try{
                AccessToken token = AccessTokenUtil.freshAccessToken();    // 从微信服务器刷新access_token
                if(token != null){
                    accessToken = token;
                }else{
                    System.out.println("get access_token failed");
                }
            }catch(IOException e){
                e.printStackTrace();
            }
            
            try{
                if(null != accessToken){
                    Thread.sleep((accessToken.getExpire_in() - 200) * 1000);    // 休眠7000秒
                }else{
                    Thread.sleep(60 * 1000);    // 如果access_token为null,60秒后再获取
                }
            }catch(InterruptedException e){
                try{
                    Thread.sleep(60 * 1000);
                }catch(InterruptedException e1){
                    e1.printStackTrace();
                }
            }
        }
    }
}
复制代码

accesToken变成了public,可以直接是一个AccessTokenThread.accessToken来访问。但是为了后期维护,最好还是不要改成public.

其实这个问题的关键是:在多线程并发访问的环境中如何正确的发布一个共享对象。

 

其实我们也可以使用Executors.newScheduledThreadPool来搞定:

复制代码
public class InitServlet2 extends HttpServlet 
{
    private static final long serialVersionUID = 1L;

    public void init(ServletConfig config) throws ServletException 
    {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleAtFixedRate(new AccessTokenRunnable(), 0, 7200-200, TimeUnit.SECONDS);
    }
}
复制代码
复制代码
public class AccessTokenRunnable implements Runnable 
{
    private static volatile AccessToken accessToken;
    
    @Override
    public void run() 
    {
        try{
            AccessToken token = AccessTokenUtil.freshAccessToken();    // 从微信服务器刷新access_token
            if(token != null){
                accessToken = token;
            }else{
                System.out.println("get access_token failed");
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    public static AccessToken getAccessToken() 
    {
        while(accessToken == null){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return accessToken;
    }
    
}
复制代码

获取accessToken方式变成了:AccessTokenRunnable.getAccessToken();

 

posted on   digdeep  阅读(6785)  评论(8编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
不懂数据库和Web安全的架构师不是一个好的程序员。
点击右上角即可分享
微信分享提示