一个统计Git代码仓库签入代码行数的批量统计脚本

图片来自pixabay.com的hansbenn会员

本文介绍一个统计Git代码仓库签入代码行数的批量统计脚本。

1. 单个Git代码仓库

对单个Git代码仓库,按提交者进行归类统计代码提交行数。

git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --numstat --author="$name" | awk 'BEGIN{add=0;subs=0;loc=0} {if($1~/^[0-9]+/){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }'; done;

请cd到指定代码仓库目录下,然后运行上面的命令,可以看到如下输出,

peipeihh    13594   275 13869

每列的数字意义如下,

  1. 第一列:代码提交人
  2. 第二列:提交的新增代码行数
  3. 第三列:提交的删除代码行数
  4. 第四列:所有提交的代码行数

2. 批量的Git代码仓库

若是有批量的Git代码仓库,则可以使用如下批量分析脚本。

# 1. parse the date of start and end

sinceDate=$1
untilDate=$2

if [ ! $sinceDate ]; then
    sinceDate="1970-01-01"
fi

if [ ! $untilDate ]; then
    untilDate=`date '+%Y-%m-%d'`
fi

echo "the period of analysis: sinceDate = $sinceDate, untilDate = $untilDate"

# 2. prepare the repository folder

repoTmp=$PWD
repoList="$PWD/repoList.txt"
repoLocalDir="$PWD/repoLocal"
repoReportFile="$PWD/tmpReport.txt"
repoFinalReportFile="$PWD/finalReport.txt"

if [ -d "$repoLocalDir" ]; then
    rm -rf $repoLocalDir;
fi
mkdir -p $repoLocalDir;

if [ -f "$repoReportFile" ]; then
    rm $repoReportFile;
fi
touch $repoReportFile;

if [ ! -f "$repoList" ]; then
    echo "Cannot find repoList.txt, please create it and list all the git repositories to be analyzed, a format is as following, "
    echo "```"
    echo "git@gitee.com:pphh/simple-demo.git master"
    echo "```"
    exit 1
fi

# 3. clone the git repo into local and do the investigation

i=1
cat $repoList | while read line
do
    cd $repoLocalDir

    repoDef=( $line )
    repoName=${repoDef[0]}
    repoBranch=${repoDef[1]}

    if [ -z $repoName ]; then
        continue
    fi

    if [ -z $repoBranch ]; then
        repoBranch="master"
    fi

    repoFolder="./$i-repo-$repoBranch"
    let i++
    echo
    echo "start to clone the repository: $repoName, branch = $repoBranch, folder = $repoFolder"
    git clone $repoName -b $repoBranch $repoFolder
    echo "clone is completed!"

    cd $repoFolder
    echo "try to investigate the submission status of the repository: $repoName, branch = $repoBranch"
    git log --format='%aN' | sort -u | while read name; do git log --numstat --author="$name" --since="$sinceDate" --until="$untilDate" | awk 'BEGIN {add=0;subs=0;all=0} {if($1~/^[0-9]+/){add += $1; subs += $2; all += $1 + $2 }} END {printf "'$name'\t%s\t%s\t%s\n", add, subs, all }' >> $repoReportFile; done;

done

# 4. merge the results

cat $repoReportFile | awk '{ newLines[$1]+=$2;deleteLines[$1]+=$3;all[$1]+=$4 } END {for (i in all) print i,newLines[i],deleteLines[i],all[i];}' > $repoFinalReportFile
rm $repoReportFile
echo
echo "name\tnew-code-lines\tdelete-code-lines\tall"
cat $repoFinalReportFile

演示步骤,

  1. 下载上面的脚本,并放到一个目录下,脚本命名为 analyzeGitRepo.sh。
  2. 在同一个目录下,创建repoList.txt文件,文件中列出所有需要分析的代码仓库,格式样例如下,
git@gitee.com:pphh/simple-demo.git master
git@gitee.com:pphh/blog.git master
  1. 运行脚本,命令格式如下
sh ./analyzeGitRepo.sh "2020-01-01" "2021-07-20"

一个输出结果如下,

% sh ./analyzeGitRepo.sh "2020-01-01" "2021-07-20"
the period of analysis: sinceDate = 2020-01-01, untilDate = 2021-07-20

