SpringBoot整合Spring-data-redis实现集中式缓存

Spring Data redis的几种序列化

从框架的角度来看,存储在Redis中的数据只是字节。虽然说Redis支持多种数据类型,但是那只是意味着存储数据的方式,而不是它所代表的内容。由我们将这些数据转化成字符串或者是其他对象。我们通过org.springframework.data.redis.serializer. RedisSerializer将自定义的对象数据和存储在Redis上的原始数据之间相互转换,顾名思义,它处理的就是序列化的过程。

先看一下RedisSerializer接口

public interface RedisSerializer {

	/**
	 * 把一个对象序列化二进制数据
	 */
	byte[] serialize(T t) throws SerializationException;

	/**
	 * 通过给定的二进制数据反序列化成对象
	 */
	T deserialize(byte[] bytes) throws SerializationException;
}

注意这里作者提示我们:Redis does not accept null keys or values but can return null replies (fornon existing keys). 大致意思Redis不接受key为null,但是对于那些不存在的key,会返回null。但是这里可以采用官方提供的org.springframework.cache.support.NullValue作为null的占位符.

NullValue源码如下:

public final class NullValue implements Serializable {

	static final Object INSTANCE = new NullValue();

	private static final long serialVersionUID = 1L;


	private NullValue() {
	}

	private Object readResolve() {
		return INSTANCE;
	}

}

下面是RedisSerializer接口的几种实现方式:

SpringBoot整合Spring-data-redis实现集中式缓存_第1张图片

首先在RedisTemplate中,我们可以看到afterPropertiesSet()方法

public void afterPropertiesSet() {
		super.afterPropertiesSet();
		boolean defaultUsed = false;
		if (defaultSerializer == null) {
			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}
		if (enableDefaultSerializer) {
			if (keySerializer == null) {
				keySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (valueSerializer == null) {
				valueSerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashKeySerializer == null) {
				hashKeySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashValueSerializer == null) {
				hashValueSerializer = defaultSerializer;
				defaultUsed = true;
			}
		}
		if (enableDefaultSerializer && defaultUsed) {
			Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
		}
		if (scriptExecutor == null) {
			this.scriptExecutor = new DefaultScriptExecutor(this);
		}
		initialized = true;
	} 

这个方法时org.springframework.beans.factory .InitializingBean接口声明的一个方法,这个接口主要就是做一些初始化动作,或者检查已经设置好bean的属性。或者在XML里面加入一个init-method,这里我们可以知道默认的RedisSerializer就是JdkSerializationRedisSerializer,StringRedisTemplate的默认序列化全是public class StringRedisTemplate extendsRedisTemplate

public StringRedisTemplate() {
		RedisSerializer stringSerializer = new StringRedisSerializer();
		setKeySerializer(stringSerializer);
		setValueSerializer(stringSerializer);
		setHashKeySerializer(stringSerializer);
		setHashValueSerializer(stringSerializer);
	}

public class StringRedisSerializer implements RedisSerializer {

	private final Charset charset;

	public StringRedisSerializer() {
		this(Charset.forName("UTF8"));
	}

	public StringRedisSerializer(Charset charset) {
		Assert.notNull(charset);
		this.charset = charset;
	}

	public String deserialize(byte[] bytes) {
		return (bytes == null ? null : new String(bytes, charset));
	}

	public byte[] serialize(String string) {
		return (string == null ? null : string.getBytes(charset));
	}
}

RedisCacheManager

RedisCacheManager的父类是AbstractTransactionSupportingCacheManager,有名字可以知道是对事务支持的一个CacheManager,默认是不会感知事务

privateboolean transactionAware = false;

对此官网的解释是:

Set whether this CacheManager should exposetransaction-aware Cache objects.

Default is "false". Set this to"true" to synchronize cache put/evict

operations with ongoing Spring-managedtransactions, performing the actual cache

put/evict operation only in theafter-commit phase of a successful transaction.

大致意思是可感知事务的意思,put,evict,意味着会改变cache,所以put,evict操作必须一个事务(同步操作),其他线程必须等正在进行put,evict操作的线程执行完,才能紧接着操作。

再往上抽取的类就是AbstractCacheManager

取几个比较经典的方法:


// Lazy cache initialization on access
	@Override
	public Cache getCache(String name) {
		Cache cache = this.cacheMap.get(name);
		if (cache != null) {
			return cache;
		}
		else {
			// Fully synchronize now for missing cache creation...
			synchronized (this.cacheMap) {
				cache = this.cacheMap.get(name);
				if (cache == null) {
					cache = getMissingCache(name);
					if (cache != null) {
						cache = decorateCache(cache);
						this.cacheMap.put(name, cache);
						updateCacheNames(name);
					}
				}
				return cache;
			}
		}
	}

这个cacheMap就是非常经典的并发容器ConcurrentHashMap,它Spring自带管理cache的工具,每个Java开发人员都应该去读一下它的实现思想。。。

private finalConcurrentMap cacheMap = newConcurrentHashMap(16);

不用说,我们直接可以想到肯定set管理cache的name。

private volatileSet cacheNames = Collections.emptySet();

关于volatile,不用多说,原子性,可见性每个Java开发人员都应该理解的。。

这个synchronized,不用多说。。也是必须理解的- -getMissingCache()这个方法默认返回null,决定权交给其实现者,可以根据name创建,也可以记录日志什么的。

	protected Cache getMissingCache(String name) {
		return null;
	}

decorateCache()这个方法顾名思义就是装饰这个cache,默认直接返回,就是我们经典的装饰模式,IO类库的设计里面也有这个装饰模式。所以说常用的设计模式也必须要掌握啊。

	protected Cache decorateCache(Cache cache) {
		return cache;
	}

这里就在子类AbstractTransactionSupportingCacheManager,里面去根据isTransactionAware字段去判断是否进行事务可感知来修饰这个cache。

	@Override
	protected Cache decorateCache(Cache cache) {
		return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache);
	}

