強大的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
- publicclass AccountService3 {
- privatefinal 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()) {
- thrownew IllegalStateException(String.format("can not find account by account name : [%s]", accountName));
- }
- return accountOptional.get();
- }
- @CacheEvict(value="accountCache",key="#account.getName()")
- publicvoid updateAccount(Account account) {
- updateDB(account);
- }
- @CacheEvict(value="accountCache",allEntries=true)
- publicvoid reload() {
- }
- privatevoid 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.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));
}
}
我們的測試程式碼如下:
[java] view plain copy print?- 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;
- publicclass AccountService3Test {
- private AccountService3 accountService3;
- privatefinal Logger logger = LoggerFactory.getLogger(AccountService3Test.class);
- @Before
- publicvoid setUp() throws Exception {
- ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext2.xml");
- accountService3 = context.getBean("accountService3", AccountService3.class);
- }
- @Test
- publicvoid testGetAccountByName() throws Exception {
- logger.info("first query.....");
- accountService3.getAccountByName("accountName");
- logger.info("second query....");
- accountService3.getAccountByName("accountName");
- }
- @Test
- publicvoid 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
- publicvoid testReload() throws Exception {
- accountService3.reload();
- // 這2行查詢資料庫
- accountService3.getAccountByName("somebody1");
- accountService3.getAccountByName("somebody2");
- // 這兩行走快取
- accountService3.getAccountByName("somebody1");
- accountService3.getAccountByName("somebody2");
- }
- }
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註解的定義,我們會發現:
[java] view plain copy print?- /**
- * 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() defaultfalse;
- /**
- * 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() defaultfalse;
- }
/**
* 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.
我們可以利用這個方法來完成這個功能,下面只給出示例程式碼:
[java] view plain copy print?- @Cacheable(value="accountCache",condition="#accountName.length() <= 4")// 快取名叫 accountCache
- public Account getAccountByName(String accountName) {
- // 方法內部實現不考慮快取邏輯,直接實現業務
- return getFromDB(accountName);
- }
@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程式碼:
[java] view plain copy print?- @Cacheable(value="accountCache",key="#accountName.concat(#password)")
- public Account getAccount(String accountName,String password,boolean sendLog) {
- // 方法內部實現不考慮快取邏輯,直接實現業務
- return getFromDB(accountName,password);
- }
@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 註釋,這個註釋可以確保方法被執行,同時方法的返回值也被記錄到快取中。
[java] view plain copy print?- @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;
- }
@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;
}
我們的測試程式碼如下
[java] view plain copy print?- 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());
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 主要針對方法配置,能夠根據一定的條件對快取進行清空
轉載自:http://blog.csdn.net/a494303877/article/details/53780631