SpringBoot actual e-business project mall (30K + STAR) address: github.com/macrozheng/…

Abstract

As many friends mentioned before, the permission management function in mall project has performance problems, because every time we access the interface for permission verification, the user information will be queried from the database. Recently this problem has been optimized, through Redis+AOP to solve the problem, the following is my optimization ideas.

Front knowledge

To learn this article, you need some knowledge of Spring Data Redis. If you don’t know Spring Data Redis, you can take a look at Spring Data Redis Best Practices! . Also need some knowledge of Spring AOP, do not know friends can see the Use of AOP in SpringBoot application interface access logging.

Problem reproduction

There is a filter in the mall Security module, and when the user logs in, the request goes through this filter with the token. This filter performs a similar operation to a cryptographic-free login based on the token carried by the user, including a step to query the logged-in user information from the database. Here is the code of this filter class.

/** * 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) {
                // The login user information will be obtained from the database
                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); }}Copy the code

When we log in to any interface, the console will print the following log, indicating that it will query the user information and the resource information of the user from the database. This operation is triggered every time we access the interface, sometimes causing performance problems.

The 2020-03-17 16:13:02. 4544-623 the DEBUG [nio - 8081 - exec - 2] C.M.M.M.U msAdminMapper. SelectByExample: = = > Preparing: select id, username, password, icon, email, nick_name, note, create_time, login_time, status from ums_admin WHERE ( username = ? ) The 2020-03-17 16:13:02. 4544-624 the DEBUG [nio - 8081 - exec - 2] C.M.M.M.U msAdminMapper. SelectByExample: = = > the Parameters: Admin (String) the 2020-03-17 16:13:02. 4544-625 the DEBUG [nio - 8081 - exec - 2] C.M.M.M.U msAdminMapper. SelectByExample: < = = Total: 1 2020-03-17 16:13:02. 4544-628 the DEBUG [nio - 8081 - exec - 2] c. acro. 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) the 2020-03-17 16:13:02. 4544-632 the DEBUG [nio - 8081 - exec - 2] c. acro. Mall. Dao. UmsRoleDao. GetMenuList: < = = Total: 24Copy the code

Use Redis as the cache

For the above problems, the most easy to think of is to store user information and user resource information in Redis to avoid frequent query database, the optimization idea of this paper is generally the same.

First we need to cache the Spring Security method that gets user information. Let’s take a look at what database queries this method performs.

/** * UmsAdminService implementation * Created by macro on 2018/4/26. */
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
    @Override
    public UserDetails loadUserByUsername(String username){
        // Get user information
        UmsAdmin admin = getAdminByUsername(username);
        if(admin ! =null) {
            // Get the user's resource information
            List<UmsResource> resourceList = getResourceList(admin.getId());
            return new AdminUserDetails(admin,resourceList);
        }
        throw new UsernameNotFoundException("Wrong username or password"); }}Copy the code

The main two operations are to get user information and to get user resource information. Next we need to add cache operations to these two operations, using the RedisTemple operation. When querying data, first go to Redis cache to query, if not in Redis, then query from the database, after querying, the data will be stored in Redis.

/** * UmsAdminService implementation * Created by macro on 2018/4/26. */
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
    // The business class used to manipulate the Redis cache
    @Autowired
    private UmsAdminCacheService adminCacheService;
    @Override
    public UmsAdmin getAdminByUsername(String username) {
        // Get the data from the cache
        UmsAdmin admin = adminCacheService.getAdmin(username);
        if(admin! =null) return  admin;
        // The cache is not fetched from the database
        UmsAdminExample example = new UmsAdminExample();
        example.createCriteria().andUsernameEqualTo(username);
        List<UmsAdmin> adminList = adminMapper.selectByExample(example);
        if(adminList ! =null && adminList.size() > 0) {
            admin = adminList.get(0);
            // Store the data in the database in the cache
            adminCacheService.setAdmin(admin);
            return admin;
        }
        return null;
    }
    @Override
    public List<UmsResource> getResourceList(Long adminId) {
        // Get the data from the cache
        List<UmsResource> resourceList = adminCacheService.getResourceList(adminId);
        if(CollUtil.isNotEmpty(resourceList)){
            return  resourceList;
        }
        // The cache is not fetched from the database
        resourceList = adminRoleRelationDao.getResourceList(adminId);
        if(CollUtil.isNotEmpty(resourceList)){
            // Store the data in the database in the cache
            adminCacheService.setResourceList(adminId,resourceList);
        }
        returnresourceList; }}Copy the code

Why RedisTemplate? Spring Cache is much easier to use than @cacheable. As a Cache, we hope that if Redis goes down, our business logic will not be affected. With Spring Cache, when Redis goes down, users will not be able to log in and so on.

Since we cache user information and user resource information in Redis, we need to delete the data in the cache when we modify user information and resource information. When to delete the data, please refer to the notes of the cache business class.

/** * Created by macro on 2020/3/13. */
public interface UmsAdminCacheService {
    /** * Delete background user cache */
    void delAdmin(Long adminId);

    /** * Delete background user resource list cache */
    void delResourceList(Long adminId);

    /** * Delete background user cache */ when role resource information changes
    void delResourceListByRole(Long roleId);

    /** * Delete background user cache */ when role resource information changes
    void delResourceListByRoleIds(List<Long> roleIds);

    /** * When the resource information changes, delete the resource item background user cache */
    void delResourceListByResource(Long resourceId);
}
Copy the code

After the above series of optimizations, the performance problem was resolved. However, with the introduction of new technologies, new problems will also arise. For example, when Redis is down, we will not be able to log in directly. Let’s use AOP to solve this problem.

Use AOP to handle cache operation exceptions

Why use AOP to solve this problem? Because our cache business class UmsAdminCacheService is already written, to ensure that the method execution in the cache business class does not affect the normal business logic, we need to add try catch logic to all the methods. With AOP, we can write try catch logic in one place and apply it to all methods. Just imagine, if we have several more cache business classes, as long as the configuration of the section, this wave of operation is more convenient!

First of all, we define an aspect, which is applied to the relevant caching business class, to handle the exception directly in its circular notification, and ensure that subsequent operations can be performed.

/** * 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(a) {}@Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            LOGGER.error(throwable.getMessage());
        }
        returnresult; }}Copy the code

After this process, even if our Redis goes down, our business logic can still execute normally.

However, not all methods need to handle exceptions, such as our verification code store, if our Redis is down, our verification code store interface needs to report an error, rather than return success.

We can do this with custom annotations, starting with a custom CacheException annotation that will be thrown if the method has one.

/** * custom annotation. Caching methods with this annotation will throw an exception */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheException {
}
Copy the code

Then we need to modify our aspect class so that methods annotated @cacheException are thrown if an exception occurs.

/** * 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(a) {}@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) {
            // Methods annotated with CacheException need to throw exceptions
            if (method.isAnnotationPresent(CacheException.class)) {
                throw throwable;
            } else{ LOGGER.error(throwable.getMessage()); }}returnresult; }}Copy the code

Next, we need to apply the @CacheException annotation to the methods that store and get captans, making sure that it is applied to the implementation class and not the interface, because the isAnnotationPresent method only gets annotations from the current method, not the method that implements the interface.

/** * UmsMemberCacheService implementation class * 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); }}Copy the code

conclusion

For frequent database queries that affect performance, Redis can be used as a cache to optimize. Caching operations should not interfere with normal business logic, and we can use AOP to uniformly handle exceptions in caching operations.

Project source code address

Github.com/macrozheng/…

The public,

Mall project full set of learning tutorials serialized, attention to the public number the first time access.