Используйте Redis+AOP для оптимизации функции управления правами, эта волна операций крутая!

Java задняя часть
Используйте Redis+AOP для оптимизации функции управления правами, эта волна операций крутая!

Адрес фактического центра электронной коммерции SpringBoot (30k+star):GitHub.com/macro-positive/…

Резюме

Многие друзья упоминали ранее, что функция управления разрешениями в проекте торгового центра имеет проблемы с производительностью, потому что каждый раз, когда интерфейс доступа проверяется на наличие разрешения, информация о пользователе будет запрашиваться из базы данных. Недавно эта проблема была оптимизирована, и проблема была решена с помощью Redis + AOP.Следующее - моя идея оптимизации.

Предварительное знание

Для изучения этой статьи требуются некоторые знания Spring Data Redis. Друзья, которые не знают, могут ее прочитать.Лучшие практики Spring Data Redis! 》. Вам также потребуются некоторые знания Spring AOP. Друзья, которые не знают, могут его посмотреть.«Использование AOP для записи журналов доступа к интерфейсу в приложениях SpringBoot».

Проблема воспроизводится

существуетmall-securityВ модуле есть фильтр, при входе пользователя запрос будет проходить через этот фильтр с токеном. Этот фильтр будет выполнять операции, аналогичные входу в систему без пароля, в соответствии с токеном, который несет пользователь. На одном этапе информация о пользователе для входа будет запрашиваться из базы данных. Ниже приведен код этого класса фильтра.

/**
 * 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.Давайте сначала посмотрим, какие операции запросов к базе данных выполняются этим методом.

/**
 * 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 после запроса.

/**
 * 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 выйдет из строя, вход пользователя и другие операции не смогут выполняться. .

Поскольку в Redis мы кэшируем как информацию о пользователе, так и информацию о пользовательских ресурсах, когда мы изменяем информацию о пользователе и информацию о ресурсах, нам необходимо удалить данные в кеше.Когда удалять, проверяйте комментарии бизнес-класса кеша.

/**
 * 后台用户缓存操作类
 * 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

Зачем использовать АОП для решения этой проблемы? Потому что наш кеш бизнес-классаUmsAdminCacheServiceНаписано.Чтобы выполнение методов в кеше бизнес-класса не влияло на обычную бизнес-логику, необходимо добавить во все методы.try catchлогика. Используя АОП, мы можем писать в одном месте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 в качестве кеша для оптимизации. Операции с кэшем не должны влиять на обычную бизнес-логику.Мы можем использовать АОП для унифицированной обработки исключений в операциях с кэшем.

Адрес исходного кода проекта

GitHub.com/macro-positive/…

публика

проект торгового центраПолный набор учебных пособий сериализуется,Обратите внимание на общедоступный номерПолучите это прямо сейчас.

公众号图片