start to clone the repository: git@gitee.com:pphh/simple-demo.git, branch = master, folder = ./1-repo-master
Cloning into './1-repo-master'...
remote: Enumerating objects: 1182, done.
remote: Counting objects: 100% (279/279), done.
remote: Compressing objects: 100% (195/195), done.
remote: Total 1182 (delta 66), reused 0 (delta 0), pack-reused 903
Receiving objects: 100% (1182/1182), 306.24 KiB | 1.76 MiB/s, done.
Resolving deltas: 100% (267/267), done.
clone is completed!
try to investigate the submission status of the repository: git@gitee.com:pphh/simple-demo.git, branch = master

start to clone the repository: git@gitee.com:pphh/blog.git, branch = master, folder = ./2-repo-master
Cloning into './2-repo-master'...
remote: Enumerating objects: 357, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 357 (delta 2), reused 0 (delta 0), pack-reused 351
Receiving objects: 100% (357/357), 2.16 MiB | 1.82 MiB/s, done.
Resolving deltas: 100% (88/88), done.
clone is completed!
try to investigate the submission status of the repository: git@gitee.com:pphh/blog.git, branch = master

name    new-code-lines  delete-code-lines   all
peipeihh 1586 0 1586
huangyh 0 0 0

分析的报告同时也输出到了目录下的finalReport.txt文件。

3. 演示脚本

见如下代码仓库,
- https://gitee.com/pphh/simple-demo/tree/master/demo-gitrepo-codeline-analysis

Java Agent中使用fastjson导致宿主应用出现找不到类的异常问题

图片来自pixabay.com的designerpoint会员

fastjson是一个目前应用广泛、高性能的Java JSON开发类库,本文记录一个在Java Agent中由于引用了fastjson,进而导致宿主应用(sprint boot)在启动过程中出现找不到类的问题。

1. 问题现象

最近在进行Java Agent的相关开发工作,某天发现在其中一个spring boot应用中报如下异常(在对宿主应用加载了Java Agent情况下),

[main] ERROR org.springframework.boot.SpringApplication 858 | Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'testHttpController': Unsatisfied dependency expressed through field 'xxxHttpClientManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'xxxHttpClientManager' defined in URL [jar:file:/xxx/xxxHttpClientManager.class]: Instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: org/springframework/http/converter/GenericHttpMessageConverter

Caused by: java.lang.NoClassDefFoundError: org/springframework/http/converter/GenericHttpMessageConverter
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
    at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at com.xxx.http.client.xxxHttpClientManager.<>(xxxHttpClientManager.java:63)

若不加载Java Agent,单独启动Spring Boot应用则能够正常运行,只要加载Java Agent就会报如上错误,除此之外,日志中并无其它的特别错误报告。

通过上述日志,分析相关代码,获悉到如下的执行顺序,

  1. Spring Boot应用启动。
  2. Spring Boot应用通过BeanInitializer (spring web 5.1.x)尝试初始化Bean。
  3. Spring Boot应用注入 @Autowired testHttpController。
  4. Spring Boot应用注入 @Autowired xxxHttpClientManager。
  5. 在xxxHttpClientManager中有执行new FastJsonHttpMessageConverter,这个类来自fastjson版本1.2.x。

上面FastJsonHttpMessageConverter位于fastjson类库的support扩展类包中,用于支持Spring的http message转化,其继承自Spring Web 5.1.x类库的GenericHttpMessageConverter类。

2. 问题分析和定位

GenericHttpMessageConverter类位于Spring Web 5.1.x类库中,是Spring Web的一个基础类库,按理说只要Spring Boot应用能够正常启动加载,不应该出现找不到这个类的问题。单独启动Spring Boot应用也确实没有这个问题,其出现在加载Java Agent情况下。

一般来说出现ClassNotFoundException异常,应该就是类不在类加载器的加载路径上。通过Arthas查看类加载器信息,

[arthas@9336]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@4e50df2e
  +-com.taobao.arthas.agent.ArthasClassloader@23d602e5
  +-sun.misc.Launcher$AppClassLoader@18b4aac2
    +-com.xxx.loader.AgentPluginClassLoader@174f0d06
    +-org.springframework.boot.loader.LaunchedURLClassLoader@47dbb1e2

正常情况下,GenericHttpMessageConverter类应该由LaunchedURLClassLoader类加载器负责加载,出现上面异常,难道是被其它加载器加载了?

继续查看和调试分析,发现有如下异常日志,

Caused by: java.lang.ClassNotFoundException: org.springframework.http.converter.GenericHttpMessageConverter
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 60 common frames omitted

根据上面的日志,应用在尝试使用AppClassLoader来加载GenericHttpMessageConverter这个类。于是现在的问题是,为什么应用会尝试走AppClassLoader来加载这个spring mvc类?