updateCacheNames()这个方法

	private void updateCacheNames(String name) {
		Set cacheNames = new LinkedHashSet(this.cacheNames.size() + 1);
		cacheNames.addAll(this.cacheNames);
		cacheNames.add(name);
		this.cacheNames = Collections.unmodifiableSet(cacheNames);
	}

一个有顺序的set集合,最后用Collections包装成一个不能修改的set视图,LinkedHashSet也是非常有必要去了解一下底层原理的。。

配置RedisCacheManager非常简单,首先RedisCacheManager依赖RedisTemplate,RedisTemplate又依赖于连接工厂,这里就是我们的RedisConnectionFactory的实现类

JedisConnectionFactory,关于这个连接工厂:

Note: Though the database index isconfigurable, the JedisConnectionFactory only supports connecting to one Redisdatabase at a time.

Because Redis is single threaded, you areencouraged to set up multiple instances of Redis instead of using multipledatabases within a single process. This allows you to get better CPU/resourceutilization.

大意就是:虽然数据库索引是可配置的,但JedisConnectionFactory只支持一次连接到一个Redis数据库。由于Redis是单线程的,因此建议您设置多个Redis实例,而不是在一个进程中使用多个数据库。这可以让你获得更好的CPU /资源利用率。

默认是下面配置:

·        hostName=”localhost”

·        port=6379

·        timeout=2000 ms

·        database=0

·        usePool=true

先看下属性

//Redis具体操作的类
	@SuppressWarnings("rawtypes")//
	private final RedisOperations redisOperations;
	//是否使用前缀修饰cache
	private boolean usePrefix = false;
	// usePrefix = true的时候使用默认的前缀DefaultRedisCachePrefix,是:
	private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();
	//远程加载缓存
	private boolean loadRemoteCachesOnStartup = false;
	//当super的缓存不存在时,是否创建缓存,false的话就不会去创建缓存
	private boolean dynamic = true;

	// 0 - never expire  永不过期
	private long defaultExpiration = 0;
	// 针对专门的key设置缓存过期时间
	private Map expires = null;
	//缓存的名字集合
	private Set configuredCacheNames;

这里官方强烈建议我们开启使用前缀。redisCacheManager.setUsePrefix(true),因为这里默认为false。

               /**
		 * @param cacheName must not be {@literal null} or empty.
		 * @param keyPrefix can be {@literal null}.
		 */
		public RedisCacheMetadata(String cacheName, byte[] keyPrefix) {

			hasText(cacheName, "CacheName must not be null or empty!");
			this.cacheName = cacheName;
			this.keyPrefix = keyPrefix;

			StringRedisSerializer stringSerializer = new StringRedisSerializer();

			// name of the set holding the keys
			this.setOfKnownKeys = usesKeyPrefix() ? new byte[] {} : stringSerializer.serialize(cacheName + "~keys");
			this.cacheLockName = stringSerializer.serialize(cacheName + "~lock");
		}

