spring-boot整合redis作为缓存(4)——spring-boot引入Redis

来源:互联网 发布:2017网络情歌 编辑:程序博客网 时间:2024/06/05 09:18
       分几篇文章总结spring-boot与Redis的整合

        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则不会产生乱码





0 0
原创粉丝点击