由于这个问题和Java Agent有密切相关,只有在加载Java Agent后出现,于是回到Java Agent,打开Agent项目,发现在项目中有引用fastjson类库,

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>${fastjson.version}</version>
</dependency>

从这可以解释相关的问题现象了,如下是整个问题的发生流程,

  1. Java应用启动,通过AppClassLoader加载Java Agent相关类包。
  2. 由于Java Agent引用了fastjson,而Agent是通过AppClassLoader加载,因此AppClassLoader加载了Agent里的fastjson类库。
  3. Spring Web应用开始运行,通过LaunchedURLClassLoader加载Spring相关类,初始化相关Bean。
  4. 应用中有Bean尝试new FastJsonHttpMessageConverter对象,由于Java类加载器双亲委派机制,在寻找FastJsonHttpMessageConverter的类过程中,LaunchedURLClassLoader会让父加载器AppClassLoader先尝试加载,而AppClassLoader中发现了fastjson类库,于是其通过AppClassLoader执行加载,而FastJsonHttpMessageConverter继承自GenericHttpMessageConverter,这个时候AppClassLoader是无法获悉GenericHttpMessageConverter类相关信息,于是报异常。

3. 问题复现

为了更好的了解这个问题,可以下载如下简单的演示项目,

应用中简单地创建一个FastJsonHttpMessageConverter对象,

public static void main(String[] args) {
    System.out.println("this is a simple app...");

    /**
     * 在加载simple-agent的情况下,会报如下异常,
     * java.lang.ClassNotFoundException: org.springframework.http.converter.GenericHttpMessageConverter
     */
    FastJsonHttpMessageConverter fastJsonMsgConverter = new FastJsonHttpMessageConverter();

    System.out.println("fastJsonMessageConverter has been created successfully.");
}

通过mvn clean package编译构建项目代码,会打出如下两个Jar包:simple-app.jar和simple-agent.jar,

+ simple-agent
  + src
  - target
    - simple-agent.jar
+ simple-app
  + src
  - target
    - simple-app.jar

直接运行simple app应用,fastJsonMessageConverter能够正常创建成功。

$ java -jar ./simple-app/target/simple-app.jar

this is a simple app...
fastJsonMessageConverter has been created successfully.

若启动simple app应用时加载Java Agent,则会出现找不到GenericHttpMessageConverter类的异常,问题得到复现。

$ java -javaagent:./simple-agent/target/simple-agent.jar -jar ./simple-app/target/simple-app.jar

this is a demo agent...
this is a simple app...
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
    at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:65)
Caused by: java.lang.NoClassDefFoundError: org/springframework/http/converter/GenericHttpMessageConverter
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
    at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at com.pphh.demo.SimpleApp.main(SimpleApp.java:17)
    ... 8 more
Caused by: java.lang.ClassNotFoundException: org.springframework.http.converter.GenericHttpMessageConverter
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 23 more

4. 解决方案

这个问题是由于fastjson中类有对扩展spring mvc类有依赖导致,若能够解决这个相互依赖,则这个问题可以得到解决。在升级的fastjson2中,可以看到已经把这个spring等相关support扩展类单独出一个类包,

<!-- 扩展类 com.alibaba.fastjson2.support.* -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2-extension</artifactId>
    <version>${fastjson2.version}</version>
</dependency>

因此,在Java Agent中可以通过升级到fastjson2引用,即可得到解决。

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>${fastjson2.version}</version>
</dependency>

5. 相关资料

  1. 高性能的JSON库fastjson
  2. 高性能的JSON库fastjson2

通过WxJava获取微信AccessToken出现acquire timeouted问题

图片来自pixabay.com的Alexas_Fotos会员

WxJava是一个目前应用广泛的微信SDK工具开发包,本文记录一个WxJava获取微信AccessToken失败出现acquire timeouted的问题。

1. 故障现象

最近产线出现了一个严重的技术故障问题,用户无法登录长达1个多小时,通过查看日志发现和WxJava相关,在故障期间内,WxJava一直无法正常获取微信Access Token,报acquire timeouted的错误,见下图。

期间尝试重启两次,在19:20第一次重启应用后问题有所减弱,但是过了5分钟又重新大量发生,直到第二次重启应用后问题消失。

2. 问题定位

通过查看错误日志的堆栈信息,

java.lang.RuntimeException: acquire timeouted
    at cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock.lock(WxMaRedisConfigImpl.java:332)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.getAccessToken(WxMaServiceImpl.java:110)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.executeInternal(WxMaServiceImpl.java:249)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.execute(WxMaServiceImpl.java:215)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.get(WxMaServiceImpl.java:199)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.jsCode2SessionInfo(WxMaServiceImpl.java:178)
    at cn.binarywang.wx.miniapp.api.impl.WxMaUserServiceImpl.getSessionInfo(WxMaUserServiceImpl.java:28)

