强大的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 主要针对方法配置,能够根据一定的条件对缓存进行清空

转载于:https://www.cnblogs.com/happy0120/p/6206275.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值