这里我们通过追踪源码可以看见构造RedisCaheMetdata的setOfKnownKeys时候会生成一个后缀为~keys的key,而这个key的在Redis中类型是zset,它是维护已知key的一个有序set,底层是LinkedHashSet。同时我们也会在官网中看到:

By default RedisCacheManager does notprefix keys for cache regions, which can lead to an unexpected growth of a ZSETused to maintain known keys. It’s highly recommended to enable the usage ofprefixes in order to avoid this unexpected growth and potential key clashesusing more than one cache region.

大致意思就是默认情况下,RedisCacheManager不会为缓存区域创建前缀,这样会导致维护管理已知的那些key的那个zset会急剧增长(ps:这个zset的name就是上面说的setOfKnownKeys)。因此强烈建议开启默认前缀,以免这个zset意外增长以及使用多个缓冲区域带来的潜在冲突。

关于这个cacheLockName,是cache名称后缀为~lock的key,作为一把锁存放在Redis服务器上。而RedisCache其中clear方法用于清除当前cache块中所有的元素,这里会加锁,而锁的实现就是在服务器上面放刚才key是cacheLockName的元素,最后清除锁则是在clear方法执行完成后在finally中清除。 put与get方法运行时会查看是否存在lock锁,存在则会sleep 300毫秒。这个过程会一直继续,直到redis服务器上不存在锁时才会进行相应的get与put操作,这里存在一个问题,如果clear方法运行时间很长,这时当前运行clear操作的机子挂了,就导致lock元素一直存在于redis服务器上。

 之后就算这个机子重新启动后,也无法正常使用cache。原因是:get与put方法在运行时,锁lock始终存在于redis服务器上,所以在使用时应当小心避免这种问题。下面可以追踪下源码看下:

   

在RedisCache类中的静态抽象内部类LockingRedisCacheCallback中,我们可以看见在Cache的元数据中设置锁。

		@Override
		public T doInRedis(RedisConnection connection) throws DataAccessException {

			if (connection.exists(metadata.getCacheLockKey())) {
				return null;
			}
			try {
				connection.set(metadata.getCacheLockKey(), metadata.getCacheLockKey());
				return doInLock(connection);
			} finally {
				connection.del(metadata.getCacheLockKey());
			}
		}

RedisCache类中的静态抽象内部类中,static abstract classAbstractRedisCacheCallback 

	private long WAIT_FOR_LOCK_TIMEOUT = 300;
	protected boolean waitForLock(RedisConnection connection) {

			boolean retry;
			boolean foundLock = false;
			do {
				retry = false;
			if(connection.exists(cacheMetadata.getCacheLockKey())) {
					foundLock = true;
					try {
						Thread.sleep(WAIT_FOR_LOCK_TIMEOUT);
					} catch (InterruptedException ex) {
						Thread.currentThread().interrupt();
					}
					retry = true;
				}
			} while (retry);

			return foundLock;
		}

