公司最近打算做高可用,分布式,集群之类的,原有的缓存架构是springcache整合的ehcache,ehcache做分布式并不适合,因此改为了redis做分布式缓存,由于原来项目中就引用了redis,用来存储一些业务数据,且默认链接的0数据库。现在打算把缓存数据和业务数据区分开,所以一个项目进行了多连接,缓存数据存在了2数据库,(目前有个问题是,以后做集群的话,会只复制0数据库内容,相应的配置还需要修改)
pom.xml
appolication.yml
spring:
cache:
type: redis
redis:
# cluster:
# (普通集群, 不使用则不用开启)在群集中执行命令时要遵循的最大重定向数目。
# max-redirects:
# (普通集群, 不使用则不用开启)以逗号分隔的“主机:端口”对列表进行引导。
# nodes:
# Redis数据库索引(默认为0)
database: 0
# Redis服务器地址
host: xxx
# Redis服务器连接端口
port: 6379
# Redis服: 务器连接密码(默认为空)
password: xxx
#连接超时时间(毫秒)
timeout: 1000
jedis:
pool :
#连接池最大连接数(使用负值表示没有限制)
max-active: 100
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 1000
#连接池中的最大空闲连接
max- idle: 50
#连接池中的最小空闲连接
min-idle: 5
redisTwo:
# cluster:
# (普通集群, 不使用则不用开启)在群集中执行命令时要遵循的最大重定向数目。
# max-redirects:
# (普通集群, 不使用则不用开启)以逗号分隔的“主机:端口”对列表进行引导。
# nodes:
# Redis数据库索引(默认为0)
database: 2
# Redis服务器地址
host: xxx
# Redis服务器连接端口
port: 6379
# Redis服: 务器连接密码(默认为空)
password: xxx
#连接超时时间(毫秒)
timeout: 1000
jedis:
pool :
#连接池最大连接数(使用负值表示没有限制)
max-active: 100
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 1000
#连接池中的最大空闲连接
max- idle: 50
#连接池中的最小空闲连接
min-idle: 5
配置文件代码:
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean(name ="redisConnectionFactoryTwo")
public RedisConnectionFactory redisConnectionFactoryTwo (
@Value("${spring.redisTwo.host}") String hostName,
@Value("${spring.redisTwo.database}") int database,
@Value("${spring.redisTwo.port}") int port,
@Value("${spring.redisTwo.password}") String password,
@Value("${spring.redisTwo.timeout}") int timeout,
@Value("${spring.redisTwo.jedis.pool.max-active}") int maxActive,
@Value("${spring.redisTwo.jedis.pool.max-wait}") long maxWait ,
@Value("${spring.redisTwo.jedis.pool.max-idle}") int maxIdle,
@Value("${spring.redisTwo.jedis.pool.min-idle}") int minIdle) {
return getRedisConnectionFactory(hostName, database, port,password, timeout, maxActive, maxWait,maxIdle, minIdle);
}
@Bean(name = "redisTemplateTwo")
public RedisTemplate<String, Serializable> redisTemplateTwo(RedisConnectionFactory redisConnectionFactoryTwo) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
//redisT emplate.setKeySerializer(new StringRedisSerializer());
//redisTemplate.setVa lueSerializer(new Jackson2JsonRedisSerializer<>(0bject.class));
redisTemplate.setConnectionFactory( redisConnectionFactoryTwo);
return redisTemplate;
}
@Bean(name ="cacheManagerTwo" )
public CacheManager cacheManagerTwo(RedisTemplate redisTemplateTwo) {
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("user_service", getCacheConfigurationWithTtl(redisTemplateTwo, 60));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisT emplateTwo.getConnectionFactory())
.cacheDefaults(getCacheConfigurationWithTtl(redisTemplateTwo, 10))
.withInitialCacheConfigurations(configMap)
.transactionAware()
.build();
}
@Bean(name = "redisConnectionFactory" )
public RedisConnectionFactory redisConnectionFactory (
@Value("${spring.redis.host}") String hostName,
@Value("${spring.redis.database}") int database,
@Value("${spring.redis.port}") int port,
@Value("${spring.redis.password}") String password,
@Value("${spring.redis.timeout}") int timeout
@Value("${spring.redis.jedis.pool.max-active}") int maxActive,
@Value("${spring.redis.jedis.pool.max-wait}") long maxWait,
@Value("${spring.redis.jedis.pool.max-idle}") int maxIdle,
@Value("${spring.redis.jedis.pool.min-idle}") int minIdle)
return getRedisConnectionFactory(hostName, database, port, password, timeout, maxActive, maxWait, maxIdle, minIdle);
}
@Primary
@Bean(name = "cacheManager")
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//全局redis缓存过期时间
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), redisCacheConfiguration);
}
@Override
public KeyGenerator keyGenerator() {
return (clazz, method, args) -> {
StringBuilder sb = new StringBuilder();
sb.append(clazz.getClass().getName()).append("#" );
sb.append(method.getName()).append("(");
for (0bject obj : args) {
sb.append(obj.toString()).append(",");
}
sb.deleteCharAt(sb.length()-1);
sb.append(")");
return sb.toString();
};
}
private RedisCacheConfiguration getCacheConfigurationWithTtl(RedisTemplate<String, 0bject> template, long minutes) {
return RedisCacheConfiguration
.defaul tCacheConfig()
//设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(template. getStringSerializer()))
//设置value
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(template. getValueSerializer()))
//不缓存值为null的值
//.disableCachingNullValues()
//缓存数据保存时间
.entryTtl(Duration.ofMinutes(minutes));
}
private RedisConnectionFactory getRedisConnectionFactory(String hostName, int database, int port, String password, int timeout,int maxActive long maxWait, int maxIdle, int minIdle) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostlarme(hostName);
jedisConnectionFactory.setDatabase(database);
jedisConnectionFactory.setPort(port);
jedisConnectionFactory.setPassword(password);
jedisConnectionFactory.setTieouE(timeout);
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxActive) ;
jedisPoolConfig.setMaxWaitMillis(maxWait);
jedisPoolConfig.setMaxIdle(maxIdle) ;
jedisPoolConfig.setMinIdle(minIdle);
jedisConnectionFactory.setRoelConfig(jedisPoolConfig);
return jedisConnectionFactory;
}
}
关于业务数据的redisdao的封装:
@Repository
public class RedisDao {
private final StringRedisTemplate redisTemplate;
private final static String ASTERISK = "*";
@Autowired
public RedisDao(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
*根据Key键进行自增操作
*@param key键
*@return key键对应的自增后的值
*/
public long incr(String key) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
Long value = ops.increment(key, delta: 1) ;
Assert.notNull(value, message: "[ERR276676010] Failed to incr"+ key);
return value;
}
/**
*根据Key键进行自诚操作
*@param key键
*@return key键 对应的自减后的值
**/
public long decr(String key) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
Long value = ops.increment(key, delta: -1) ;
Assert.notNull(value, message: "[ERR277653053] Failed to decr"+ key);
return value;
}
/**
*set存储键值对
*@param key键
*@param value值
*return key键对应的原值
**/
public void set(String key, String value) (
try{
ValueOperations<String, String> ops = redisTemplate.opsForValue( ) ;
ops.set(key,value);
}catch(Exception e){
e.printStackTrace();
}
}
/**
* set存储键值对
*@param key键
*@param value值
*@param time 存储时间
*@return key键对应的原值
*/
public void set(String key , String value, long time) {
try{
ValueOperations<String, String> ops = redisTemplate.opsForValue( );
ops.set (key,value,time, TimeUnit.SECONDS);
}catch(Exception e){
e. printStackTrace();
}
}
/**
*根据key键获取value值
*@param keyb 键
*@return key键 对应的值
**/
public String get(String key) { return key==nul1?null:redisTemplate.opsForValue().get(key); }
/**
*根据单个key键删除
*@param key 键
**/
public void del(String key) {
if (key!=nu1l){
redisTemplate.delete(key);
}
}
/* *
*根据多个key键批量
*@param keys 键值组
**/
public void del(String keys) {
if(keys!=null){
redisTemplate.delete(Arrays.asList(keys));
}
}
/**
*指定缓存失效时间
*@param key 键
*@paramtime 时间(秒)
*@return是否设 置成功
*/
public boolean expire(String key, long time){
try{
if(time>0){
redisTemplate.expire(key , time , TimeUnit.SECONDS) ;
}
return true;
}catch(Exception e){
e.printStackTrace();
return false;
}
}
/**
根据key获取过期时间
@param key键
@return时间 (秒),返回e代表永久有
*/
public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); }
/**
*判断key是否存在
@param key
键
@return true存在, false不存
**/
public boolean hasKey(String key){
try{
return redis Template.hasKey(key);
}catch (Exception e){
e. printStackTrace() ;
return false;
}
}
/**
*hash表get操作
@param key hash表的key
@param f ields hash表中的字段名
@return查出结果
**/
public List<String> hmget(String key, String fields) {
HashOperations<String, String, String> ops = redisTemplate.opsForHash();
return ops.multiGet(key, Arrays.asList(fields));
}
/**
*$查询list中start stop范围内的值
*@param key list key
*@param start 开始索引
* @param stop 结束索引
*@return查出的结果
**/
public List<string> lrange(String key, int start, int stop) {
return redisTemplate.opsForList().range(key, start, stop);
}
/**
*数据Left push到list
*@param key list key
*@param values push值列表通配符删除
*@return插入的值个数
*/
public long lpush(String key, String values) {
Long result = redisTemplate.opsForList().leftPushAll(key, values);
return result != null ? result : 0;
}
/**
*清理批次执行redis缓存数据
*/
protected void clearOrderData(String key){
Set<String> keys = this.redisTemplate.keys("*" +key + "*");
redisTemplate.delete(keys);
}
/**
*通配符删除
* @param wildcardKey 用于删除的key 通配符,例如: EV_ *,*EV*
*/
public long wildcardDel(@NotNu1l String wildcardKey) {
Set<String> keys = this.redisTemplate.keys(wildcardKey);
Long result = redisTemplate.delete(keys);
return result != null ? result : 0;
}
/**
通配符删除
@param wildcardKey 用于删除的key 通配符,例如: EV_ *,*EV*
**/
public int wildcardCount(@NotNu1l String wildcardKey) {
intlimit=1000;
Set<String> keys = this.scanValues(wildcardRight(wildcardKey), limit);
return keys.size( );
}
/**
通配符删除
@param wildcardKey 用于删除的key 通配符,例如: EV_ *,*EV*
**/
public Set<String> gueryKeys (@NotNu1l String wildcardKey){
intlimit=1000;
return this.scanKeys(wiLdcardRight(wildcardKey), limit);
}
/**
*左右增加通配符
*/
public static String wildcard(String str) { return ASTERISK + str + ASTERISK; }
/**
*左边增加通配符
*/
public static String wildcardLeft(String str) { return ASTERISK + str; }
/**
*右边增加通配符
*/
public static String wildcardRight(String str) { return str + ASTERISK;}
/**
自定义redis scan操作获取vaLue集合
* @param pattern
*@paramL imit
*@return {@Link Cursor< String>}
**/
private Set<String> scanValues(String pattern, int limit) {
return (Set<String>)redisTemplate.execute(new.RedisCallback<Set<String>>() {
@Override
public Set<String> doInRedis( Redi sConnection connection) throws DataAccessException {
Set<String> valueSet = new HashSet<>( ) ;
try (Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder()
.match(pattern).count(limit).build())) {
while(cursor.hasNext()) {
byte[] bytes = connection.get(cursor.next());
String value = String.valueOf(redisTemplate.getValueSerializer().deserialize(bytes));
valueSet.add(value);
}
} catch (IOException e) {
log.error(String.format("get cursor close {%s}", e));
}
return valueSet ;
}
});
}
/**
*自定义redis scan操作 获取key集合
@param pattern
@paramL imit
@return
{@link Cursor<String>}
**/
private Set<String> scanKeys(String pattern, int limit) {
return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<String> keySet = new HashSet<>();
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(limit).build();
Cursor<byte[]> cursor = connection.scan(options ) ;
while (cursor.hasNext()){
keySet.add(new String(cursor.next()));
}
return keySet;
});
}
}
问题1. 多数据源连接配置
上述配置文件中,redisConnectionFactory和redisConnectionFactoryTwo两个连接配置,同样也创建了两个cacheManager,需要注意的地方是要默认指定一个一个cacheManager,直接加上@Primary注解。在使用缓存注解的时候,需要加上cacheManger,指定要使用的cacheManager,否则默认加了@Primary注解的cacheManager。如下图所示。
关于springCache可以参考这篇文章。
问题2. 指定缓存有效时间
RedisCacheManager类中有一个withInitialCacheConfigurations这里可以传入自定义配置,注意这个方法的参数是一个map类型,key为缓存名称对应缓存注解中的cacheNames,value 为RedisCacheConfiguration 。configMap.put(“user_service”, getCacheConfigurationWithTtl(redisTemplateTwo, 60));
再提供一个getCacheConfigurationWithTtl 方法 把有效期作为参数传进去
private RedisCacheConfiguration getCacheConfigurationWithTtl(RedisTemplate<String, 0bject> template, long minutes) {
return RedisCacheConfiguration
.defaul tCacheConfig()
//设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(template. getStringSerializer()))
//设置value
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(template. getValueSerializer()))
//不缓存值为null的值
//.disableCachingNullValues()
//缓存数据保存时间
.entryTtl(Duration.ofMinutes(minutes));
}
问题3. 允许缓存值为null的数据
RedisCacheConfiguration该配置类中,封装了disableCachingNullValues()方法,就是给配置类中的cacheNullValues赋值为false;该类的默认构造方法中是给cacheNullValues赋值为true;如果不想缓存null的值,可以在配置创建的时候给配置类调用disableCachingNullValues()方法。
除了配置上面添加不允许存储值为null的配置以外,还可以在缓存注解上添加 unless= “#result == null”
问题4. 序列化失败问题
缓存的时候采取的是jdk自带的序列化,因为要缓存的对象由于业务需要里面有很多嵌套对象,如果用jackson序列化会OOM,采用jdk序列化就要注意类中的属性(除去静态变量)是否都实现了序列化接口,或者添加了transient关键字。
问题5. 反序列化失败问题
堆栈异常信息:
项目中是因为服务器上配置错了,,存储到数据库2和0的两种序列化方式不同,0的用的jackson,2用的jdk自带的序列化。当时以为是由于提的common包中 的对象版本不一致,,且对象的序列化id没有声明,所以先是把对象的序列化id都加上了
idea中配置提示实现序列化接口的类,声明序列化id
备注一张图: