使用Redis+AOP優化權限管理功能,這波操作賊爽!

摘要

之前有很多朋友提過,mall項目中的權限管理功能有性能問題,因為每次訪問接口進行權限校驗時都會從數據庫中去查詢用戶信息。最近對這個問題進行了優化,通過Redis+AOP解決了該問題,下面來講下我的優化思路。

使用Redis+AOP優化權限管理功能,這波操作賊爽!


問題重現

在mall-security模塊中有一個過濾器,當用戶登錄後,請求會帶著token經過這個過濾器。這個過濾器會根據用戶攜帶的token進行類似免密登錄的操作,其中有一步會從數據庫中查詢登錄用戶信息,下面是這個過濾器類的代碼。

<code>/**
* JWT登錄授權過濾器
* Created by macro on 2018/4/26.
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//此處會從數據庫中獲取登錄用戶信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}/<code>

當我們登錄後訪問任意接口時,控制檯會打印如下日誌,表示會從數據庫中查詢用戶信息和用戶所擁有的資源信息,每次訪問接口都觸發這種操作,有的時候會帶來一定的性能問題。

<code>2020-03-17 16:13:02.623 DEBUG 4544 --- [nio-8081-exec-2] c.m.m.m.UmsAdminMapper.selectByExample   : ==>  Preparing: select id, username, password, icon, email, nick_name, note, create_time, login_time, status from ums_admin WHERE ( username = ? ) 
2020-03-17 16:13:02.624 DEBUG 4544 --- [nio-8081-exec-2] c.m.m.m.UmsAdminMapper.selectByExample : ==> Parameters: admin(String)
2020-03-17 16:13:02.625 DEBUG 4544 --- [nio-8081-exec-2] c.m.m.m.UmsAdminMapper.selectByExample : <== Total: 1
2020-03-17 16:13:02.628 DEBUG 4544 --- [nio-8081-exec-2] c.macro.mall.dao.UmsRoleDao.getMenuList : ==> Preparing: SELECT m.id id, m.parent_id parentId, m.create_time createTime, m.title title, m.level level, m.sort sort, m.name name, m.icon icon, m.hidden hidden FROM ums_admin_role_relation arr LEFT JOIN ums_role r ON arr.role_id = r.id LEFT JOIN ums_role_menu_relation rmr ON r.id = rmr.role_id LEFT JOIN ums_menu m ON rmr.menu_id = m.id WHERE arr.admin_id = ? AND m.id IS NOT NULL GROUP BY m.id
2020-03-17 16:13:02.628 DEBUG 4544 --- [nio-8081-exec-2] c.macro.mall.dao.UmsRoleDao.getMenuList : ==> Parameters: 3(Long)
2020-03-17 16:13:02.632 DEBUG 4544 --- [nio-8081-exec-2] c.macro.mall.dao.UmsRoleDao.getMenuList : <== Total: 24/<code>

使用Redis作為緩存

對於上面的問題,最容易想到的就是把用戶信息和用戶資源信息存入到Redis中去,避免頻繁查詢數據庫,本文的優化思路大體也是這樣的。

首先我們需要對Spring Security中獲取用戶信息的方法添加緩存,我們先來看下這個方法執行了哪些數據庫查詢操作。

<code>/**
* UmsAdminService實現類
* Created by macro on 2018/4/26.
*/
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
@Override

public UserDetails loadUserByUsername(String username){
//獲取用戶信息
UmsAdmin admin = getAdminByUsername(username);
if (admin != null) {
//獲取用戶的資源信息
List<umsresource> resourceList = getResourceList(admin.getId());
return new AdminUserDetails(admin,resourceList);
}
throw new UsernameNotFoundException("用戶名或密碼錯誤");
}
}/<umsresource>/<code>

主要是獲取用戶信息和獲取用戶的資源信息這兩個操作,接下來我們需要給這兩個操作添加緩存操作,這裡使用的是RedisTemple的操作方式。當查詢數據時,先去Redis緩存中查詢,如果Redis中沒有,再從數據庫查詢,查詢到以後在把數據存儲到Redis中去。

<code>/**
* UmsAdminService實現類
* Created by macro on 2018/4/26.
*/
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
//專門用來操作Redis緩存的業務類
@Autowired
private UmsAdminCacheService adminCacheService;
@Override
public UmsAdmin getAdminByUsername(String username) {
//先從緩存中獲取數據
UmsAdmin admin = adminCacheService.getAdmin(username);
if(admin!=null) return admin;
//緩存中沒有從數據庫中獲取
UmsAdminExample example = new UmsAdminExample();
example.createCriteria().andUsernameEqualTo(username);
List<umsadmin> adminList = adminMapper.selectByExample(example);
if (adminList != null && adminList.size() > 0) {
admin = adminList.get(0);

//將數據庫中的數據存入緩存中
adminCacheService.setAdmin(admin);
return admin;
}
return null;
}
@Override
public List<umsresource> getResourceList(Long adminId) {
//先從緩存中獲取數據
List<umsresource> resourceList = adminCacheService.getResourceList(adminId);
if(CollUtil.isNotEmpty(resourceList)){
return resourceList;
}
//緩存中沒有從數據庫中獲取
resourceList = adminRoleRelationDao.getResourceList(adminId);
if(CollUtil.isNotEmpty(resourceList)){
//將數據庫中的數據存入緩存中
adminCacheService.setResourceList(adminId,resourceList);
}
return resourceList;
}
}/<umsresource>/<umsresource>/<umsadmin>/<code>

