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

SpringBoot實戰電商項目mall(30k+star)地址:github.com/macrozheng/…java

摘要

以前有不少朋友提過,mall項目中的權限管理功能有性能問題,由於每次訪問接口進行權限校驗時都會從數據庫中去查詢用戶信息。最近對這個問題進行了優化,經過Redis+AOP解決了該問題,下面來說下個人優化思路。git

前置知識

學習本文須要一些Spring Data Redis的知識,不瞭解的朋友能夠看下《Spring Data Redis 最佳實踐!》。 還須要一些Spring AOP的知識,不瞭解的朋友能夠看下《SpringBoot應用中使用AOP記錄接口訪問日誌》github

問題重現

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

/** * 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);
    }
}
複製代碼

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

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
複製代碼

使用Redis做爲緩存

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

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

/** * 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("用戶名或密碼錯誤");
    }
}
複製代碼

主要是獲取用戶信息和獲取用戶的資源信息這兩個操做,接下來咱們須要給這兩個操做添加緩存操做,這裏使用的是RedisTemple的操做方式。當查詢數據時,先去Redis緩存中查詢,若是Redis中沒有,再從數據庫查詢,查詢到之後在把數據存儲到Redis中去。app

/** * 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;
    }
}
複製代碼

上面這種查詢操做其實用Spring Cache來操做更簡單,直接使用@Cacheable便可實現,爲何還要使用RedisTemplate來直接操做呢?由於做爲緩存,咱們所但願的是,若是Redis宕機了,咱們的業務邏輯不會有影響,而使用Spring Cache來實現的話,當Redis宕機之後,用戶的登陸等種種操做就會都沒法進行了。ide

因爲咱們把用戶信息和用戶資源信息都緩存到了Redis中,因此當咱們修改用戶信息和資源信息時都須要刪除緩存中的數據,具體何時刪除,查看緩存業務類的註釋便可。post

/** * 後臺用戶緩存操做類 * 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);
}
複製代碼

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

使用AOP處理緩存操做異常

爲何要用AOP來解決這個問題呢?由於咱們的緩存業務類UmsAdminCacheService已經寫好了,要保證緩存業務類中的方法執行不影響正常的業務邏輯,就須要在全部方法中添加try catch邏輯。使用AOP,咱們能夠在一個地方寫上try catch邏輯,而後應用到全部方法上去。試想下,咱們若是又多了幾個緩存業務類,只要配置下切面便可,這波操做多方便!

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

/** * 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;
    }

}
複製代碼

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

不過並非全部的方法都須要處理異常的,好比咱們的驗證碼存儲,若是咱們的Redis宕機了,咱們的驗證碼存儲接口須要的是報錯,而不是返回執行成功。

對於上面這種需求咱們能夠經過自定義註解來完成,首先咱們自定義一個CacheException註解,若是方法上面有這個註解,發生異常則直接拋出。

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

以後須要改造下咱們的切面類,對於有@CacheException註解的方法,若是發生異常直接拋出。

/** * 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;
    }

}
複製代碼

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

/** * 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);
    }
}
複製代碼

總結

對於影響性能的,頻繁查詢數據庫的操做,咱們能夠經過Redis做爲緩存來優化。緩存操做不應影響正常業務邏輯,咱們可使用AOP來統一處理緩存操做中的異常。

項目源碼地址

github.com/macrozheng/…

公衆號

mall項目全套學習教程連載中,關注公衆號第一時間獲取。

公衆號圖片
相關文章
相關標籤/搜索