Skip to content

修复长时间运行时 Redis 命令中断导致 accessToken 刷新失败的问题#3932

Open
Copilot wants to merge 2 commits intodevelopfrom
copilot/fix-access-token-concurrency-issue
Open

修复长时间运行时 Redis 命令中断导致 accessToken 刷新失败的问题#3932
Copilot wants to merge 2 commits intodevelopfrom
copilot/fix-access-token-concurrency-issue

Conversation

Copy link
Contributor

Copilot AI commented Mar 17, 2026

在长时间批量操作场景下,线程被中断后 Lettuce 抛出 RedisCommandInterruptedException(Spring Data Redis 包装为 RedisSystemException),该异常从 isAccessTokenExpired() 向上传播至调用链顶端,导致整个操作崩溃。由于中断标志未被清除,后续所有 Redis 操作也会连续失败。

变更

AbstractWxCpInRedisConfigImpl

  • isAccessTokenExpired()isJsapiTicketExpired()isAgentJsapiTicketExpired() 添加异常捕获
  • Redis 操作失败时:记录 warn 日志,调用 Thread.interrupted() 清除中断标志(否则后续锁获取/token 刷新也会失败),返回 true 触发 token 刷新
@Override
public boolean isAccessTokenExpired() {
  try {
    Long expire = redisOps.getExpire(this.accessTokenKey);
    return expire == null || expire < 2;
  } catch (Exception e) {
    log.warn("获取access_token过期时间时发生异常,将视为已过期以触发刷新,异常信息: {}", e.getMessage());
    Thread.interrupted(); // 清除中断标志,确保后续锁获取和 token 刷新能正常执行
    return true;
  }
}

RedisTemplateSimpleDistributedLock

  • lock() 捕获 InterruptedException 时原为 // Ignore,改为 Thread.currentThread().interrupt() 正确恢复中断标志
Original prompt

This section details on the original issue you should resolve

<issue_title>长时间删除二维码configId, 程序accessToken报错 无法有效兼容? 貌似对于accessToken的并发有点问题?</issue_title>
<issue_description>

简要描述

程序批量跑删除二维码的 configId,大概跑了10来个小时突然报错,如下日志:
应该是token方面的原因?

模块版本情况

  • WxJava 模块名:
  • WxJava 版本号:

    com.github.binarywang
    weixin-java-cp
    4.6.0

详细描述

尽量详细描述。请不要使用截图,尽量使用文字描述,代码直接贴上来,日志则请附在后面所示区域。
挨个configId批量删除, 删除了10来个小时,有30来万个无用的二维码configId需要删除

        wxCpService.getExternalContactService().deleteContactWay(configId);

日志

org.springframework.data.redis.RedisSystemException: Redis command interrupted; nested exception is io.lettuce.core.RedisCommandInterruptedException: Command interrupted
	at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:62) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:277) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.await(LettuceConnection.java:1085) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.lambda$doInvoke$4(LettuceConnection.java:938) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.lettuce.LettuceInvoker$Synchronizer.invoke(LettuceInvoker.java:665) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.lettuce.LettuceInvoker.just(LettuceInvoker.java:94) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.ttl(LettuceKeyCommands.java:322) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.DefaultedRedisConnection.ttl(DefaultedRedisConnection.java:221) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.connection.DefaultStringRedisConnection.ttl(DefaultStringRedisConnection.java:1428) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.core.RedisTemplate.lambda$getExpire$12(RedisTemplate.java:866) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:191) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at org.springframework.data.redis.core.RedisTemplate.getExpire(RedisTemplate.java:866) ~[spring-data-redis-2.7.10.jar!/:2.7.10]
	at me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps.getExpire(RedisTemplateWxRedisOps.java:32) ~[weixin-java-common-4.6.0.jar!/:?]
	at me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl.isAccessTokenExpired(AbstractWxCpInRedisConfigImpl.java:123) ~[weixin-java-cp-4.6.0.jar!/:?]
	at me.chanjar.weixin.cp.api.impl.WxCpServiceImpl.getAccessToken(WxCpServiceImpl.java:40) ~[weixin-java-cp-4.6.0.jar!/:?]
	at me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl.executeInternal(BaseWxCpServiceImpl.java:337) ~[weixin-java-cp-4.6.0.jar!/:?]
	at me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl.execute(BaseWxCpServiceImpl.java:289) ~[weixin-java-cp-4.6.0.jar!/:?]
	at me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl.post(BaseWxCpServiceImpl.java:258) ~[weixin-java-cp-4.6.0.jar!/:?]

</issue_description>

Comments on the Issue (you are @copilot in this section)

Custom agent used: 全部用中文
需要用中文,包括PR标题和分析总结过程


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix access token concurrency issue during QR code deletion 修复长时间运行时 Redis 命令中断导致 accessToken 刷新失败的问题 Mar 17, 2026
Copilot AI requested a review from binarywang March 17, 2026 02:41
@binarywang binarywang marked this pull request as ready for review March 17, 2026 02:42
Copilot AI review requested due to automatic review settings March 17, 2026 02:42
@binarywang binarywang added this to the 4.8.3 milestone Mar 17, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e50d1c2782

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore
Thread.currentThread().interrupt();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid re-interrupting inside lock retry loop

When lock() is waiting and Thread.sleep is interrupted, setting the interrupt flag immediately inside the loop causes the next iteration to run with Thread.currentThread().isInterrupted()==true. In production with Lettuce/Spring Redis, subsequent tryLock() Redis calls can then fail immediately with RedisCommandInterruptedException (or loop without effective sleeping), so a single interrupt while contending for the lock can make lock acquisition fail continuously instead of retrying normally.

Useful? React with 👍 / 👎.

@augmentcode
Copy link

augmentcode bot commented Mar 17, 2026

🤖 Augment PR Summary

总结:该 PR 主要修复长时间批量操作时线程被中断导致 Redis 命令中断,从而引发 accessToken / ticket 刷新链路整体失败的问题。

改动点:

  • AbstractWxCpInRedisConfigImpl 中,为 isAccessTokenExpired()isJsapiTicketExpired()isAgentJsapiTicketExpired() 增加异常捕获。
  • 当 Redis 获取 TTL 失败时,记录 warn 日志,并将其视为“已过期”以触发刷新流程,避免异常继续向上传播导致批处理崩溃。
  • 在上述异常场景下调用 Thread.interrupted() 清除中断标志,避免后续 Redis/锁获取继续因中断标志而失败。
  • RedisTemplateSimpleDistributedLock.lock() 中,捕获 InterruptedException 后由原先忽略改为恢复线程中断标志,避免中断信号被悄然吞掉。
  • 新增 RedisTemplateSimpleDistributedLockInterruptTest,覆盖 lock() 在 sleep 被中断时应保留中断标志的行为,以及 tryLock() 的基本分支。
  • 新增 AbstractWxCpInRedisConfigImplTest,验证 Redis 异常时将 token/ticket 视为过期并触发刷新,同时验证中断标志的清除行为。

