Use Redis + AOP to optimize the permission management function, this wave of operation is cool!

Posted May 26, 20208 min read

SpringBoot combat e-commerce project mall(30k + star) address: https://github.com/macrozheng/mall

Summary

Many friends mentioned before that the permission management function in the mall project has performance problems, because each time the access interface is used for permission verification, the user information is queried from the database. Recently, this problem has been optimized, and the problem has been solved through Redis + AOP. Let me talk about my optimization ideas below.

Pre-knowledge

Learning this article requires some knowledge of Spring Data Redis. Friends who do not understand can read "Spring Data Redis Best Practices! .
You also need some knowledge of Spring AOP. Friends who do n t know can read [Accessing logs using AOP recording interface in Spring Boot application]

Reproduce the problem

There is a filter in the mall-security module. When the user logs in, the request will pass this filter with a token. This filter will perform a similar operation of secret-free login based on the token carried by the user. In one step, it will query the login user information from the database.

/**
* JWT login authorization filter
* 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 here
                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);
    }
}

When we access any interface after logging in, the console will print the following log, indicating that user information and resource information owned by the user will be queried from the database. This operation is triggered every time the interface is accessed, and sometimes brings Performance issues.

2020-03-17 16:13:02.623 DEBUG 4544 --- [nio-8081-exec-2]cmmmUmsAdminMapper.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

Use Redis as a cache

For the above problem, the easiest thing to think of is to store user information and user resource information in Redis to avoid frequent query of the database. The optimization ideas in this article are generally the same.

First, we need to add a cache to the method of obtaining user information in Spring Security. Let's first look at which database query operations are performed by this method.

/**
* UmsAdminService implementation class
* 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 user resource information
List resourceList = getResourceList(admin.getId());
return new AdminUserDetails(admin, resourceList);
}
throw new UsernameNotFoundException("Wrong username or password");
}
}

It is mainly two operations of obtaining user information and obtaining user resource information. Next, we need to add a cache operation to these two operations. The RedisTemple operation method is used here. When querying data, first go to the Redis cache to query, if there is no Redis, then query from the database, and then store the data in Redis after the query.

/**
* UmsAdminService implementation class
* Created by macro on 2018/4/26.
* /
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
//Business class dedicated to operating Redis cache
@Autowired
private UmsAdminCacheService adminCacheService;
@Override
public UmsAdmin getAdminByUsername(String username) {
//First get the data from the cache
UmsAdmin admin = adminCacheService.getAdmin(username);
if(admin! = null) return admin;
//Not retrieved from the database in the cache
UmsAdminExample example = new UmsAdminExample();
example.createCriteria(). andUsernameEqualTo(username);
List adminList = adminMapper.selectByExample(example);
if(adminList! = null && adminList.size()> 0) {
admin = adminList.get(0);
//Store the data in the database into the cache
adminCacheService.setAdmin(admin);
return admin;
}
return null;
}
@Override
public List getResourceList(Long adminId) {
//First get the data from the cache
List resourceList = adminCacheService.getResourceList(adminId);
if(CollUtil.isNotEmpty(resourceList)) {
return resourceList;
}
//Not retrieved from the database in the cache
resourceList = adminRoleRelationDao.getResourceList(adminId);
if(CollUtil.isNotEmpty(resourceList)) {
//Store the data in the database into the cache
adminCacheService.setResourceList(adminId, resourceList);
}
return resourceList;
}
}

The above query operation is easier to operate with Spring Cache, which can be achieved directly using @Cacheable. Why use RedisTemplate for direct operation? Because as a cache, what we hope is that if Redis is down, our business logic will not be affected, and if Spring Cache is used to implement, after Redis is down, various operations such as user login will not be possible Too.

Since we cache user information and user resource information in Redis, when we modify user information and resource information, we need to delete the data in the cache. When to delete it, check the comments of the cache business class.

/**
* Background user cache operation 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 the relevant background user cache when the role-related resource information changes
     * /
    void delResourceListByRole(Long roleId);

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

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

After a series of optimizations above, the performance problem was solved. However, after the introduction of new technologies, new problems will also arise. For example, when Redis goes down, we cannot log in directly. Next, we use AOP to solve this problem.

Using AOP to handle cache operation exceptions

Why use AOP to solve this problem? Because our cache business class UmsAdminCacheService has been written, to ensure that the execution of methods in the cache business class does not affect normal business logic, we need to add try catch logic to all methods. Using AOP, we can write try catch logic in one place, and then apply it to all methods. Imagine if we have a few more cache business classes, just configure the aspect, how convenient is this wave of operation!

First of all, we first define a aspect, apply it on the relevant cache business class, and directly handle the exception in its surround notification to ensure that subsequent operations can be performed.

/**
* Redis cache aspect, to prevent Redis downtime from affecting normal business logic
* 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;
    }

}

After this processing, even if our Redis is down, our business logic can be executed normally.

However, not all methods need to handle exceptions, such as our verification code storage. If our Redis goes down, our verification code storage interface needs to report an error instead of returning successful execution.

For the above requirements, we can complete it through custom annotations. First, we customize a CacheException annotation. If there is this annotation on the method, an exception will be thrown directly.

/**
* Custom annotation, the cache method with this annotation will throw an exception
* /
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheException {
}

After that, we need to transform our aspect class. For methods annotated with @CacheException, if an exception occurs, it is directly thrown.

/**
* Redis cache aspect, to prevent Redis downtime from affecting normal business logic
* 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) {
            //Methods annotated with CacheException need to throw an exception
            if(method.isAnnotationPresent(CacheException.class)) {
                throw throwable;
            } else {
                LOGGER.error(throwable.getMessage());
            }
        }
        return result;
    }

}

Next, we need to apply the @ CacheException annotation to the method of storing and obtaining the verification code. The thing to note here is that it should be applied to the implementation class instead of the interface, because the isAnnotationPresent method can only get the current method. Annotation, but can not get the annotation on the interface method it implements.

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

to sum up

For operations that affect performance and frequently query the database, we can use Redis as a cache to optimize. Cache operations should not affect normal business logic. We can use AOP to handle exceptions in cache operations uniformly.

Project source address

https://github.com/macrozheng/mall

No public

mall project In the serialization of the full set of learning tutorials, follow the public number to get it for the first time.

Public Account Picture