上面這種查詢操作其實用Spring Cache來操作更簡單,直接使用@Cacheable即可實現,為什麼還要使用RedisTemplate來直接操作呢?因為作為緩存,我們所希望的是,如果Redis宕機了,我們的業務邏輯不會有影響,而使用Spring Cache來實現的話,當Redis宕機以後,用戶的登錄等種種操作就會都無法進行了。

由於我們把用戶信息和用戶資源信息都緩存到了Redis中,所以當我們修改用戶信息和資源信息時都需要刪除緩存中的數據,具體什麼時候刪除,查看緩存業務類的註釋即可。

<code>/**
* 後臺用戶緩存操作類
* Created by macro on 2020/3/13.
*/
public interface UmsAdminCacheService {
/**
* 刪除後臺用戶緩存
*/
void delAdmin(Long adminId);

/**
* 刪除後臺用戶資源列表緩存
*/
void delResourceList(Long adminId);

/**
* 當角色相關資源信息改變時刪除相關後臺用戶緩存
*/
void delResourceListByRole(Long roleId);

/**
* 當角色相關資源信息改變時刪除相關後臺用戶緩存
*/
void delResourceListByRoleIds(List<long> roleIds);

/**
* 當資源信息改變時,刪除資源項目後臺用戶緩存
*/
void delResourceListByResource(Long resourceId);
}/<long>/<code>

經過上面的一系列優化之後,性能問題解決了。但是引入新的技術之後,新的問題也會產生,比如說當Redis宕機以後,我們直接就無法登錄了,下面我們使用AOP來解決這個問題。

使用AOP處理緩存操作異常

為什麼要用AOP來解決這個問題呢?因為我們的緩存業務類UmsAdminCacheService已經寫好了,要保證緩存業務類中的方法執行不影響正常的業務邏輯,就需要在所有方法中添加try catch邏輯。使用AOP,我們可以在一個地方寫上try catch邏輯,然後應用到所有方法上去。試想下,我們如果又多了幾個緩存業務類,只要配置下切面即可,這波操作多方便!

首先我們先定義一個切面,在相關緩存業務類上面應用,在它的環繞通知中直接處理掉異常,保障後續操作能執行。

<code>/**
* Redis緩存切面,防止Redis宕機影響正常業務邏輯
* Created by macro on 2020/3/17.
*/
@Aspect
@Component
@Order(2)
public class RedisCacheAspect {
private static Logger LOGGER = LoggerFactory.getLogger(RedisCacheAspect.class);

@Pointcut("execution(public * com.macro.mall.portal.service.*CacheService.*(..)) || execution(public * com.macro.mall.service.*CacheService.*(..))")
public void cacheAspect() {
}

@Around("cacheAspect()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
LOGGER.error(throwable.getMessage());
}
return result;
}

}/<code>

這樣處理之後,就算我們的Redis宕機了,我們的業務邏輯也能正常執行。

不過並不是所有的方法都需要處理異常的,比如我們的驗證碼存儲,如果我們的Redis宕機了,我們的驗證碼存儲接口需要的是報錯,而不是返回執行成功。

對於上面這種需求我們可以通過自定義註解來完成,首先我們自定義一個CacheException註解,如果方法上面有這個註解,發生異常則直接拋出。

<code>/**
* 自定義註解,有該註解的緩存方法會拋出異常
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheException {
}/<code>

之後需要改造下我們的切面類,對於有@CacheException註解的方法,如果發生異常直接拋出。

<code>/**
* Redis緩存切面,防止Redis宕機影響正常業務邏輯
* Created by macro on 2020/3/17.
*/
@Aspect
@Component
@Order(2)
public class RedisCacheAspect {
private static Logger LOGGER = LoggerFactory.getLogger(RedisCacheAspect.class);


@Pointcut("execution(public * com.macro.mall.portal.service.*CacheService.*(..)) || execution(public * com.macro.mall.service.*CacheService.*(..))")
public void cacheAspect() {
}

@Around("cacheAspect()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
//有CacheException註解的方法需要拋出異常
if (method.isAnnotationPresent(CacheException.class)) {
throw throwable;
} else {
LOGGER.error(throwable.getMessage());
}
}
return result;
}

}/<code>

接下來我們需要把@CacheException註解應用到存儲和獲取驗證碼的方法上去,這裡需要注意的是要應用在實現類上而不是接口上,因為isAnnotationPresent方法只能獲取到當前方法上的註解,而不能獲取到它實現接口方法上的註解。

<code>/**
* UmsMemberCacheService實現類
* Created by macro on 2020/3/14.
*/
@Service
public class UmsMemberCacheServiceImpl implements UmsMemberCacheService {
@Autowired
private RedisService redisService;

@CacheException
@Override
public void setAuthCode(String telephone, String authCode) {

String key = REDIS_DATABASE + ":" + REDIS_KEY_AUTH_CODE + ":" + telephone;
redisService.set(key,authCode,REDIS_EXPIRE_AUTH_CODE);
}

@CacheException
@Override
public String getAuthCode(String telephone) {
String key = REDIS_DATABASE + ":" + REDIS_KEY_AUTH_CODE + ":" + telephone;
return (String) redisService.get(key);
}
}/<code>

總結

對於影響性能的,頻繁查詢數據庫的操作,我們可以通過Redis作為緩存來優化。緩存操作不該影響正常業務邏輯,我們可以使用AOP來統一處理緩存操作中的異常。


作者:MacroZheng
鏈接:https://juejin.im/post/5e78b96b6fb9a07cb83e4a10


分享到:


相關文章: