redis缓存实践
Redis 是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
我们将redis作为mysql的缓存服务器,每次请求数据都会去redis里查一遍,若没有则请求mysql走真正的查询再把数据存到redis。当有新增,修改,删除操作的时候更新缓存中的数据以防止脏读的发生。
引入redis的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
配置文件
#redis
spring.redis.database=0
spring.redis.host=172.16.10.11
spring.redis.port=6379
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
redis缓存配置类
@Configuration
@EnableCaching //开启缓存
public class RedisCacheConfig {
/**
* 自定义key的生成规则,保证key的唯一性
* @return
*/
@Bean
public KeyGenerator wiselyKeyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
@Bean
public RedisCacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置key的超时时间
cacheManager.setDefaultExpiration(60*30);
return cacheManager;
}
@Bean
public RedisTemplate<String, String> redisTemplate(
RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
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);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
封装一个redis的操作类
@Component
public class RedisClient {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 获取key对应list的长度
* @param key
* @return
*/
public long getListSize(String key) {
Long size = stringRedisTemplate.opsForList().size(key);
return size;
}
/**
* 根据key获取list
* @param key
* @param i
* @return
*/
public String getListData(String key,int i) {
String json = stringRedisTemplate.opsForList().index(key,i);
return json;
}
/**
* 逐条加入数据到key所在的list
* @param key
* @param json
*/
public void setListData(String key,String json) {
stringRedisTemplate.opsForList().rightPush(key, json);
}
/**
* 修改index的数据
* @param key
* @param index
* @param json
*/
public void updateListData(String key, int index, String json) {
stringRedisTemplate.opsForList().set(key, index, json);
}
/**
* 获取key的值
* @param key
* @return
*/
public String getValue(String key){
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 设置key的值
* @param key
* @param json
*/
public void setValue(String key,String json){
stringRedisTemplate.opsForValue().set(key, json);
}
}
写一个简单的controller
@Controller
@RequestMapping("/user")
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserDao userDao;
@RequestMapping(value = {"/{id}"}, method = RequestMethod.GET)
@Cacheable(value="userCache",key= "'user' + #id" )
@ResponseBody
public User user(@PathVariable long id) {
User u = userDao.findOne(id);
log.info("走db查询");
return u;
}
}
关于Spring缓存的三个注解:
1.@Cacheable
@Cacheable是用来声明方法是可缓存的。将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法。直接从缓存中取值。最简单的格式需要制定缓存名称。
默认key生成:
默认key的生成按照以下规则:
- 如果没有参数,则使用0作为key
- 如果只有一个参数,使用该参数作为key
- 如果又多个参数,使用包含所有参数的hashCode作为key
也可以通过keyGenerator="wiselyKeyGenerator"自定义key的生成策略。
2.@CachePut
如果缓存需要更新,且不干扰方法的执行,可以使用注解@CachePut。@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
3.@CacheEvict
@CacheEvict要求指定一个或多个缓存,使之都受影响。此外,还提供了一个额外的参数allEntries 。表示是否需要清除缓存中的所有元素。默认为false,当指定了allEntries为true时,将会清除缓存中所有的元素。
首次访问http://localhost:8080/user/1时会打日志走了db查询,之后的每次访问都没有再打印db的日志。通过redis客户端可以看到我们缓存的数据
缓存.png更新缓存
controller里新建一个更新数据的方法,配合@CachePut更新缓存
@Controller
@RequestMapping("/user")
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserDao userDao;
@RequestMapping(value = {"/{id}"}, method = RequestMethod.GET)
@Cacheable(value="userCache",key= "'user' + #id" )
@ResponseBody
public User user(@PathVariable long id) {
User u = userDao.findOne(id);
log.info("走db查询");
return u;
}
@RequestMapping(value = {"/update/{id}"}, method = RequestMethod.GET)
@CachePut(value= "userCache",key= "'user' + #id")
@ResponseBody
public User update(@PathVariable long id) {
User user = userDao.findOne(id);
user.setAge(10086);
userDao.save(user);
return user;
}
}
访问http://localhost:8080/user/update/1 更新数据同时更新了缓存
上述是针对单一一条数据的缓存,实际中也会有缓存列表数据的场景,我们看下列表数据要如何缓存和更新
在原来的controller上新增查询列表的接口
@RequestMapping(value = {""}, method = RequestMethod.GET)
@ResponseBody
public List<User> list() {
List<User> dataList = new ArrayList<>();
Long size = redisClient.getListSize("userList");
if(0==size){
dataList= userDao.findAll();
log.info("FindAll 走db查询");
if (null != dataList) {
for (int i = 0; i < dataList.size(); i++) {
redisClient.setListData("userList", JSON.toJSONString(dataList.get(i)));
}
}
}else{
log.info("FindAll 从redis中获取");
for (int i = 0; i < size; i++) {
String json = redisClient.getListData("userList",i);
User u = JSON.parseObject(json, User.class);
dataList.add(u);
}
}
return dataList;
}
@RequestMapping(value = {"/update/{id}"}, method = RequestMethod.GET)
@CachePut(value= "userCache",key= "'user' + #id")
@ResponseBody
public User update(@PathVariable long id) {
User user = userDao.findOne(id);
user.setAge(10086);
userDao.save(user);
Long size = redisClient.getListSize("userList");
for (int j = 0; j < size; j++) {
User u = JSON.parseObject(redisClient.getListData("userList", j), User.class);
if(id==u.getId()){
redisClient.updateListData("userList", j, JSON.toJSONString(user));
log.info("已更新缓存");
}
}
return user;
}
对于列表数据缓存的更新我还没有想好一个更好的方式,先实现一种。
缓存的删除
@RequestMapping(value = {"/delete/{id}"})
@CacheEvict(value = "userCache", allEntries = true)
@ResponseBody
public String delete(@PathVariable long id) {
userDao.delete(id);
return "success";
}
这样实现了一个简单的读写分离,实际场景中还会更加严谨,本文仅提供一个想法。
代码gti地址:http://git.oschina.net/gpy1994/redisdemo
福利
雏田