zoukankan      html  css  js  c++  java
  • Spring Cache 下对分页请求的正确缓存方式

    前言

      在 spring boot 应用程式开发的时候,在对 service 层加入缓存支持的过程中,遇到了处理分页缓存的难题,在摸索了多个解决方式后,找到了比较适合,特此记录

    问题描述

      在程序中存在 User与 Note 实体。假设用户此时需要从服务器获得 Note 数据,在大部分情况下,用户不需要一次性获取所有的 Note 数据,我们会使用 Page 来减少带宽压力的,同时使用缓存来减少对数据库的访问。

      在缓存流程中,在对数据进行查找时候,需要先查找缓存中是否存在相应 key 的数据,如果没有,则触发一次数据库访问并把查询到的结果存入缓存中,这在 Spring Boot 中以注解的方式很容易实现,我们可以这样子使用: 

     1     @Transactional(readOnly = true)
     2     @Cacheable(
     3             key = "'user_'+#user.id+':'+#pageable.pageSize+'_'+#pageable.pageNumber",
     4             condition = "#user!=null && #pageable!=null",
     5             unless = "#result==null",
     6             cacheNames = "note"
     7            )
     8     @Nonnull
     9     @Override
    10     public Page<Note> findAllByUserAndPageable(@Nonnull User user, @Nonnull Pageable pageable) throws IllegalArgumentException {

      但是当用户对其中一个 Note 进行更改时,缓存的更新就成了一大难题,试想有一个方法如下:

    1     @Nonnull
    2     @Override
    3     public Note addOne(@Nonnull Note toAdd) throws IllegalArgumentException {...

      我们新增了一个 Note,现在程序需要清除与之有关联的缓存数据,比如 分页,Spring Cache 提供了 @CacheEvict ,现在我们使用它来清除缓存,很自然的,对应产生缓存的方式,我们在 @CacheEvict 的数据中加入 key = "'user_'+#user.id+':'#pageable.pageSize+'_'+#pageable.pageNumber"。很不幸,这是个错误的使用方式,在这里,我们获取不到 pageable 对象。

      @CacheEvict 里边有个 allEntries 选项,把它设置为 true 清空 当前 cacheNames 下的所有缓存如何?同样的,这不是一个好的处理方式。如果这么做了,当前缓存中的所有 缓存,包括那些没有被修改的数据,比如单个的 Note 缓存同样被删的一干二净。但是,其中的涉及小细节给了我们一些 hint。

    解决思路

      在上面的问题中,我们发现使用 allEntries 并不适用,它会导致缓存 cacheNames 下的所有数据被清空,请注意是指定的 cacheNames 下的所有数据,那么我们是否可以将分页数据单独放入对应 User 的 cacheName 下,当发生缓存更改时,直接删除只包含分页缓存的 cacheName 下的缓存? 比如 cacheNames="#principle.username" ?

      如果通读过 Spring-doc 的 Cache 章节,会发现,cacheNames 属性并不支持 SpEL 表达式,所以以上的设置并不行得通。似乎山穷水尽,其实不然。仔细观察 @Cache... 的通用属性,会发现 cacheResolver 的注释是这样的。

    The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use.

      可以使用 bean ,至少有一丝曙光,点进去 CacheResolver,他只规定了一个方法 resolveCaches,查看它的 Hierarchy 视图,所幸类试图并不复杂,

      让我们查看 CacheResolverAdapter 的源码与注释。

    Spring's {@link CacheResolver} implementation that delegates to a standard
    JSR-107 {@link javax.cache.annotation.CacheResolver}.
    Used internally to invoke user-based JSR-107 cache resolvers.

      使用JCacheCache 的 JCache (JSR-107)实现, pass

      查看 AbstractCacheResolver 的源码与注释。

    A base {@link CacheResolver} implementation that requires the concrete
    implementation to provide the collection of cache name(s) based on the
    invocation context.

       很幸运,我们很快发现了候选。但是我们需要参考 spring 的默认实现 SimpleCacheResolver。

    A simple {@link CacheResolver} that resolves the {@link Cache} instance(s)
    based on a configurable {@link CacheManager} and the name of the
    cache(s) as provided by {@link BasicOperation#getCacheNames() getCacheNames()}.

    解决进行

      让我们针对 cache names 细节做一些具体实现:继承这个类,更改 getCacheNames 方法,然后我们就得到了一个具体的实现

     1     /**
     2      * dynamic generates cache name: findAllByUserAndPageable_${userId}
     3      * <br>
     4      * use for page&lt;Note&gt; with annotation{@link org.springframework.cache.annotation.CacheEvict}, {@link org.springframework.cache.annotation.Cacheable}
     5      */
     6     public static class DynamicNotePageCacheNames extends SimpleCacheResolver implements CacheResolver {
     7         private final AuthenticationHelper authenticationHelper;
     8         public static final String FIND_ALL_BY_USER_AND_PAGEABLE = "findAllByUserAndPageable";
     9 
    10         public DynamicNotePageCacheNames(CacheManager cacheManager, AuthenticationHelper authenticationHelper) {
    11             super(cacheManager);
    12             log.debug(String.format("using customize CacheResolver: %s , cacheManager: %s",
    13                     this.getClass().getName(), cacheManager.getClass().getName()));
    14             this.authenticationHelper = authenticationHelper;
    15         }
    16 
    17         @Override
    18         protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
    19 
    20             Long uId = authenticationHelper.checkAndExtractUserFromAuthentication().getId();
    21             String cacheName = String.format("%s_%d", FIND_ALL_BY_USER_AND_PAGEABLE, uId);
    22             log.debug(String.format("generate cache name %s for target %s", cacheName, context.getTarget()));
    23             return Collections.singleton((String.format("%s_%d", FIND_ALL_BY_USER_AND_PAGEABLE, uId)));
    24         }
    25     }

      这里我从 Security 中获取到当前的登录用户的 id,将 id 与 方法字符串拼接起来作为 cachename 返回。需要注意的是 在方法体中我并没有对 uId 进行空检查是因为在调用 checkAndExtractUserFromAuthentication 方法 尝试获取 user 的时候,如果当前 Authentication 中只存在匿名用户的时候,方法会抛出  AccessDeniedException 错误,直接导致了 403 返回,具体可参考章节附。

      让我们使用这个bean,实例化并且改写 service 层

        @Bean("dynamicNotePageCacheNames")
        public DynamicNotePageCacheNames cacheResolver() {
            return new DynamicNotePageCacheNames(cacheManager, authenticationHelper);
        }

      @Cacheable的使用:

     1     @Transactional(readOnly = true)
     2     @Cacheable(
     3             key = "#pageable.pageSize+'_'+#pageable.pageNumber",
     4             condition = "#user!=null && #pageable!=null",
     5             unless = "#result==null",
     6            cacheResolver = "dynamicNotePageCacheNames"
     7     )
     8     @Nonnull
     9     @Override
    10     public Page<Note> findAllByUserAndPageable(@Nonnull User user, @Nonnull Pageable pageable) throws IllegalArgumentException {

      @CacheEvict的使用:

        @Transactional(rollbackFor = EntityExistsException.class)
        @Caching(
                evict = {
                        @CacheEvict(
                                condition = "#toAdd!=null ",
                                cacheResolver = "dynamicNotePageCacheNames", allEntries = true),
                  
                },
                put = @CachePut(key = "'id_'+#result.id",
                        condition = "#toAdd!=null",
                        unless = "#result==null", cacheNames = CustomCacheConfig.NOTE)
        )
        @Nonnull
        @Override
        public Note addOne(@Nonnull Note toAdd) throws IllegalArgumentException {

      需要注意的是,类上的注解 @CacheConfig 会覆盖方法中的设置,参考

    AuthenticationHelper:

     1 /**
     2  *
     3  *
     4  * @author pancc
     5  * @version 1.0
     6  */
     7 @Component
     8 public class AuthenticationHelper {
     9     private final UserService userService;
    10 
    11     public AuthenticationHelper(UserService userService) {
    12         this.userService = userService;
    13     }
    14 
    15     /**
    16      * extract User From Authentication at current request, at this case it refers to a {@link UsernamePasswordAuthenticationToken}<br>
    17      * <br>
    18      * <code>anonymousUser</code> filter out anonymous user set by spring security web
    19      *
    20      * @param authentication if null, will get from current request
    21      * @return optional with current user  or empty
    22      * @see JWTAuthenticationFilter
    23      * @see TokenAuthenticationProcessor#getAuthentication(String)
    24      * @see AnonymousAuthenticationFilter#AnonymousAuthenticationFilter(java.lang.String)
    25      */
    26     private Optional<User> extractUserOptionalFromAuthentication(Authentication authentication) {
    27         final String anonymous = "anonymousUser";
    28         if (authentication == null) {
    29             authentication = SecurityContextHolder.getContext().getAuthentication();
    30         }
    31         if (authentication == null || !authentication.isAuthenticated()) {
    32             return Optional.empty();
    33         }
    34         if ((authentication.getPrincipal().getClass().equals(String.class))) {
    35             String username = (String) authentication.getPrincipal();
    36             if (username == null || username.contentEquals(anonymous)) {
    37                 return Optional.empty();
    38             }
    39             return userService.findByUsername(username);
    40         }
    41         return Optional.empty();
    42     }
    43 
    44     /**
    45      * extract User From Authentication at current request, at this case it refers to a {@link UsernamePasswordAuthenticationToken}<br>
    46      *
    47      * @return optional with current user  or empty
    48      * @see JWTAuthenticationFilter
    49      * @see TokenAuthenticationProcessor#getAuthentication(String)
    50      */
    51     public Optional<User> extractUserOptionalFromAuthentication() {
    52         return this.extractUserOptionalFromAuthentication(null);
    53     }
    54 
    55     /**
    56      * extract User From Authentication, at this case it points to a {@link UsernamePasswordAuthenticationToken}<br>
    57      * this method may throw {@link AccessDeniedException} if not caught, resulting in server response 403 code
    58      * <br>
    59      * <b>required login</b>
    60      *
    61      * @param authentication {@link UsernamePasswordAuthenticationToken}
    62      * @return current user or throw AccessDeniedException that means 403 Http-code
    63      * @throws AccessDeniedException if current Authentication contains bad Credentials
    64      * @see ExceptionResolver.AccessDeniedAdvice
    65      * @see JWTAuthenticationFilter
    66      * @see TokenAuthenticationProcessor#getAuthentication(String)
    67      */
    68     public User checkAndExtractUserFromAuthentication(Authentication authentication) throws AccessDeniedException {
    69         return this.extractUserOptionalFromAuthentication(authentication).orElseThrow(() -> new AccessDeniedException("Access Denied"));
    70     }
    71 
    72     /**
    73      * extract User From Authentication at current request, at this case it refers to a {@link UsernamePasswordAuthenticationToken}<br>
    74      * this method may throw {@link AccessDeniedException} if not caught, resulting in server response 403 code
    75      * <br>
    76      * <b>required login</b>
    77      *
    78      * @return current user or throw AccessDeniedException that means 403 Http-code
    79      * @throws AccessDeniedException if current Authentication contains bad Credentials
    80      * @see ExceptionResolver.AccessDeniedAdvice
    81      * @see JWTAuthenticationFilter
    82      * @see TokenAuthenticationProcessor#getAuthentication(String)
    83      */
    84     public User checkAndExtractUserFromAuthentication() throws AccessDeniedException {
    85         return this.extractUserOptionalFromAuthentication(null).orElseThrow(() -> new AccessDeniedException("Access Denied"));
    86     }
    87 
    88 }
  • 相关阅读:
    频繁FGC解决方案
    ThreadLocal
    Session与Cookie
    Socket通信流程
    SpringBoot面试题
    面向对象3大特性:封装、继承、多态——继承(继承方法的重写和初始化顺序、final & super关键字、Object类)
    面向对象3大特性:封装、继承、多态——封装(this 、访问修饰符、内部类)
    java类和对象、构造方法、静态变量、静态方法、静态初始化块
    数组的使用、eclipse调试程序、练习小demo以及方法的定义和重载
    java中的条件语句if...else... switch 和循环语句while do...while for
  • 原文地址:https://www.cnblogs.com/siweipancc/p/spring-cache-pageable-data-in-proper-way.html
Copyright © 2011-2022 走看看