• 强大的Spring缓存技术(中)


    如何清空缓存


    好,到目前为止,我们的 spring cache 缓存程序已经运行成功了,但是还不完美,因为还缺少一个重要的缓存管理逻辑:清空缓存.


    当账号数据发生变更,那么必须要清空某个缓存,另外还需要定期的清空所有缓存,以保证缓存数据的可靠性。


    为了加入清空缓存的逻辑,我们只要对 AccountService2.java 进行修改,从业务逻辑的角度上看,它有两个需要清空缓存的地方


    • 当外部调用更新了账号,则我们需要更新此账号对应的缓存


    • 当外部调用说明重新加载,则我们需要清空所有缓存


    我们在AccountService2的基础上进行修改,修改为AccountService3,代码如下:


    import com.google.common.base.Optional;

    import com.rollenholt.spring.cache.example1.Account;

    import org.slf4j.Logger;

    import org.slf4j.LoggerFactory;

    import org.springframework.cache.annotation.CacheEvict;

    import org.springframework.cache.annotation.Cacheable;

    import org.springframework.stereotype.Service;

     

    /**

     * @author wenchao.ren

     *         2015/1/5.

     */

    @Service

    public class AccountService3 {

     

        private final Logger logger = LoggerFactory.getLogger(AccountService3.class);

     

        // 使用了一个缓存名叫 accountCache

        @Cacheable(value="accountCache")

        public Account getAccountByName(String accountName) {

     

            // 方法内部实现不考虑缓存逻辑,直接实现业务

            logger.info("real querying account... {}", accountName);

            Optional<Account> accountOptional = getFromDB(accountName);

            if (!accountOptional.isPresent()) {

                throw new IllegalStateException(String.format("can not find account by account name : [%s]", accountName));

            }

     

            return accountOptional.get();

        }

     

        @CacheEvict(value="accountCache",key="#account.getName()")

        public void updateAccount(Account account) {

            updateDB(account);

        }

     

        @CacheEvict(value="accountCache",allEntries=true)

        public void reload() {

        }

     

        private void updateDB(Account account) {

            logger.info("real update db...{}", account.getName());

        }

     

        private Optional<Account> getFromDB(String accountName) {

            logger.info("real querying db... {}", accountName);

            //Todo query data from database

            return Optional.fromNullable(new Account(accountName));

        }

    }


    我们的测试代码如下:


    import com.rollenholt.spring.cache.example1.Account;

    import org.junit.Before;

    import org.junit.Test;

    import org.slf4j.Logger;

    import org.slf4j.LoggerFactory;

    import org.springframework.context.support.ClassPathXmlApplicationContext;

     

    public class AccountService3Test {

     

        private AccountService3 accountService3;

     

        private final Logger logger = LoggerFactory.getLogger(AccountService3Test.class);

     

        @Before

        public void setUp() throws Exception {

            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext2.xml");

            accountService3 = context.getBean("accountService3", AccountService3.class);

        }

     

        @Test

        public void testGetAccountByName() throws Exception {

     

            logger.info("first query.....");

            accountService3.getAccountByName("accountName");

     

            logger.info("second query....");

            accountService3.getAccountByName("accountName");

     

        }

     

        @Test

        public void testUpdateAccount() throws Exception {

            Account account1 = accountService3.getAccountByName("accountName1");

            logger.info(account1.toString());

            Account account2 = accountService3.getAccountByName("accountName2");

            logger.info(account2.toString());

     

            account2.setId(121212);

            accountService3.updateAccount(account2);

     

            // account1会走缓存

            account1 = accountService3.getAccountByName("accountName1");

            logger.info(account1.toString());

            // account2会查询db

            account2 = accountService3.getAccountByName("accountName2");

            logger.info(account2.toString());

     

        }

     

        @Test

        public void testReload() throws Exception {

            accountService3.reload();

            // 这2行查询数据库

            accountService3.getAccountByName("somebody1");

            accountService3.getAccountByName("somebody2");

     

            // 这两行走缓存

            accountService3.getAccountByName("somebody1");

            accountService3.getAccountByName("somebody2");

        }

    }


    在这个测试代码中我们重点关注testUpdateAccount()方法,在测试代码中我们已经注释了在update完account2以后,再次查询的时候,account1会走缓存,而account2不会走缓存,而去查询db,观察程序运行日志,运行日志为:


    01:37:34.549 [main] INFO  c.r.s.cache.example3.AccountService3 - real querying account... accountName1

    01:37:34.551 [main] INFO  c.r.s.cache.example3.AccountService3 - real querying db... accountName1

    01:37:34.552 [main] INFO  c.r.s.c.example3.AccountService3Test - Account{id=0, name='accountName1'}

    01:37:34.553 [main] INFO  c.r.s.cache.example3.AccountService3 - real querying account... accountName2

    01:37:34.553 [main] INFO  c.r.s.cache.example3.AccountService3 - real querying db... accountName2

    01:37:34.555 [main] INFO  c.r.s.c.example3.AccountService3Test - Account{id=0, name='accountName2'}

    01:37:34.555 [main] INFO  c.r.s.cache.example3.AccountService3 - real update db...accountName2

    01:37:34.595 [main] INFO  c.r.s.c.example3.AccountService3Test - Account{id=0, name='accountName1'}

    01:37:34.596 [main] INFO  c.r.s.cache.example3.AccountService3 - real querying account... accountName2

    01:37:34.596 [main] INFO  c.r.s.cache.example3.AccountService3 - real querying db... accountName2

    01:37:34.596 [main] INFO  c.r.s.c.example3.AccountService3Test - Account{id=0, name='accountName2'}


    我们会发现实际运行情况和我们预估的结果是一致的。


    如何按照条件操作缓存


    前面介绍的缓存方法,没有任何条件,即所有对 accountService 对象的 getAccountByName 方法的调用都会起动缓存效果,不管参数是什么值。


    如果有一个需求,就是只有账号名称的长度小于等于 4 的情况下,才做缓存,大于 4 的不使用缓存


    虽然这个需求比较坑爹,但是抛开需求的合理性,我们怎么实现这个功能呢?


    通过查看CacheEvict注解的定义,我们会发现:


    /**

     * Annotation indicating that a method (or all methods on a class) trigger(s)

     * a cache invalidate operation.

     *

     * @author Costin Leau

     * @author Stephane Nicoll

     * @since 3.1

     * @see CacheConfig

     */

    @Target({ElementType.METHOD, ElementType.TYPE})

    @Retention(RetentionPolicy.RUNTIME)

    @Inherited

    @Documented

    public @interface CacheEvict {

     

        /**

         * Qualifier value for the specified cached operation.

         * <p>May be used to determine the target cache (or caches), matching the qualifier

         * value (or the bean name(s)) of (a) specific bean definition.

         */

        String[] value() default {};

     

        /**

         * Spring Expression Language (SpEL) attribute for computing the key dynamically.

         * <p>Default is "", meaning all method parameters are considered as a key, unless

         * a custom {@link #keyGenerator()} has been set.

         */

        String key() default "";

     

        /**

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

         * <p>Mutually exclusive with the {@link #key()} attribute.

         */

        String keyGenerator() default "";

     

        /**

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

         * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none

         * is set already.

         * <p>Mutually exclusive with the {@link #cacheResolver()}  attribute.

         * @see org.springframework.cache.interceptor.SimpleCacheResolver

         */

        String cacheManager() default "";

     

        /**

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

         */

        String cacheResolver() default "";

     

        /**

         * Spring Expression Language (SpEL) attribute used for conditioning the method caching.

         * <p>Default is "", meaning the method is always cached.

         */

        String condition() default "";

     

        /**

         * Whether or not all the entries inside the cache(s) are removed or not. By

         * default, only the value under the associated key is removed.

         * <p>Note that setting this parameter to {@code true} and specifying a {@link #key()}

         * is not allowed.

         */

        boolean allEntries() default false;

     

        /**

         * Whether the eviction should occur after the method is successfully invoked (default)

         * or before. The latter causes the eviction to occur irrespective of the method outcome (whether

         * it threw an exception or not) while the former does not.

         */

        boolean beforeInvocation() default false;

    }


    定义中有一个condition描述:


    Spring Expression Language (SpEL) attribute used for conditioning the method caching.Default is “”, meaning the method is always cached.


    我们可以利用这个方法来完成这个功能,下面只给出示例代码:


    @Cacheable(value="accountCache",condition="#accountName.length() <= 4")// 缓存名叫 accountCache 

    public Account getAccountByName(String accountName) {

        // 方法内部实现不考虑缓存逻辑,直接实现业务

        return getFromDB(accountName);

    }


    注意其中的 condition=”#accountName.length() <=4”,这里使用了 SpEL 表达式访问了参数 accountName 对象的 length() 方法,条件表达式返回一个布尔值,true/false,当条件为 true,则进行缓存操作,否则直接调用方法执行的返回结果。


    如果有多个参数,如何进行 key 的组合


    我们看看CacheEvict注解的key()方法的描述:


    Spring Expression Language (SpEL) attribute for computing the key dynamically. Default is “”, meaning all method parameters are considered as a key, unless a custom {@link #keyGenerator()} has been set.


    假设我们希望根据对象相关属性的组合来进行缓存,比如有这么一个场景:


    要求根据账号名、密码和是否发送日志查询账号信息


    很明显,这里我们需要根据账号名、密码对账号对象进行缓存,而第三个参数“是否发送日志”对缓存没有任何影响。所以,我们可以利用 SpEL 表达式对缓存 key 进行设计


    我们为Account类增加一个password 属性, 然后修改AccountService代码:


    @Cacheable(value="accountCache",key="#accountName.concat(#password)") 

    public Account getAccount(String accountName,String password,boolean sendLog) { 

      // 方法内部实现不考虑缓存逻辑,直接实现业务

      return getFromDB(accountName,password); 

    }


    注意上面的 key 属性,其中引用了方法的两个参数 accountName 和 password,而 sendLog 属性没有考虑,因为其对缓存没有影响。


    accountService.getAccount("accountName", "123456", true);// 查询数据库

    accountService.getAccount("accountName", "123456", true);// 走缓存

    accountService.getAccount("accountName", "123456", false);// 走缓存

    accountService.getAccount("accountName", "654321", true);// 查询数据库

    accountService.getAccount("accountName", "654321", true);// 走缓存


    如何做到:既要保证方法被调用,又希望结果被缓存


    根据前面的例子,我们知道,如果使用了 @Cacheable 注释,则当重复使用相同参数调用方法的时候,方法本身不会被调用执行,即方法本身被略过了,取而代之的是方法的结果直接从缓存中找到并返回了。


    现实中并不总是如此,有些情况下我们希望方法一定会被调用,因为其除了返回一个结果,还做了其他事情,例如记录日志,调用接口等,这个时候,我们可以用 @CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。


    @Cacheable(value="accountCache")

     public Account getAccountByName(String accountName) { 

       // 方法内部实现不考虑缓存逻辑,直接实现业务

       return getFromDB(accountName); 

     } 

     

     // 更新 accountCache 缓存

     @CachePut(value="accountCache",key="#account.getName()")

     public Account updateAccount(Account account) { 

       return updateDB(account); 

     } 

     private Account updateDB(Account account) { 

       logger.info("real updating db..."+account.getName()); 

       return account; 

     }


    我们的测试代码如下


    Account account = accountService.getAccountByName("someone"); 

    account.setPassword("123"); 

    accountService.updateAccount(account); 

    account.setPassword("321"); 

    accountService.updateAccount(account); 

    account = accountService.getAccountByName("someone"); 

    logger.info(account.getPassword());


    如上面的代码所示,我们首先用 getAccountByName 方法查询一个人 someone 的账号,这个时候会查询数据库一次,但是也记录到缓存中了。然后我们修改了密码,调用了 updateAccount 方法,这个时候会执行数据库的更新操作且记录到缓存,我们再次修改密码并调用 updateAccount 方法,然后通过 getAccountByName 方法查询,这个时候,由于缓存中已经有数据,所以不会查询数据库,而是直接返回最新的数据,所以打印的密码应该是“321”


    @Cacheable、@CachePut、@CacheEvict 注释介绍


    • @Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存


    • @CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用


    -@CachEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空





  • 相关阅读:
    区域赛系列一多边形划分(卡特兰数)
    Going Home(最大匹配km算法)
    奔小康赚大钱(km)
    Air Raid(最小路径覆盖)
    Save Princess(丑数)
    Asteroids(最小点覆盖)
    Windows命令行命令总结
    SPI协议详解
    Python实现串口通信(pyserial)
    python中进制转换
  • 原文地址:https://www.cnblogs.com/zhangboyu/p/7448906.html
Copyright © 2020-2023  润新知