强大的Spring缓存技术(中)

时间:2021-09-05 16:14:52

好,到目前为止,我们的 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 主要针对方法配置,能够根据一定的条件对缓存进行清空