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中去,避免頻繁查詢數據庫,本文的優化思路大致也是這樣的。緩存
首先咱們須要對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來解決這個問題呢?由於咱們的緩存業務類
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來統一處理緩存操做中的異常。
mall項目全套學習教程連載中,關注公衆號第一時間獲取。