使用NoSQL
有了RedisTemplate之后,我们就可以开始保存获取以及删除键值对了。
假设我们想通过RedisTemplate<String,Product>
保存Product,其中key是num属性的值,如下代码展示了如何借助opsForValue()
来完成该功能:
redis.opsForValue().set(product.getNum(),product);//存储num-对象的键值对
如果想获得该对象Product,且num=123456:
Product product=redis.opsForValue().get("123456");
那我们使用List类型的value与之类似,只需要使用opsForList()
即可:(假设我们希望在List类型的条目尾部添加一个值)
redis.opsForList().rightPush("cart",product);
使用rightPush()
在列的尾部添加了一个Product,所使用这个列表在存储时,key为cart。如果这个key尚未存在,将会创建一个。同理leftPush()
会在头部添加。
我们也有方法获取单个元素:
Product first=redis.opsForList().leftPop("cart");
Product last=redis.opsForList().rightPop("cart");
这两个方法有个副作用是获取的同时,也出队列了。我们如果只想获取值的话,甚至可能是在中间获取,那么我们可以使用range()
方法:
List<Product> products=redis.opsForList().range("cart",2,12);
除了操作列表外,我们还可以使用opsForSet()操作Set,最为常用的操作就是Set中添加一个元素:
redis.opsForSet().add("cart",product);
在我们有多个Set并填充值之后,我们就可以对这些Set进行一些操作,比如获取其差异,求交集,求并集:
List<Product> diff=redis.opsForSet().difference("cart","cart1");
List<Product> union=redis.opsForSet().union("cart","cart1");
List<Product> isect=redis.opsForSet().isect("cart","cart1");
当然还可以移除它的元素:
redis.opsForSet().remove(product);
Redis缓存
其实缓存的条目无非就是一个键值对,很自然的想到Redis缓存。
Redis可以用来为Spring缓存抽象机制存储缓存条目。RedisCacheManager是一个CacheManager的实现。RedisCacheManager会与一个Redis服务器写作,并通过RedisTemplate将缓存条目存储到Redis中。
为了使用RedisCacheManager,我们需要RedisTemplate bean以及RedisConnectionFactory实现类的一个bean。
最后配置:
package com.promusician.config;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@PropertySource({"classpath:redis.properties"})
public class RedisConfig {
//log
private static final Logger log=Logger.getLogger(RedisConfig.class.getName());
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Value("${redis.password}")
private String password;
// @Value("${redis.maxIdle}")
// private int maxIndle;
// @Value("${redis.testOnBorrow}")
// private boolean testOnBorrow;
// @Value("${redis.maxWait}")
// private int maxWait;
// @Value("${redis.maxActive}")
// private int maxActive;
@Bean
public JedisConnectionFactory jedisConnectionFactory(){
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostName(host);
jedisConnectionFactory.setPort(port);
jedisConnectionFactory.setPassword(password);
// jedisConnectionFactory.afterPropertiesSet();
return jedisConnectionFactory;
}
@Bean
public RedisTemplate redisTemplate(JedisConnectionFactory jedisConnectionFactory){
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate){
RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
return redisCacheManager;
}
}
在配置完缓存管理器并启动缓存后,就可以在bean方法上应用缓存规则了。
为方法添加注解以支持缓存
以下的注解能用在方法级别或者类级别上:
我们可以看到@Cacheable
和@CachePut
注解都可以填充缓存,但是他们的工作方式略有差异。
@Cacheable
首先在缓存中查找条目,如果找到了匹配的条目,那么就不会对方法进行调用了。如果没有找到匹配的条目,方法会被调用并且返回值放到缓存之中。
@CachePut
并不会在缓存中检查匹配的值,目标方法总是会调用,并将返回值添加到缓存中。
他们俩属性有一些是共有的:
在最简单的情况下,只需要使用value属性指定一个或多个缓存即可。例如考虑SpittleRepository中的findOne()方法。在初始保存之后,Spittle就不会再发生变化了。如果有的Spittle比较热门并且会被频繁的请求,我们可以在findOne()上添加@Cacheable
注解,确保将Spittle保存在缓存中,从而避免对数据库的不必要的访问。
@Cacheable("spittleCache")
public Spittle findOne(long id) {
return jdbc.queryForObject(
"select id, message, created_at, latitude, longitude" +
" from Spittle" +
" where id = ?",
new SpittleRowMapper(), id);
}
使用@Cacheable(“spittleCache”),当findOne()被调用时,缓存切面会拦截调用并在缓存中查找之前以名"spittleCache"存储的返回值,缓存的Key是传递到findOne方法中的id参数。
将值放到缓存之中
@CachePut
采用了一种更为直接的流程。带有这个注解的方法始终会被调用,而且它的返回值也会放到缓存中。这提供了很便利机制,让我们在请求之前预先加载缓存。
例如,当一个全新的Spittle通过SpittleRepository的save()方法保存之后,很可能马上就会请求这条记录。所以当save()方法调用后,立即将Spittle塞到缓存中是很有意义的。当其他人通过findOne()查找时,它已经准备就绪。
@Cacheable("spittleCache")
Spittle save(Spittle spittle);
这里唯一的问题就是,缓存的key。前文说过,缓存的key是基于方法的参数决定的。因为save的参数是Spittle,那么它会做缓存的key。然而,诡异的是,Spittle的键值对都是spittle,更不幸的是,我们希望用id作为他的key。
让我们看一下怎么自定义缓存的key。
自定义缓存Key
@Cacheable
和@CachePut
都有一个名为key属性,这个属性能够替换默认的key,它是通过一个SpEL表达式计算得到的。
具体到我们现在的场景,我们需要将Key设置为所保存Spittle的ID,以参数形式传递给save()返回的Spittle得到的id属性。幸好,为缓存编写SpEL表达式的时候,Spring暴露了一些很有用的元数据。
对于Save()方法, 我们需要的键是所返回的Spittle对象的id,表达式#result能够得到方法调动的返回值Spittle对象。我们可以将key属性设置为#result.id来引用id属性。
@CachePut(value="spittleCache",key="#result.id")
Spittle save(Spittle spittle);
条件化缓存
通过为方法添加Spring的缓存注解,Spring就会围绕这个方法创建一个缓存切面。但是,在有些场景下我们可能希望将缓存功能关闭。
@Cacheable
和@CachePut
提供了两个属性以实现条件化缓存:unless和condition。这两个属性都接受一个SpEL表达式。如果unless属性的SpEL表达式计算结果为true,那么缓存方法返回的数据就不会放到缓存中。与之类似,如果condition计算结果为false,那么这个方法缓存就会被禁用掉。
表面上看unless和condition都是做的相同的事情,但是实际上,unless属性只能组织对象放进缓存,但是在这个方法调用的时候,依然先去缓存中查找,没找到才调用方法。而condition的表达结果为false时,不会去缓存中查找,直接调用方法,同时返回值也不会放进缓存中。
作为样例(尽管有些牵强),假设对于message属性包含"NoCache"的Spittle对象不进行缓存。为了阻止这样的对象被缓存起来,我们使用unless:
@Cacheable(value="spittleCache"
unless="#result.message.contains('NoCache')")
Spittle findOne(long id);
假如我们对于ID小于10的Spittle对象不使用缓存,也不希望从缓存中获取数据:
@Cacheable(value="spittleCache"
unless="#result.message.contains('NoCache')"
condition="#id>=10")
Spittle findOne(long id);
移除缓存条目
@CacheEvict
并不往缓存中添加任何东西,相反,它还会移除一个或更多的在缓存中的条目。
在什么场景需要从缓存中移除内容呢?当缓存值不再合法时,我们应该确保将其从缓存中移除,这样的话,后续的缓存命中就不会返回旧的或者已经不存在的值。
这样的话,SpittleRepository的remove()方法就是使用@CacheEvict的绝佳选择:
@CacheEvict("spittleCache")
void remove(long spittleId);