1、redis的安装
2、redis的设置
依赖
需要添加的依赖如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
配置
配置可以用xml,properties,yml和javaconfig。这里推荐使用javaconfig,因为这样做最灵活。比如用其他的配置方式是不能配置缓存的过期时间。javaconfig不但能配置所有spring提供的功能,还能自己进行扩展。
这里先举一个yml配置的例子
spring:
redis:
database: 0
host: 192.168.58.133
password: nmamtf
port: 6379
timeout: 0
pool:
max-idle: 8
min-idle: 0
max-active: 8
max-wait: -1
cache:
type: Redis
cache-name: user,authTree,auth,role,vehicle,vehicleApply,vehicleApplyCollection,msgBox,report,breakRule,deviceParam,device,driver,route,area,system
每一项的意思我不细说了,配过连接池的话,都能看得懂。
这里其实有一个问题。这样配置了以后,通过上篇文章的@Cacheable添加缓存以后,redis中看到的key是乱码。
这里再给一个javaconfig配置的例子,用来解决上面的问题,并且为缓存添加过期时间
@EnableCaching
@Configuration
public class RedisConfiguration {
@Value("${vehicle.redis.host}")
private String host;
@Value("${vehicle.redis.password}")
private String password;
@Value("${vehicle.redis.port}")
private int port;
@Value("${vehicle.redis.pool.max-idle}")
private int max_idle;
@Value("${vehicle.redis.pool.min-idle}")
private int min_idle;
@Value("${vehicle.redis.pool.max-wait}")
private int max_wait;
@Value("${vehicle.redis.caches.name}")
private String cache_name;
@Value("${vehicle.redis.caches.expiration:-1}")
private String expiration;
@Value("${vehicle.redis.defaultExpiration}")
private long defaultExpiration;
@Bean
public JedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
redisConnectionFactory.setHostName(host);
redisConnectionFactory.setPort(port);
redisConnectionFactory.setPassword(password);
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(max_idle);
jedisPoolConfig.setMaxWaitMillis(max_wait);
jedisPoolConfig.setMinIdle(min_idle);
redisConnectionFactory.setPoolConfig(jedisPoolConfig);
return redisConnectionFactory;
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
redisTemplate.setConnectionFactory(cf);
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
List<String> cacheNames=new ArrayList<String>();
Map<String,Long> cacheExpirations=new HashMap<String,Long>(cacheNames.size(),1);
String[] exps=expiration.split(",");
Cache c=new Cache();
Optional.ofNullable(cache_name)
.ifPresent(cname -> {
c.index=0;
Arrays.asList(cname.split(","))
.forEach(name -> {
if(name!=null && !name.equals("")){
cacheNames.add(name);
c.index=c.index++;
if(exps[c.index]!=null && !exps[c.index].equals("")){
cacheExpirations.put(name, Long.valueOf(exps[c.index]));
}
}
});
});
cacheManager.setCacheNames(cacheNames);
cacheManager.setDefaultExpiration(defaultExpiration);
cacheManager.setExpires(cacheExpirations);
return cacheManager;
}
public class Cache{
public int index;
public String name;
public long expiration;
}
}
结合@Value的配置如下
vehicle:
redis:
host: 192.168.58.136
password: nmamtf
port: 6379
pool:
max-idle: 8
min-idle: 0
max-wait: -1
caches:
name: user,authTree,auth,role,vehicle,vehicleApply,vehicleApplyCollection,msgBox,report,breakRule,deviceParam,device,driver,route,area,system
expiration: 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600
defaultExpiration: 3600
关于@Value,我这里就不介绍了,已经不在这篇文章讨论的范围了。
另外需要解释一下的是,为什么会有Cache这么一个内部类。这里引出一个spring的不完善之处,一开始是想通过yml结合@Value这么做的
vehicle:
redis:
caches: [
{name: area,expiration: 600},
{name: route,expiration: 600}
...
]
@Value("${vehicle.redis.caches}")
private Cache[] caches;
这种语法在yml中是可以的,问题出在@Value上,@Value只能解析出字符串,其他对象,数组都不能解析。问题在于spring以前是对properties提供支持的,这个时候只存在字符串的占位符,引入yml后,该功能还没有完善。spring用的是PropertySourcesPlaceholderConfigurer,调用的是processProperties方法,源码如下
可以看到其中对占位符的操作时StringValueResolver。所以要完美支持yml中的对象和数组占位符,需要扩展PropertySourcesPlaceholderConfigurer即可。,
题外话说完,来看配置中的关键地方。
1、配置JedisConnectionFactory
a、通过JedisConnectionFactory可以设置redis的属性,包括url,密码,端口号,以及链接池
b、通过JedisPoolConfig来设置redis的连接池,并通过redisConnectionFactory.setPoolConfig(jedisPoolConfig);设置到JedisConnectionFactory
2、配置RedisTemplate
如果直接用jedis来操作redis,那么使用Jedis对象即可,这个RedisTemplate是spring-data对jedis的封装。
a、把JedisConnectionFactory设置到RedisTemplate
b、指定字符串序列化工具为key的序列化工具,redisTemplate.setKeySerializer(new StringRedisSerializer());解决乱码的关键。何为字符串序列化,其实就是
String的getBytes()方法。默认使用的是对象的序列化方法,就是调用ObjectOutputStream的write方法。这样的话,就算key是String类型,也会加入string对象的
一些额外信息,因此会造出乱码。
3、配置CacheManager
这个是spring-boot配置缓存必须的,详情请看spring-boot的缓存这篇文章。
a、使用的CacheManager为RedisCcacheManager.setCacheNamesacheManager
b、通过RedisCcacheManager的setCacheNames(Collection<String>)添加缓存
c、通过RedisCcacheManager的setExpires(Map<String, Long>)添加缓存的超期时间
d、通过RedisCcacheManager的setDefaultExpiration(Long)配置默认超期时间
乱码的解决
其实通过上面的讲解,我们已经知道乱码的解决方法:
1、key必须为字符串,这也是为什么上篇文章,自定义key的时候,BaseCacheKeyGenerator返回的是key.toString(),而不是key的原因。
2、key的序列化方式必须用String的getBytes()方法,也就是redisTemplate.setKeySerializer(new StringRedisSerializer());
乱码原因源码分析
整个缓存key的调用过程如下:
动态代理执行Aop,其中一般有两个,一个是事务,一个是cache。cache先执行,进入到CacheAspectSupport类
CacheAspectSupport->privateObject execute(CacheOperationInvoker invoker, CacheOperationContexts contexts)
Cache.ValueWrapper cacheHit =findCachedItem(contexts.get(CacheableOperation.class));为@Cacheable的相应操作
List<CachePutRequest> cachePutRequests = newLinkedList<CachePutRequest>();为@Cachput的相应操作
processCacheEvicts(contexts.get(CacheEvictOperation.class),false, result.get());为@CacheEvict的相应操作
private Object execute(CacheOperationInvoker invoker, CacheOperationContexts contexts) {
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class), ExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Cache.ValueWrapper result = null;
// If there are no put requests, just use the cache hit
if (cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
result = cacheHit;
}
// Invoke the method if don't have a cache hit
if (result == null) {
result = new SimpleValueWrapper(invokeOperation(invoker));
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(result.get());
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get());
return result.get();
}
其中,我们只关心@Cacheable的操作,在privateCache.ValueWrapper findCachedItem(Collection<CacheOperationContext>contexts) 方法中
private Cache.ValueWrapperfindCachedItem(Collection<CacheOperationContext> contexts) {
Objectresult = ExpressionEvaluator.NO_RESULT;
for(CacheOperationContext context : contexts) {
if(isConditionPassing(context, result)) {
Object key = generateKey(context, result);
Cache.ValueWrapper cached = findInCaches(context, key);
if(cached != null) {
returncached;
}
else{
if(logger.isTraceEnabled()) {
logger.trace("Nocache entry for key '" + key + "' in cache(s) " +context.getCacheNames());
}
}
}
}
returnnull;
}
Object key = generateKey(context, result);生成我们的key,在context中存在我们自定义的BaseCacheKeyGenerator。它是context.metadata.keyGenerator,context会调用KeyGenerator的public Object generate(Object target, Method method, Object... params)方法。
private Object generateKey(CacheOperationContext context, Object result) {
Object key = context.generateKey(result);
if (key == null) {
throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
"using named params on classes without debug info?) " + context.metadata.operation);
}
if (logger.isTraceEnabled()) {
logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
}
return key;
}
CacheAspectSupport$CacheOperationContext的generateKey方法
protected Object generateKey(Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
}
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}
回到CacheAspectSupport的findCachedItem方法
Cache.ValueWrapper cached = findInCaches(context, key);用来根据key找到相应的缓存。
接下来,我们来看privateCache.ValueWrapper findInCaches(CacheOperationContext context, Object key) 方法
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
Cache.ValueWrapper wrapper = doGet(cache, key);看来实际获取缓存的是doGet方法
protected Cache.ValueWrapper doGet(Cache cache, Object key) {
try {
return cache.get(key);
}
catch (RuntimeException e) {
getErrorHandler().handleCacheGetError(e, cache, key);
return null; // If the exception is handled, return a cache miss
}
}
实际是
cache.get(key)
public ValueWrapper get(Object key) {
return get(new RedisCacheKey(key).usePrefix(this.cacheMetadata.getKeyPrefix()).withKeySerializer(
redisOperations.getKeySerializer()));
}
其中我们的BaseKey被封装成了RedisCacheKey,其实我们的key没有变,只是RedisCacheKey多了一些redis的成员变量而已。
然后又调用了一个publicRedisCacheElement get(final RedisCacheKey cacheKey) 方法
public RedisCacheElement get(final RedisCacheKey cacheKey) {
notNull(cacheKey, "CacheKey must not be null!");
byte[] bytes = (byte[]) redisOperations.execute(new AbstractRedisCacheCallback<byte[]>(new BinaryRedisCacheElement(
new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) {
@Override
public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
return connection.get(element.getKeyBytes());
}
});
return (bytes == null ? null : new RedisCacheElement(cacheKey, cacheValueAccessor.deserializeIfNecessary(bytes)));
}
其中我们可以看到,最底层还是通过 connection.get(element.getKeyBytes());来实现的,connection为jedis的封装。
我们这里重点要关注的是newBinaryRedisCacheElement( newRedisCacheElement(cacheKey, null), cacheValueAccessor)这个构造器
public BinaryRedisCacheElement(RedisCacheElement element, CacheValueAccessor accessor) {
super(element.getKey(), element.get());
this.element = element;
this.keyBytes = element.getKeyBytes();
this.accessor = accessor;
lazyLoad = element.get() instanceof Callable;
this.valueBytes = lazyLoad ? null : accessor.convertToBytesIfNecessary(element.get());
}
其中this.keyBytes= element.getKeyBytes();就是用来把key进行序列化的操作。
public byte[] getKeyBytes() {
byte[] rawKey = serializeKeyElement();
if (!hasPrefix()) {
return rawKey;
}
byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length);
System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length);
return prefixedKey;
}
其中关键代码
byte[]rawKey = serializeKeyElement();
private byte[] serializeKeyElement() {
if (serializer == null && keyElement instanceof byte[]) {
return (byte[]) keyElement;
}
return serializer.serialize(keyElement);
}
其中关键代码
returnserializer.serialize(keyElement);这里的serializer为JdkSerializationRedisSerializer implementsRedisSerializer<Object>,如果我们在配置的时候设置redisTemplate.setKeySerializer(new StringRedisSerializer()),则这里的serializer变为StringRedisSerializer。
public byte[] serialize(Object object) {
if (object == null) {
return SerializationUtils.EMPTY_ARRAY;
}
try {
return serializer.convert(object);
} catch (Exception ex) {
throw new SerializationException("Cannot serialize", ex);
}
}
其中关键代码return serializer.convert(object);
public byte[] convert(Object source) {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
try {
this.serializer.serialize(source, byteStream);
return byteStream.toByteArray();
}
catch (Throwable ex) {
throw new SerializationFailedException("Failed to serialize object using " +
this.serializer.getClass().getSimpleName(), ex);
}
}
其中关键代码为 this.serializer.serialize(source,byteStream);这里的serializer为DefaultSerializer,
DefaultSerializer的serialize(source,byteStream)方法源码如下
public void serialize(Object object, OutputStream outputStream) throws IOException {
if (!(object instanceof Serializable)) {
throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " +
"but received an object of type [" + object.getClass().getName() + "]");
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
}
看到了吗,这里用的是ObjectOutputStream进行的序列化,所以造成了乱码
如果是StringRedisSerializer的话,serializer方法的源码如下:
public byte[] serialize(String string) {
return string == null ? null : string.getBytes(this.charset);
}
这样对于字符串格式的key则不会产生乱码