可以看到和WxJava通过分布式锁获取token有关,产线用的WxJava代码版本是3.6.0,之前已经稳定运行了一年多。

下载相应版本WxJava代码,找到报错的代码行,

// 类文件 - cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl

public class WxMaServiceImpl implements WxMaService {

  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
    if (!this.getWxMaConfig().isAccessTokenExpired() && !forceRefresh) {
      return this.getWxMaConfig().getAccessToken();
    }

    Lock lock = this.getWxMaConfig().getAccessTokenLock();
    lock.lock(); // <-- 问题发生代码行 WxMaServiceImpl.java:110
    try {
      String url = String.format(WxMaService.GET_ACCESS_TOKEN_URL, this.getWxMaConfig().getAppid(),
        this.getWxMaConfig().getSecret());
      try {
        HttpGet httpGet = new HttpGet(url);
        if (this.getRequestHttpProxy() != null) {
          RequestConfig config = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
          httpGet.setConfig(config);
        }
        try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
          String resultContent = new BasicResponseHandler().handleResponse(response);
          WxError error = WxError.fromJson(resultContent, WxType.MiniApp);
          if (error.getErrorCode() != 0) {
            throw new WxErrorException(error);
          }
          WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
          this.getWxMaConfig().updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());

          return this.getWxMaConfig().getAccessToken();
        } finally {
          httpGet.releaseConnection();
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    } finally {
      lock.unlock();
    }

  }
}

// 类文件 - cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock

public class WxMaRedisConfigImpl implements WxMaConfig {

  private class DistributedLock implements Lock {

    private JedisLock lock;

    private DistributedLock(String key) {
      this.lock = new JedisLock(getRedisKey(key));
    }

    @Override
    public void lock() {
      try (Jedis jedis = jedisPool.getResource()) {
        if (!lock.acquire(jedis)) {
          throw new RuntimeException("acquire timeouted"); // <-- 问题发生代码行 WxMaRedisConfigImpl.java:332
        }
      } catch (InterruptedException e) {
        throw new RuntimeException("lock failed", e);
      }
    }

    // ...
  }
}

上面的代码主要是通过redis分布式锁,使得只有一个进程中的一个线程执行token获取并存储到redis,梳理getAccessToken()方法的执行路径,可以发现其大概经历的步骤如下,

若仔细研读这个流程图,可以发现其中有很多相互抢占竞争资源的问题,其中有竞争的资源有3处,

  1. DistributedLock.lock():分布式锁,多个进程/线程抢占这个锁,只有获取到此锁的线程才能执行锁更新操作。
  2. jedisPool.getResource():单进程中多个线程竞争从连接池获取redis连接。
  3. JedisLock.acquire()和JedisLock.release():两个方法被synchronized修饰,这个是一个比较隐蔽的同步琐,被单进程中的多个线程所竞争抢占。

2.1 问题一:在jedisPool.getResource()时等待超时

如下图所示,

整个获取token过程需要多次获取jedisPool.getResource(),特别是获取到分布式锁的线程,即使通过http请求获取到了微信token之后,并不一定能够顺利地将token写入redis,因为写入时还需要再获取一次redis连接,若获取redis连接失败,则可能刷新token失败。

2.2 问题二:多次频繁刷新token

如下图所示,

在多个并发情况下,受到分布式锁的影响,当拥有锁的一个线程在更新token的时候,多个线程会被阻塞在distributedLock.lock(),但第一个拥有分布式锁的线程更新完token,所有其它阻塞的线程会继续执行后续的token刷新操作,进而导致频繁刷新token,在最差情况下,可能会导致一直无序地循环刷新token。

2.3 问题三:无法通过JedisLock.release()释放分布式锁

如下图所示,

当拥有分布式锁的一个线程结束更新token时,需要通过JedisLock.release()释放分布式锁,但是注意JedisLock.acquire()和JedisLock.release()两个方法被synchronized修饰,它们两个需要竞争抢占同一个JedisLock对象上的同步锁,很有可能的情况下,由于其它线程不停的acquire(),导致当前拥有分布式锁的线程永久无法得到释放。

3. 解决方案

可以看到3.6.0版本上的WxMaServiceImpl.getAccessToken()很多问题,根本原因就是在代码实现中几个竞争资源被来回穿插抢占,如何将锁资源的竞争关系解放,是解决关键。