connection.exists(cacheMetadata.getCacheLockKey()就是判断哪个锁是否还在Redis中。下面我们可以简单测试下:

    @RequestMapping(value = "save/{key}.do", method = RequestMethod.POST)
    @Cacheable(value = "cache",key = "#key",condition = "#key !=  ''")
    public String save( @PathVariable String key)  {
    	System.out.println("走数据库");
    	System.out.println(cacheManager.getCacheNames());
    	return "succful";
    }

 

当我们这样设置的时候

redisCacheManager.setUsePrefix(false);

redisCacheManager.setDefaultExpiration(60*30);

这时候就会产生一个cache名+~keys的一个zset维护key的名字的一个集合

SpringBoot整合Spring-data-redis实现集中式缓存_第2张图片

redisCacheManager.setUsePrefix(true);

redisCacheManager.setCachePrefix(new MyRedisCachePrefix());

这个MyRedisCachePrefix实现了MyRedisCachePrefix接口,默认是DefaultRedisCachePrefix()是:作为分隔符,这里只是换成了#。同时这时候我们的这些缓存都有了命名空间cache加上我们自定义的#分隔符,防止了缓存的冲突。

SpringBoot整合Spring-data-redis实现集中式缓存_第3张图片

上面对于值的序列化都统一采用了Jackson2JsonRedisSerializer

template.setValueSerializer(jackson2JsonRedisSerializer);,对于key的序列化采用了

StringRedisSerializer。

对于序列的化采用是跟String交互的多就用StringRedisSerializer,存储POLO类的Json数据时就用Jackson2JsonRedisSerializer。

对于data-redis封装Cache来说,好处是非常明显的,既可以很方便的缓存对象,相比较于SpringCache、Ecache这些进程级别的缓存来说,现在缓存的内存的是使用redis的内存,不会消耗JVM的内存,提升了性能。当然这里Redis不是必须的,换成其他的缓存服务器一样可以,只要实现Spring的Cache类,并配置到XML里面就行。和原生态的jedis相比,只要方法上加上注解,就可以实现缓存,对于应用开发人员来说,使用缓存变的简单。同时也有利于对不同的业务缓存进行分组统计、监控。


附录:SpringBoot整合data-redis

Redis单节点:

 @Bean  
    public RedisConnectionFactory redisConnectionFactory() {  
        JedisConnectionFactory cf = new JedisConnectionFactory();  
        cf.setHostName("10.188.182.140");  
        cf.setPort(6379);  
        cf.setPassword("root");  
        cf.afterPropertiesSet();  
        return cf;  
}

或者在application.properites、application.yml文件里配置

Spring.redis.host: 172.26.223.153 

Spring.redis.port: 6379 

哨兵模式:

类注解:@RedisSentinelConfiguration或者@ PropertySource

配置文件

·        spring.redis.sentinel.master:mymaster

·        spring.redis.sentinel.nodes: 127.0.0.1:6379

@Bean
public RedisConnectionFactory jedisConnectionFactory() {
  RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() .master("mymaster")
  .sentinel("127.0.0.1", 26379) .sentinel("127.0.0.1", 26380);
  return new JedisConnectionFactory(sentinelConfig);
}

集群模式:

·        spring.redis.cluster.nodes:node1,node2…….

·        spring.redis.cluster.max-redirects: 集群之间最大重定向次数

@Component
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class ClusterConfigurationProperties {

    /*
     * spring.redis.cluster.nodes[0] = 127.0.0.1:7379
     * spring.redis.cluster.nodes[1] = 127.0.0.1:7380
     * ...
     */
    List nodes;

    /**
     * Get initial collection of known cluster nodes in format {@code host:port}.
     *
     * @return
     */
    public List getNodes() {
        return nodes;
    }

    public void setNodes(List nodes) {
        this.nodes = nodes;
    }
}
@Configuration
public class AppConfig {

    /**
     * Type safe representation of application.properties
     */
    @Autowired ClusterConfigurationProperties clusterProperties;

    public @Bean RedisConnectionFactory connectionFactory() {

        return new JedisConnectionFactory(
            new RedisClusterConfiguration(clusterProperties.getNodes()));
    }
}

开启缓存,注意默认@SpringBootApplication会扫描当前同级目录及其子目录的带有@Configuration的类(仅仅支持1.2+版本的springboot,之前是有三个注解@Configuration@ComponentScan@EnableAtuoConfiguration),当启动类上使用@ComponentScan注解的时候就只会扫描你自定义的基础包。当有xml.文件的时候,建议在@Configuration类上面

@ImportResource({"classpath:xxx.xml","classpath:yyy.xml"})导入

又或者当你的你@Configuration配置类没有默认在@SpringBootApplication扫描的路径下,可以使用@Import({xxx.class,yyy.class})

@Configuration
@EnableCaching 
public class RedisConfig extends CachingConfigurerSupport {
	    @Bean
    public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
        redisCacheManager.setDefaultExpiration(60*30);
        redisCacheManager.setTransactionAware(true);
        redisCacheManager.setUsePrefix(true);
        redisCacheManager.setCachePrefix(new MyRedisCachePrefix());
        return redisCacheManager;
    }
    @Bean
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate template = new StringRedisTemplate(factory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
		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);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }


}

























你可能感兴趣的:(分布式,Spring)