spring-boot整合redis作为缓存(4)——spring-boot引入Redis
来源:互联网 发布:2017网络情歌 编辑:程序博客网 时间:2024/06/05 09:18
1、redis的安装
2、redis的设置
3、spring-boot的缓存
4、自定义key
5、spring-boot引入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@Configurationpublic 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 evictionsprocessCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT);// Check if we have a cached item matching the conditionsCache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));// Collect puts from any @Cacheable miss, if no cached item is foundList<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 hitif (cachePutRequests.isEmpty() && !hasCachePut(contexts)) {result = cacheHit;}// Invoke the method if don't have a cache hitif (result == null) {result = new SimpleValueWrapper(invokeOperation(invoker));}// Collect any explicit @CachePutscollectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests);// Process any collected put requests, either from @CachePut or a @Cacheable missfor (CachePutRequest cachePutRequest : cachePutRequests) {cachePutRequest.apply(result.get());}// Process any late evictionsprocessCacheEvicts(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) {@Overridepublic 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则不会产生乱码
- spring-boot整合redis作为缓存(4)——spring-boot引入Redis
- spring-boot整合redis作为缓存(2)——spring-boot的缓存
- spring-boot整合redis作为缓存(1)——redis的设置
- spring-boot整合redis作为缓存(3)——自定义key
- REDIS学习(3.2)spring boot 使用redis作为缓存
- spring-boot整合redis作为mysql二级缓存
- spring boot整合redis
- spring boot 整合 redis
- spring boot整合redis
- Spring Boot整合Redis
- spring boot redis整合
- spring boot整合redis实现缓存机制
- Spring Boot 整合 Redis 实现缓存操作
- Spring Boot 整合 Redis 实现缓存操作
- Spring Boot 整合 Redis 实现缓存操作
- Spring Boot 整合 Redis 实现缓存操作
- Spring Boot 整合 Redis 实现缓存操作
- spring-boot | 整合Redis缓存数据
- React Component Lifecycle
- RCTDeviceEventEmitter 实现简单的观察者模式
- 快速幂运算
- 未来已来——十五年网龄生活洞见科技之飞跃
- 常用排序算法总结
- spring-boot整合redis作为缓存(4)——spring-boot引入Redis
- python kmeans实战
- Java oop 第十一章 CMS
- 使用ffmpeg步骤
- 类的无参方法
- 回调函数实现
- Cocos2d-js 触摸事件的简单封装
- MVC框架简介,SSH 和SSM
- 怎么再vue中使用jquery?