技术说明:该修复更偏向“容错与自愈”策略:当 Redis TTL 读取不可用/被中断时,系统优先保证 token 刷新链路可继续执行,从而提升长时间运行任务的稳定性。

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore
Thread.currentThread().interrupt();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里在捕获 InterruptedException 后立即重新设置中断标志,会导致后续 Thread.sleep 反复“立刻抛出”,在锁长时间不可用时可能形成忙等/高 CPU 循环并频繁触发 tryLock() 访问 Redis。建议确认 lock() 在中断场景下的期望语义(继续等待但避免自旋 vs 直接放弃等待)。

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

} catch (Exception e) {
log.warn("获取access_token过期时间时发生异常,将视为已过期以触发刷新,异常信息: {}", e.getMessage());
// 清除中断标志,确保后续的锁获取和token刷新操作能够正常执行
Thread.interrupted();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里无条件调用 Thread.interrupted() 会清除调用方设置的中断信号(例如任务取消/应用关闭),即使异常并非由中断导致也会吞掉中断语义;建议至少仅在确认当前线程已被中断或异常链明确为“命令被中断”时再清除。

Severity: medium

Other Locations
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java:163
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java:200

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Long expire = redisOps.getExpire(this.accessTokenKey);
return expire == null || expire < 2;
} catch (Exception e) {
log.warn("获取access_token过期时间时发生异常,将视为已过期以触发刷新,异常信息: {}", e.getMessage());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log.warn 目前只记录了 e.getMessage(),会丢失堆栈与根因类型信息,后续排查 Redis 中断/连接问题可能不够。建议确认是否需要保留异常对象信息以便定位。

Severity: low

Other Locations
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java:162
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java:199

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 旨在提升 WxJava 在长时间批量运行场景下对 Redis “命令被中断”异常的容错能力,避免因线程中断标志残留导致 accessToken / ticket 刷新链路持续失败,从而引发整条调用链崩溃。

Changes:

  • AbstractWxCpInRedisConfigImpl 的 token/ticket 过期判断中增加异常捕获:Redis 异常时视为过期并触发刷新,同时清理线程中断标志
  • 修正 RedisTemplateSimpleDistributedLock.lock()InterruptedException 的处理,新增中断相关测试
  • 补充单测覆盖 Redis 异常场景下的 token/ticket 过期判断行为与线程中断标志处理

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java 为 accessToken/jsapiTicket/agentJsapiTicket 的过期检查增加异常容错与日志,并处理线程中断标志
weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImplTest.java 新增单测验证 Redis 异常时过期判断返回 true 且中断标志被清理
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java 调整 lock() 捕获 InterruptedException 时的中断处理行为
weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockInterruptTest.java 新增单测覆盖 lock() 在 sleep 被中断时的中断标志处理与 tryLock 基本行为

return expire == null || expire < 2;
} catch (Exception e) {
log.warn("获取agent_jsapi_ticket过期时间时发生异常,将视为已过期,异常信息: {}", e.getMessage());
Thread.interrupted();
Comment on lines +59 to +64

boolean expired = config.isAccessTokenExpired();

Assert.assertTrue(expired, "Redis异常时应将token视为已过期");
// 中断标志应该被清除,允许后续操作正常进行
Assert.assertFalse(Thread.currentThread().isInterrupted(), "处理异常后线程中断标志应被清除");
Comment on lines 44 to 51
public void lock() {
while (!tryLock()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore
Thread.currentThread().interrupt();
}
}
Comment on lines 124 to +133
public boolean isAccessTokenExpired() {
Long expire = redisOps.getExpire(this.accessTokenKey);
return expire == null || expire < 2;
try {
Long expire = redisOps.getExpire(this.accessTokenKey);
return expire == null || expire < 2;
} catch (Exception e) {
log.warn("获取access_token过期时间时发生异常,将视为已过期以触发刷新,异常信息: {}", e.getMessage());
// 清除中断标志,确保后续的锁获取和token刷新操作能够正常执行
Thread.interrupted();
return true;
}
Comment on lines 157 to +165
public boolean isJsapiTicketExpired() {
Long expire = redisOps.getExpire(this.jsapiTicketKey);
return expire == null || expire < 2;
try {
Long expire = redisOps.getExpire(this.jsapiTicketKey);
return expire == null || expire < 2;
} catch (Exception e) {
log.warn("获取jsapi_ticket过期时间时发生异常,将视为已过期,异常信息: {}", e.getMessage());
Thread.interrupted();
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

长时间删除二维码configId, 程序accessToken报错 无法有效兼容? 貌似对于accessToken的并发有点问题?

3 participants