1. 程式人生 > >強大的Spring快取技術(中)

強大的Spring快取技術(中)

如何清空快取

好,到目前為止,我們的 spring cache 快取程式已經執行成功了,但是還不完美,因為還缺少一個重要的快取管理邏輯:清空快取.

當賬號資料發生變更,那麼必須要清空某個快取,另外還需要定期的清空所有快取,以保證快取資料的可靠性。

為了加入清空快取的邏輯,我們只要對 AccountService2.Java 進行修改,從業務邏輯的角度上看,它有兩個需要清空快取的地方

  • 當外部呼叫更新了賬號,則我們需要更新此賬號對應的快取

  • 當外部呼叫說明重新載入,則我們需要清空所有快取

我們在AccountService2的基礎上進行修改,修改為AccountService3,程式碼如下:

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. import com.google.common.base.Optional;  
  2. import com.rollenholt.spring.cache.example1.Account;  
  3. import org.slf4j.Logger;  
  4. import org.slf4j.LoggerFactory;  
  5. import org.springframework.cache.annotation.CacheEvict;  
  6. import org.springframework.cache.annotation.Cacheable;  
  7. import org.springframework.stereotype.Service;  
  8. /** 
  9.  * @author wenchao.ren 
  10.  *         2015/1/5. 
  11.  */
  12. @Service
  13. publicclass AccountService3 {  
  14.     privatefinal Logger logger = LoggerFactory.getLogger(AccountService3.class);  
  15.     // 使用了一個快取名叫 accountCache
  16.     @Cacheable(value="accountCache")  
  17.     public Account getAccountByName(String accountName) {  
  18.         // 方法內部實現不考慮快取邏輯,直接實現業務
  19.         logger.info("real querying account... {}", accountName);  
  20.         Optional<Account> accountOptional = getFromDB(accountName);  
  21.         if (!accountOptional.isPresent()) {  
  22.             thrownew IllegalStateException(String.format("can not find account by account name : [%s]", accountName));  
  23.         }  
  24.         return accountOptional.get();  
  25.     }  
  26.     @CacheEvict(value="accountCache",key="#account.getName()")  
  27.     publicvoid updateAccount(Account account) {  
  28.         updateDB(account);  
  29.     }  
  30.     @CacheEvict(value="accountCache",allEntries=true)  
  31.     publicvoid reload() {  
  32.     }  
  33.     privatevoid updateDB(Account account) {  
  34.         logger.info("real update db...{}", account.getName());  
  35.     }  
  36.     private Optional<Account> getFromDB(String accountName) {  
  37.         logger.info("real querying db... {}", accountName);  
  38.         //Todo query data from database
  39.         return Optional.fromNullable(new Account(accountName));  
  40.     }  
  41. }  
save_snippets.png
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?在CODE上檢視程式碼片派生到我的程式碼片
  1. import com.rollenholt.spring.cache.example1.Account;  
  2. import org.junit.Before;  
  3. import org.junit.Test;  
  4. import org.slf4j.Logger;  
  5. import org.slf4j.LoggerFactory;  
  6. import org.springframework.context.support.ClassPathXmlApplicationContext;  
  7. publicclass AccountService3Test {  
  8.     private AccountService3 accountService3;  
  9.     privatefinal Logger logger = LoggerFactory.getLogger(AccountService3Test.class);  
  10.     @Before
  11.     publicvoid setUp() throws Exception {  
  12.         ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext2.xml");  
  13.         accountService3 = context.getBean("accountService3", AccountService3.class);  
  14.     }  
  15.     @Test
  16.     publicvoid testGetAccountByName() throws Exception {  
  17.         logger.info("first query.....");  
  18.         accountService3.getAccountByName("accountName");  
  19.         logger.info("second query....");  
  20.         accountService3.getAccountByName("accountName");  
  21.     }  
  22.     @Test
  23.     publicvoid testUpdateAccount() throws Exception {  
  24.         Account account1 = accountService3.getAccountByName("accountName1");  
  25.         logger.info(account1.toString());  
  26.         Account account2 = accountService3.getAccountByName("accountName2");  
  27.         logger.info(account2.toString());  
  28.         account2.setId(121212);  
  29.         accountService3.updateAccount(account2);  
  30.         // account1會走快取
  31.         account1 = accountService3.getAccountByName("accountName1");  
  32.         logger.info(account1.toString());  
  33.         // account2會查詢db
  34.         account2 = accountService3.getAccountByName("accountName2");  
  35.         logger.info(account2.toString());  
  36.     }  
  37.     @Test
  38.     publicvoid testReload() throws Exception {  
  39.         accountService3.reload();  
  40.         // 這2行查詢資料庫
  41.         accountService3.getAccountByName("somebody1");  
  42.         accountService3.getAccountByName("somebody2");  
  43.         // 這兩行走快取
  44.         accountService3.getAccountByName("somebody1");  
  45.         accountService3.getAccountByName("somebody2");  
  46.     }  
  47. }  
save_snippets.png
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?在CODE上檢視程式碼片派生到我的程式碼片
  1. /** 
  2.  * Annotation indicating that a method (or all methods on a class) trigger(s) 
  3.  * a cache invalidate operation. 
  4.  * 
  5.  * @author Costin Leau 
  6.  * @author Stephane Nicoll 
  7.  * @since 3.1 
  8.  * @see CacheConfig 
  9.  */
  10. @Target({ElementType.METHOD, ElementType.TYPE})  
  11. @Retention(RetentionPolicy.RUNTIME)  
  12. @Inherited
  13. @Documented
  14. public@interface CacheEvict {  
  15.     /** 
  16.      * Qualifier value for the specified cached operation. 
  17.      * <p>May be used to determine the target cache (or caches), matching the qualifier 
  18.      * value (or the bean name(s)) of (a) specific bean definition. 
  19.      */
  20.     String[] value() default {};  
  21.     /** 
  22.      * Spring Expression Language (SpEL) attribute for computing the key dynamically. 
  23.      * <p>Default is "", meaning all method parameters are considered as a key, unless 
  24.      * a custom {@link #keyGenerator()} has been set. 
  25.      */
  26.     String key() default"";  
  27.     /** 
  28.      * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} to use. 
  29.      * <p>Mutually exclusive with the {@link #key()} attribute. 
  30.      */
  31.     String keyGenerator() default"";  
  32.     /** 
  33.      * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to 
  34.      * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none 
  35.      * is set already. 
  36.      * <p>Mutually exclusive with the {@link #cacheResolver()}  attribute. 
  37.      * @see org.springframework.cache.interceptor.SimpleCacheResolver 
  38.      */
  39.     String cacheManager() default"";  
  40.     /** 
  41.      * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use. 
  42.      */
  43.     String cacheResolver() default"";  
  44.     /** 
  45.      * Spring Expression Language (SpEL) attribute used for conditioning the method caching. 
  46.      * <p>Default is "", meaning the method is always cached. 
  47.      */
  48.     String condition() default"";  
  49.     /** 
  50.      * Whether or not all the entries inside the cache(s) are removed or not. By 
  51.      * default, only the value under the associated key is removed. 
  52.      * <p>Note that setting this parameter to {@code true} and specifying a {@link #key()} 
  53.      * is not allowed. 
  54.      */
  55.     boolean allEntries() defaultfalse;  
  56.     /** 
  57.      * Whether the eviction should occur after the method is successfully invoked (default) 
  58.      * or before. The latter causes the eviction to occur irrespective of the method outcome (whether 
  59.      * it threw an exception or not) while the former does not. 
  60.      */
  61.     boolean beforeInvocation() defaultfalse;  
  62. }  
save_snippets.png
/**
 * 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?在CODE上檢視程式碼片派生到我的程式碼片
  1. @Cacheable(value="accountCache",condition="#accountName.length() <= 4")// 快取名叫 accountCache 
  2. public Account getAccountByName(String accountName) {  
  3.     // 方法內部實現不考慮快取邏輯,直接實現業務
  4.     return getFromDB(accountName);  
  5. }  
save_snippets.png
@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?在CODE上檢視程式碼片派生到我的程式碼片
  1. @Cacheable(value="accountCache",key="#accountName.concat(#password)")   
  2. public Account getAccount(String accountName,String password,boolean sendLog) {   
  3.   // 方法內部實現不考慮快取邏輯,直接實現業務
  4.   return getFromDB(accountName,password);   
  5. }  
save_snippets.png
@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?在CODE上檢視程式碼片派生到我的程式碼片
  1. @Cacheable(value="accountCache")  
  2.  public Account getAccountByName(String accountName) {   
  3.    // 方法內部實現不考慮快取邏輯,直接實現業務
  4.    return getFromDB(accountName);   
  5.  }   
  6.  // 更新 accountCache 快取
  7.  @CachePut(value="accountCache",key="#account.getName()")  
  8.  public Account updateAccount(Account account) {   
  9.    return updateDB(account);   
  10.  }   
  11.  private Account updateDB(Account account) {   
  12.    logger.info("real updating db..."+account.getName());   
  13.    return account;   
  14.  }  
save_snippets.png
@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?在CODE上檢視程式碼片派生到我的程式碼片
  1. Account account = accountService.getAccountByName("someone");   
  2. account.setPassword("123");   
  3. accountService.updateAccount(account);   
  4. account.setPassword("321");   
  5. accountService.updateAccount(account);   
  6. account = accountService.getAccountByName("someone");   
  7. logger.info(account.getPassword());  
save_snippets.png
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