如下是一个解决方案的示例代码,

// 类文件 - cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl

  private String getAccessToken(boolean forceRefresh) throws WxErrorException{
    if (!this.getWxMaConfig().isAccessTokenExpired() && !forceRefresh) {
      return this.getWxMaConfig().getAccessToken();
    }

    // 若是redis分布式锁实现,则走新方法更新token
    if (this.getWxMaConfig() instanceof WxMaRedisConfigImpl){
      return getAccessTokenV2((WxMaRedisConfigImpl) this.getWxMaConfig(), forceRefresh);
    }

    // 略...
  }

  /**
   * 更新redis中的token并返回
   * @param redisConfig
   * @param forceRefresh
   * @return
   */
  private String getAccessTokenV2(WxMaRedisConfigImpl redisConfig, boolean forceRefresh) throws WxErrorException{

    // 处理1:获取分布式锁的当前线程,后续都将使用当前jedis连接完成后续操作
    Jedis jedis = redisConfig.getJedis();

    try{
      DistributedLock lock = redisConfig.AccessTokenLock();

      lock.lock(jedis);
      log.info("当前线程获取到redis分布式锁");
      try {

        // 处理2:二次判断token是否有效
        if (!redisConfig.isAccessTokenExpired(jedis) && !forceRefresh) {
          log.info("二次判断token有效,无需再次刷新");
          return redisConfig.getAccessToken(jedis);
        }

        String url = String.format(WxMaService.GET_ACCESS_TOKEN_URL, this.getWxMaConfig().getAppid(),
          this.getWxMaConfig().getSecret());
        try {
          HttpGet httpGet = new HttpGet(url);
          if (this.getRequestHttpProxy() != null) {
            RequestConfig config = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
            httpGet.setConfig(config);
          }
          Date start = new Date();

          try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
            Date end = new Date();
            String resultContent = new BasicResponseHandler().handleResponse(response);
            WxError error = WxError.fromJson(resultContent, WxType.MiniApp);
            if (error.getErrorCode() != 0) {
              throw new WxErrorException(error);
            }
            WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
            redisConfig.updateAccessToken(jedis, accessToken.getAccessToken(), accessToken.getExpiresIn());
            return accessToken.getAccessToken();
          } finally {
            httpGet.releaseConnection();
          }
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      } finally {
        lock.unlock(jedis);
      }
    } finally {
      jedis.close();
    }
  }

// 类文件 - cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock

  /**
   * 基于redis的分布式锁.
   * 处理3:通过已有jedis连接获取和释放分布式锁,见lock()/unlock()
   */
  private class DistributedLock implements GuoquanRedisLock {

    private JedisLock lock;

    private DistributedLock(String key) {
      this.lock = new JedisLock(getRedisKey(key));
    }

    @Override
    public void lock(Jedis jedis) {
      try {
        if (!lock.acquire(jedis)) {
          throw new RuntimeException("acquire timeouted");
        }
      }catch (InterruptedException e){
        throw new RuntimeException("lock failed", e);
      }
    }

    @Override
    public void unlock(Jedis jedis) {
      lock.release(jedis);
    }

  }

在上面的代码中,主要改变如下,

  1. 处理1:获取分布式锁的当前线程,后续都将使用当前jedis连接完成后续操作。
  2. 处理2:二次判断token是否有效,避免循环刷新token。
  3. 处理3:通过已有jedis连接获取和释放分布式锁,见lock()/unlock()。

应用通过如上代码上线后,acquire timeouted的问题没有再出现,问题得到解决。

4. 发生的版本和后续WxJava优化

上面的问题存在WxJava的多个版本,包括v3.6.0到v3.8.0的版本。

直到v3.9.0及后续版本,可以看到相关问题的陆续修复,如下是两个相关的代码提交,

      if (!this.getWxMaConfig().isAccessTokenExpired() && !forceRefresh) {
        return this.getWxMaConfig().getAccessToken();
      }

locks同一目录下有另外一个RedisTemplateSimpleDistributedLock的实现供配置使用,其在版本v3.9.0已经支持,需要进行如下配置,

# 存储配置redis(可选)
wx.mp.config-storage.type = redistemplate             # 配置类型: Memory(默认), Jedis, RedisTemplate

更多配置请参考README文件

5. 思考

有些问题一直不出现,不代表着不存在,在高并发情况下锁、线程池、连接池等资源竞争问题都将无限放大,可靠的软件产品需要经历一个持续不断打磨的历练过程。