Springboot中Spring-cache与redis整合。这也是一个不错的框架,与spring的事务使用类似,只要添加一些注解方法,就可以动态的去操作缓存了,减少代码的操作。如果这些注解不满足项目的需求,我们也可以参考spring-cache的实现思想,使用AOP代理+缓存操作来管理缓存的使用。
在这个例子中我使用的是redis,当然,因为spring-cache的存在,我们可以整合多样的缓存技术,例如Ecache、Mamercache等。
org.springframework.boot
spring-boot-starter-cache
org.springframework.boot
spring-boot-starter-data-redis
无论使用spring-boot的哪个版本,我们都需要先配置redis连接,两个版本的redis客户端连接池使用有所不同。
spring-boot版本 | 默认客户端类型 |
---|---|
1.5.x | jedis |
2.x | lettuce |
在1.5.x中,我们配置jedis连接池只需要配置 spring.redis.pool.* 开始的配置即可,如下配置
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.min-idle=0
spring.redis.pool.max-idle=8
但在2.x版本中由于引入了不同的客户端,需要指定配置哪种连接池,如下配置
在application.properties中添加如下代码:
#jedis客户端
server.port=8080
spring.cache.type=redis
spring.redis.host=192.168.140.130 # server host
spring.redis.password= 123456
spring.redis.port= 6379
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-idle=8
#lettuce客户端
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.shutdown-timeout=100ms
除此之外还可以看到时间配置上还需要带上对应的单位。
通用配置方式只能满足整个程序所有缓存都采用相同公共配置的方式,如果需要特殊处理,如我们的案列,则需要自己采用代码的方式来配置。
采用代码的方式,只要需要配置的是CacheMananger,采用Redis时具体实现我们需要使用其子类RedisCacheMananger来做配置
package com.answer.config;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* redis缓存配置
* Created by [email protected] on 2017/9/21.
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public KeyGenerator KeyGenerator() {
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 CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager manager = new RedisCacheManager(redisTemplate);
manager.setUsePrefix(true);
RedisCachePrefix cachePrefix = new RedisPrefix("prefix");
manager.setCachePrefix(cachePrefix);
// 整体缓存过期时间
manager.setDefaultExpiration(3600L);
// 设置缓存过期时间。key和缓存过期时间,单位秒
Map expiresMap = new HashMap<>();
expiresMap.put("user", 1000L);
manager.setExpires(expiresMap);
return manager;
}*/
@Bean
CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//初始化一个RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
//设置CacheManager的值序列化方式为JdkSerializationRedisSerializer,但其实RedisCacheConfiguration默认就是使用StringRedisSerializer序列化key,JdkSerializationRedisSerializer序列化value,所以以下注释代码为默认实现
//ClassLoader loader = this.getClass().getClassLoader();
//JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(loader);
//RedisSerializationContext.SerializationPair
以上代码中RedisCacheConfiguration类为2.x新增的配置类,增加了几个配置项。这里比较奇怪的是调用它的配置方法每一次都会重新生成一个配置对象,而不是在原对象上直接修改参数值,这一点本人没搞懂作者为何要选择这种方式。
到这里基本的配置就已经完成了
如果我们想在其他项目中添加该微服务想进行缓存的操作,需依赖king-cache模块:
com.answer
king-cache
0.0.1-SNAPSHOT
添加以下依赖:
com.answer
king-cache
0.0.1-SNAPSHOT
com.google.guava
guava
19.0
org.projectlombok
lombok
compile
依上图所示分别创建以下实例:
Info.java
package com.answer.entity;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
*
* Created by [email protected] on 2017/9/22.
*/
@Data
@AllArgsConstructor
public class Info implements Serializable{
/**
* @Fields serialVersionUID : TODO(用一句话描述这个变量表示什么)
*/
private static final long serialVersionUID = 5457507957150215600L;
public Info(String string, String string2) {
// TODO Auto-generated constructor stub
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
private String phone;
private String address;
}
User.java
package com.answer.entity;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* Created by [email protected] on 2017/9/21.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable{
/**
* @Fields serialVersionUID : TODO(用一句话描述这个变量表示什么)
*/
private static final long serialVersionUID = 1L;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private Long id;
private String name;
private String password;
}
IndexController.java
package com.answer.web;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.answer.entity.User;
import com.answer.service.UserService;
import com.google.common.collect.ImmutableMap;
/**
*
* Created by [email protected] on 2017/9/21.
*/
@RestController
public class IndexController {
@Autowired
private UserService userService;
@GetMapping("/users")
@ResponseBody
public List users() {
return userService.list();
}
@GetMapping("/user/{id}")
@ResponseBody
public User findUserById(@PathVariable("id") Long id) {
return userService.findUserById(id);
}
@GetMapping("/upuser/{id}")
@ResponseBody
public User upuser(@PathVariable("id") Long id) {
return userService.upuser(id);
}
@GetMapping("/info/{id}")
@ResponseBody
public User findInfoById(@PathVariable("id") Long id) {
return userService.findInfoById(id);
}
@GetMapping("/user/{id}/{name}")
@ResponseBody
public Map update(@PathVariable("id") Long id, @PathVariable("name") String name) {
User user = userService.findUserById(id);
user.setName(name);
userService.update(user);
return ImmutableMap.of("ret", 0, "msg", "ok");
}
}
UserService.java
package com.answer.service;
import java.util.List;
import com.answer.entity.User;
/**
*
* Created by [email protected] on 2017/9/21.
*/
public interface UserService {
List list();
User findUserById(Long id);
User findInfoById(Long id);
void update(User user);
void remove(Long id);
User upuser(Long id);
}
UserServiceImpl.java
package com.answer.service.impl;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.answer.entity.Info;
import com.answer.entity.User;
import com.answer.service.UserService;
import lombok.extern.slf4j.Slf4j;
/**
*
* Created by [email protected] on 2017/9/21.
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
private Map userMap = new HashMap<>();
private Map infoMap = new HashMap<>();
public UserServiceImpl() {
User u1=new User();
u1.setId(1L);
u1.setName("1111");
u1.setPassword("11223434");
User u2=new User();
u2.setId(1L);
u2.setName("1111");
u2.setPassword("11223434");
User u3=new User();
u3.setId(1L);
u3.setName("1111");
u3.setPassword("11223434");
userMap.put(1L,u1);
userMap.put(2L,u2);
userMap.put(3L,u3);
infoMap.put(1L, new Info("18559198715", "福州市"));
}
@Override
public List list() {
return Arrays.asList(userMap.values().toArray());
}
@Override
@Cacheable(value = "user", key = "'user'.concat(#id.toString())")
public User findUserById(Long id) {
//log.info("findUserById query from db, id: {}", id);
System.out.println("findUserById query from db, id: {}======"+id);
return userMap.get(id);
}
@Override
@Cacheable(value = "info", key = "'info'.concat(#id.toString())")
public User findInfoById(Long id) {
// log.info("findInfoById query from db, id: {}", id);
System.out.println("findUserById query from db, id: {}======"+id);
return userMap.get(id);
}
@Override
@CachePut(value = "user", key = "'user'.concat(#user.id.toString())")
public void update(User user) {
//log.info("update db, user: {}", user.toString());
System.out.println("findUserById query from db, id: {}======");
userMap.put(user.getId(), user);
}
@Override
@CacheEvict(value = "user", key = "'user'.concat(#id.toString())")
public void remove(Long id) {
//log.info("remove from db, id: {}", id);
System.out.println("findUserById query from db, id: {}======");
userMap.remove(id);
}
@Override
@CacheEvict(value = "user", key = "'user'.concat(#id.toString())")
public User upuser(Long id) {
User d= userMap.get(id);
d.setName("000000000000000000000000000000000000000000000000");
return d;
}
}
注意:启动项目前一定要对king-cache进行打包(右键项目:Run As ---Maven Install)
然后:king-cache-1 右键 maven ---Update Project
最后:以Debug 模式启动 项目:king-cache-1
在以下方法出添加断点:
然后多次访问:http://localhost:8080/user/3
你会发现第一次进入断点,后面几次不再进入断点.
项目源码下载地址: https://download.csdn.net/download/guokezhongdeyuzhou/10322780
注意以下注解的作用:
@Cacheable 触发缓存入口
@CacheEvict 触发移除缓存
@CacahePut 更新缓存
@Caching 将多种缓存操作分组
@CacheConfig 类级别的缓存注解,允许共享缓存名称
该注解是可以将缓存分类,它是类级别的注解方式。我们可以这么使用它。
这样的话,UseCacheRedisService的所有缓存注解例如@Cacheable的value值就都为user。
@CacheConfig(cacheNames = "user")
@Service
public class UseCacheRedisService {}
在redis的缓存中显示如下
127.0.0.1:6379> keys *
1) "user~keys"
2) "user_1"
127.0.0.1:6379> get user~keys
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> type user~keys
zset
127.0.0.1:6379> zrange user~keys 0 10
1) "user_1"
我们注意到,生成的user~keys,它是一个zset类型的key,如果使用get会报WRONGTYPE Operation against a key holding the wrong kind of value。这个问题坑了我很久
一般用于查询操作,根据key查询缓存.
查询的例子,当第一查询的时候,redis中不存在key,会从db中查询数据,并将返回的结果插入到redis中。
@Cacheable
public List selectAllUser(){
return userMapper.selectAll();
}
调用方式。
@Test
public void selectTest(){
System.out.println("===========第一次调用=======");
List list = useCacheRedisService.selectAllUser();
System.out.println("===========第二次调用=======");
List list2 = useCacheRedisService.selectAllUser();
for (User u : list2){
System.out.println("u = " + u);
}
}
打印结果,大家也可以试一试
只输出一次sql查询的语句,说明第二次查询是从redis中查到的。
===========第一次调用=======
keyGenerator=com.lzl.redisService.UseCacheRedisService.selectAllUser.
keyGenerator=com.lzl.redisService.UseCacheRedisService.selectAllUser.
DEBUG [main] - ==> Preparing: SELECT id,name,sex,age,password,account FROM user
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 1
===========第二次调用=======
keyGenerator=com.lzl.redisService.UseCacheRedisService.selectAllUser.
u = User{id=1, name='fsdfds', sex='fdsfg', age=24, password='gfdsg', account='gfds'}
redis中的结果
我们可以看到redis中已经存在
com.lzl.redisService.UseCacheRedisService.selectAllUser.记录了。
这么长的一串字符key是根据自定义key值生成的。
127.0.0.1:6379> keys *
1) "user~keys"
2) "com.lzl.redisService.UseCacheRedisService.selectAllUser."
3) "user_1"
127.0.0.1:6379> get com.lzl.redisService.UseCacheRedisService.selectAllUser.
"[\"java.util.ArrayList\",[[\"com.lzl.bean.User\",{\"id\":1,\"name\":\"fsdfds\",\"sex\":\"fdsfg\",\"age\":24,\"password\":\"gfdsg\",\"account\":\"gfds\"}]]]"
一般用于更新和插入操作,每次都会请求db
通过key去redis中进行操作。
1. 如果key存在,更新内容
2. 如果key不存在,插入内容。
/**
* 单个user对象的插入操作,使用user+id
* @param user
* @return
*/
@CachePut(key = "\"user_\" + #user.id")
public User saveUser(User user){
userMapper.insert(user);
return user;
}
redis中的结果
多了一条记录user_2
127.0.0.1:6379> keys *
1) "user~keys"
2) "user_2"
3) "com.lzl.redisService.UseCacheRedisService.selectAllUser."
4) "user_1"
127.0.0.1:6379> get user_2
"[\"com.lzl.bean.User\",{\"id\":2,\"name\":\"fsdfds\",\"sex\":\"fdsfg\",\"age\":24,\"password\":\"gfdsg\",\"account\":\"gfds\"}]"
根据key删除缓存中的数据。allEntries=true表示删除缓存中的所有数据。
@CacheEvict(key = "\"user_\" + #id")
public void deleteById(Integer id){
userMapper.deleteByPrimaryKey(id);
}
测试方法
@Test
public void deleteTest(){
useCacheRedisService.deleteById(1);
}
redis中的结果
user_1已经移除掉。
127.0.0.1:6379> keys *
1) "user~keys"
2) "user_2"
3) "com.lzl.redisService.UseCacheRedisService.selectAllUser."
测试allEntries=true时的情形。
@Test
public void deleteAllTest(){
useCacheRedisService.deleteAll();
}
@CacheEvict(allEntries = true)
public void deleteAll(){
userMapper.deleteAll();
}
redis中的结果
redis中的数据已经全部清空
127.0.0.1:6379> keys *
(empty list or set)
通过注解的属性值可以看出来,这个注解将其他注解方式融合在一起了,我们可以根据需求来自定义注解,并将前面三个注解应用在一起
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
使用例子如下
@Caching(
put = {
@CachePut(value = "user", key = "\"user_\" + #user.id"),
@CachePut(value = "user", key = "#user.name"),
@CachePut(value = "user", key = "#user.account")
}
)
public User saveUserByCaching(User user){
userMapper.insert(user);
return user;
}
@Test
public void saveUserByCachingTest(){
User user = new User();
user.setName("dkjd");
user.setAccount("dsjkf");
useCacheRedisService.saveUserByCaching(user);
}
redis中的执行结果
一次添加三个key
127.0.0.1:6379> keys *
1) "user~keys"
2) "dsjkf"
3) "dkjd"
4) "user_3"