緩存
什么是緩存?
在高並發下,為了提高訪問的性能,需要將數據庫中 一些經常展現和不會頻繁變更的數據,存放在存取速率更快的內存中。這樣可以
- 降低數據的獲取時間,帶來更好的體驗
- 減輕數據庫的壓力
緩存適用於讀多寫少的場合,查詢時緩存命中率很低、寫操作很頻繁等場景不適宜用緩存。
MySQL有自己的查詢緩存,為什么還要使用 Redis 等緩存應用?
- 當只有一台 MySQL服務器時,可以將緩存放置在本地。這樣當有相同的 SQL 查詢到達時,可以直接從緩存中取到查詢結果,不需要進行 SQL 的解析和執行。MySQL 提供了服務器層面的緩存支持。
- 如果有多台 MySQL 服務器,請求會隨機分發給多台中的一台,我們無法保證相同的請求會到達同一台服務器,本地緩存命中率較低。所以基於本機的緩存就沒有什么意義,此時采用的策略應該是將查詢結果緩存在 Redis 或者 Memcache 中。
而Redis是一個高性能的 key-value 內存數據庫,恰恰可以作為緩存使用。
GitHub 地址:https://github.com/antirez/redis 。Github 是這么描述的:
Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, HyperLogLogs, Bitmaps.
但是mysql自己本身有查詢緩存,memcached也是一個優秀的內存數據庫,為什么一定要選擇redis
緩存更新
查看緩存更新的套路,緩存更新的模式有四種:
- Cache aside
- Read through
- Write through
- Write behind cachin。
這里我們使用的是 Cache Aside 策略,從三個維度:
- 命中:應用程序從cache中取數據,取到后返回。執行圖中1,2步
- 失效:應用程序先從cache取數據,沒有得到,則從數據庫中取數據,成功后,放到緩存中。執行圖中1,2,3,4,1,2步
- 更新:先把數據存到數據庫中,成功后,再讓緩存失效。執行圖中1,2步
spring配置redis緩存
接下來講解一下spring的配置。
依賴配置
pom.xml中添加
<!-- redis cache-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.2.RELEASE</version>
</dependency>
RedisConfig
現在我們使用的是java config 配置,因此需要將本RedisConfig放在可以被
<context:component-scan base-package=""/>
掃描的包下。
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
/**
* @author 李文浩
* @version 2017/11/5.
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public JedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
// Defaults
redisConnectionFactory.setHostName("127.0.0.1");
redisConnectionFactory.setPort(6379);
return redisConnectionFactory;
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.afterPropertiesSet();
setSerializer(redisTemplate);
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
// 設置緩存過期時間,秒
rcm.setDefaultExpiration(600);
return rcm;
}
private void setSerializer(RedisTemplate<String, String> template) {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
}
@Override
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(":" + method.getName());
for (Object obj : params) {
sb.append(":" + null == obj ? "null" : obj.toString());
}
return sb.toString();
}
};
}
}
如果我們不配置重寫keyGenerator()
方法的話,默認的key生成策略是
Cacheable.java
/**
* Spring Expression Language (SpEL) expression for computing the key dynamically.
* <p>Default is {@code ""}, meaning all method parameters are considered as a key,
* unless a custom {@link #keyGenerator} has been configured.
* <p>The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
* <ul>
* <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
* references to the {@link java.lang.reflect.Method method}, target object, and
* affected cache(s) respectively.</li>
* <li>Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
* <li>Method arguments can be accessed by index. For instance the second argument
* can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
* can also be accessed by name if that information is available.</li>
* </ul>
*/
String key() default "";
也就是把所有的方法參數作為一個key,但是這可能會重復。
緩存注解
@CacheConfig
:主要用於配置該類中會用到的一些共用的緩存配置。在這里@CacheConfig(cacheNames = "companies")
,配置了該數據訪問對象中返回的內容將存儲於名為companies的緩存對象中,我們也可以不使用該注解,直接通過@Cacheable
自己配置緩存集的名字來定義。@Cacheable
: 聲明Spring在調用方法之前,首先應該在緩存中查找方法的返回值。如果這個值能夠找到,就會返回存儲的值,否則的話,這個方法就會被調用,返回值會放在緩存之中。該注解主要有下面幾個參數:value
、cacheNames
:兩個等同的參數(cacheNames為Spring4新增,作為value的別名),用於指定緩存存儲的集合名。由於Spring4中新增了@CacheConfig
,因此在Spring3中原本必須有的value屬性,也成為非必需項了key
:緩存對象存儲在Map集合中的key值,非必需,缺省按照函數的所有參數組合作為key值,若自己配置需使用SpEL表達式,比如:@Cacheable(key = "#p0"):使用函數第一個參數作為緩存的key值,更多關於SpEL表達式的詳細內容可參考官方文檔condition
:緩存對象的條件,非必需,也需使用SpEL表達式,只有滿足表達式條件的內容才會被緩存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有當第一個參數的長度小於3的時候才會被緩存,若做此配置上面的AAA用戶就不會被緩存,讀者可自行實驗嘗試。unless
:另外一個緩存條件參數,非必需,需使用SpEL表達式。它不同於condition參數的地方在於它的判斷時機,該條件是在函數被調用之后才做判斷的,所以它可以通過對result進行判斷。keyGenerator
:用於指定key生成器,非必需。若需要指定一個自定義的key生成器,我們需要去實現org.springframework.cache.interceptor.KeyGenerator接口,並使用該參數來指定。需要注意的是:該參數與key是互斥的cacheManager
:用於指定使用哪個緩存管理器,非必需。只有當有多個時才需要使用cacheResolver
:用於指定使用那個緩存解析器,非必需。需通過org.springframework.cache.interceptor.CacheResolver接口來實現自己的緩存解析器,並用該參數指定。
除了這里用到的兩個注解之外,還有下面幾個核心注解:
@CachePut
: 表明Spring應該將方法的返回值放到緩存中,在方法的調用前並不會檢查緩存,方法始終都會被調用。它的參數與@Cacheable
類似,具體功能可參考上面對@Cacheable
參數的解析。@CacheEvict
:配置於函數上,通常用在刪除方法上,用來從緩存中移除相應數據。除了同@Cacheable
一樣的參數之外,它還有下面兩個參數:allEntries
:非必需,默認為false。當為true時,會移除所有數據beforeInvocation
:非必需,默認為false,會在調用方法之后移除數據。當為true時,會在調用方法之前移除數據。
緩存與數據庫一致性
- 數據庫處理要求強一致實時性的數據,例如金融數據、交易數據。
- Redis處理不要求強一致實時性的數據,例如網站最熱貼排行榜。
也就是說根據你的業務需求,設置你的過期時間,容許redis有一些不一致。
注意:
- 緩存java對象時必須實現Serilaizable接口,因為Spring會將對象先序列化之后再存入到Redis中。
- 緩存方法的
@Cacheable
最好使用方法名,避免不同的方法的 @Cacheable 值一致,然后再配以以上緩存策略。 - 在我將這個
@Cacheable
放置在SSM的dao層和service層時,redis緩存可以正常運行,但是當我將@Cacheable
放在action層上時就會有NPE。 @Cacheable
沒有配置名字,改為@Cacheable("值")
,否則會出現如下錯誤。java.lang.IllegalStateException: No cache could be resolved for 'Builder[public abstract studio.jikewang.entity.TeacherClass studio.jikewang.dao.TeacherClassDao.getTeacherClass(int)] caches=[] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'' using resolver 'org.springframework.cache.interceptor.SimpleCacheResolver@4f8d471b'. At least one cache should be provided per cache operation.
參考文檔: