最近一直在搞公司Ai云平臺的服務(wù)網(wǎng)關(guān),項目涉及到了一個Oauth2的認證系統(tǒng)。實現(xiàn)認證系統(tǒng)的時候,用了Spring Security Oauth2框架,然后通過redis來實現(xiàn)用戶的token等信息的存儲。
在進行壓測的時候,發(fā)現(xiàn)系統(tǒng)經(jīng)常會出現(xiàn)假死狀態(tài),出現(xiàn)假死狀態(tài)的時候,所有請求都會被掛起不返回。發(fā)現(xiàn)這種情況時,我起初猜測是死鎖引起的,就用jconsole連到測試服務(wù)器來檢測死鎖,不過并沒有檢測到死鎖。
發(fā)現(xiàn)沒有死鎖,我就登上服務(wù)器,用jstack命令dump下當前線程的堆棧信息。拿到堆棧信息之后,我發(fā)現(xiàn)大量的線程都被阻塞在從JedisPool獲取Jedis資源上,具體堆棧信息貼在下面:

再結(jié)合JedisPool的源碼發(fā)現(xiàn),線程都阻塞在從資源隊列中獲取資源這步。這是什么鬼?怎么會這樣?起初我懷疑是Jedis對象創(chuàng)建失敗了,所以資源隊列中沒有Jedis對象,于是我dump下了堆信息,使用VisualVM分析堆信息,但是我發(fā)現(xiàn),堆中是有Jedis對象的,而且Jedis對象個數(shù)正好和JedisPool設(shè)置的最大對象個數(shù)一致??磥碛植洛e了?。?!難道是使用JedisPool的時候有地方忘記歸還資源了???我檢查了一遍代碼,使用jedispoll的時候清一色的try()語句:
try (Jedis jedis = jedisPool.getResource()){
...
}
我這就納悶了,這是怎么回事,資源也都釋放了,為什么會這樣?靜下心來再去看線程堆棧信息,我發(fā)現(xiàn)一個問題:

這兩個方法,在一個線程調(diào)用鏈中,方法實現(xiàn)如下:
@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String key = ACCESS_KEY + authentication.getOAuth2Request().getClientId();
try (Jedis jedis = redisSource.getConnect();) {
String accessToken = jedis.get(key);
if (accessToken != null) {
return readAccessToken(accessToken);
}
}
return null;
}
@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
String key = TOKEN_PREFIX + tokenValue;
try (Jedis jedis = redisSource.getConnect();) {
List<String> result = jedis.hmget(key, "access_token", "access_key_id", "access_key", "refresh_token", "user_id");
if (result != null && result.size() > 0 && result.get(0) != null) {
DefaultOAuth2AccessToken auth2AccessToken = new DefaultOAuth2AccessToken(tokenValue);
auth2AccessToken.setRefreshToken(new DefaultOAuth2RefreshToken(result.get(3)));
Map<String, Object> map = new HashMap<>();
map.put("accessKeyId", result.get(1));
map.put("userId", result.get(4));
long expire = jedis.ttl(key);
auth2AccessToken.setExpiration(new Date(System.currentTimeMillis() + expire * 1000));
auth2AccessToken.setAdditionalInformation(map);
if (logger.isDebugEnabled()) {
logger.debug("讀取到accessToken:" + MoreObjects.toStringHelper(auth2AccessToken).toString());
}
return auth2AccessToken;
}
}
return null;
}
細心的朋友肯定能發(fā)現(xiàn),在獲取Jedis對象的時候有一個問題:重入了!?。]錯,就是重入了。當并發(fā)高的時候,請求一起打過來,多個線程同時執(zhí)行g(shù)etAccessToken(OAuth2Authentication authentication)方法的時候,獲取了Jedis對象,然后進入readAccessToken(String tokenValue)方法,這個時候,Jedis對象都被外部的getAccessToken(OAuth2Authentication authentication)方法持有,所以就被阻塞了,而外部的getAccessToken(OAuth2Authentication authentication)等不到readAccessToken(String tokenValue)執(zhí)行完成,所以永遠都不會釋放自己持有的Jedis對象?。?!而這種情況是檢測不到死鎖的。竟然是因為這個原因?。。∧馨l(fā)現(xiàn)這個錯誤也是運氣好呀。