专栏名称: ImportNew
伯乐在线旗下账号,专注Java技术分享,包括Java基础技术、进阶技能、架构设计和Java技术领域动态等。
目录
相关文章推荐
芋道源码  ·  阿里云,这次可以啊! ·  昨天  
Java编程精选  ·  SpringBoot 实现 PDF ... ·  3 天前  
芋道源码  ·  如何设计一个全局唯一的订单号? ·  3 天前  
芋道源码  ·  你了解 SpringBoot 在一次 ... ·  1 周前  
51好读  ›  专栏  ›  ImportNew

强大的Spring缓存技术(中)

ImportNew  · 公众号  · Java  · 2016-12-20 20:44

正文

(点击上方公众号,可快速关注)


来源:Rollen Holt

链接:www.cnblogs.com/rollenholt/p/4202631.html


如何清空缓存


好,到目前为止,我们的 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 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 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.

     *

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.

     *

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.

     *

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.

     *

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.

     *

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.

     *

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()

public Account getAccountByName(String accountName) {

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

    return getFromDB(accountName);

}


注意其中的 condition=”#accountName.length()


如果有多个参数,如何进行 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 主要针对方法配置,能够根据一定的条件对缓存进行清空


觉得本文对你有帮助?请分享给更多人

关注「ImportNew」,看技术干货