最近一直在搞公司Ai云平臺的服務網關,項目涉及到了一個Oauth2的認證系統。實現認證系統的時候,用了Spring Security Oauth2框架,然后通過redis來實現用戶的token等信息的存儲。
在進行壓測的時候,發現系統經常會出現假死狀態,出現假死狀態的時候,所有請求都會被掛起不返回。發現這種情況時,我起初猜測是死鎖引起的,就用jconsole連到測試服務器來檢測死鎖,不過并沒有檢測到死鎖。
發現沒有死鎖,我就登上服務器,用jstack命令dump下當前線程的堆棧信息。拿到堆棧信息之后,我發現大量的線程都被阻塞在從JedisPool獲取Jedis資源上,具體堆棧信息貼在下面:
再結合JedisPool的源碼發現,線程都阻塞在從資源隊列中獲取資源這步。這是什么鬼?怎么會這樣?起初我懷疑是Jedis對象創建失敗了,所以資源隊列中沒有Jedis對象,于是我dump下了堆信息,使用VisualVM分析堆信息,但是我發現,堆中是有Jedis對象的,而且Jedis對象個數正好和JedisPool設置的最大對象個數一致。看來又猜錯了!!!難道是使用JedisPool的時候有地方忘記歸還資源了???我檢查了一遍代碼,使用jedispoll的時候清一色的try()語句:
try (Jedis jedis = jedisPool.getResource()){
...
}
我這就納悶了,這是怎么回事,資源也都釋放了,為什么會這樣?靜下心來再去看線程堆棧信息,我發現一個問題:
這兩個方法,在一個線程調用鏈中,方法實現如下:
@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;
}
細心的朋友肯定能發現,在獲取Jedis對象的時候有一個問題:重入了!!!沒錯,就是重入了。當并發高的時候,請求一起打過來,多個線程同時執行getAccessToken(OAuth2Authentication authentication)方法的時候,獲取了Jedis對象,然后進入readAccessToken(String tokenValue)方法,這個時候,Jedis對象都被外部的getAccessToken(OAuth2Authentication authentication)方法持有,所以就被阻塞了,而外部的getAccessToken(OAuth2Authentication authentication)等不到readAccessToken(String tokenValue)執行完成,所以永遠都不會釋放自己持有的Jedis對象!!!而這種情況是檢測不到死鎖的。竟然是因為這個原因!!!能發現這個錯誤也是運氣好呀。