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

25 篇文章 0 订阅
11 篇文章 0 订阅
       分几篇文章总结 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
@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则不会产生乱码





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值