缓存在开发中是一个必不可少的优化点,近期在公司的项目重构中,关于缓存优化了很多点,比如在加载一些数据比较多的场景中,会大量使用缓存机制提高接口响应速度,简介提升用户体验。关于缓存,很多人对它都是既爱又恨,爱它的是:它能大幅提升响应效率,恨的是它如果处理不好,没有用好比如LRU这种策略,没有及时更新数据库的数据就会导致数据产生滞后,进而产生用户的误读。
这个注解的的主要作用就是全局配置缓存,比如配置缓存的名字(cacheNames),只需要在类上配置一次,下面的方法就默认以全局配置为主,不需要二次配置,节省了部分代码。
这个注解是最重要的,主要实现的功能再进行一个读操作的时候。就是先从缓存中查询,如果查找不到,就会走数据库的执行方法,这是缓存的注解最重要的一个方法,基本上我们的所有缓存实现都要依赖于它。它具有的属性为cacheNames:缓存名字,condtion:缓存的条件,unless:不缓存的条件。可以指定SPEL表达式来实现,也可以指定缓存的key,缓存的内部实现一般都是key,value形式,类似于一个Map(实际上cacheable的缓存的底层实现就是concurrenHashMap),指定了key,那么缓存就会以key作为键,以方法的返回结果作为值进行映射。
这个注解主要是配合@Cacheable一起使用的,它的主要作用就是清除缓存,当方法进行一些更新、删除操作的时候,这个时候就要删除缓存。如果不删除缓存,就会出现读取不到最新缓存的情况,拿到的数据都是过期的。它可以指定缓存的key和conditon,它有一个重要的属性叫做allEntries默认是false,也可以指定为true,主要作用就是清除所有的缓存,而不以指定的key为主。
这个注解它总是会把数据缓存,而不会去每次做检查它是否存在,相比之下它的使用场景就比较少,毕竟我们希望并不是每次都把所有的数据都给查出来,我们还是希望能找到缓存的数据,直接返回,这样能提升我们的软件效率。
这个注解它是上面的注解的综合体,包含上面的三个注解(cacheable、cachePut、CacheEvict),可以使用这一个注解来包含上面的所有的注解,看源码如下
主要需要注意的是我们上述讲述的缓存注解都是基于service层(不能放在contoller和dao层),首先我们在类上配置一个CacheConfig,然后配置一个cacheNames,那么下面的方法都是以这个缓存名字作为默认值,他们的缓存名字都是这个,不必进行额外的配置。当进行select查询方法的时候,我们配置上@Cacheable,并指定key,这样除了第一次之外,我们都会把结果缓存起来,以后的结果都会把这个缓存直接返回。而当进行更新数据(删除或者更新操作)的时候,使用@CacheEvict来清除缓存,防止调用@Cacheabel的时候没有更新缓存
@Service
@CacheConfig(cacheNames = "articleCache")
public class ArticleService {
private AtomicInteger count =new AtomicInteger(0);
@Autowired
private ArticleMapper articleMapper;
/**
* 增加一篇文章 每次就进行缓存
* @return
*/
@CachePut
public Integer addArticle(Article article){
Integer result = articleMapper.addArticle(article.getTitle(), article.getAuthor(), article.getContent(), article.getFileName());
if (result>0) {
Integer lastInertId = articleMapper.getLastInertId();
System.out.println("--执行增加操作--id:" + lastInertId);
}
return result;
}
/**
* 获取文章 以传入的id为键,当state为0的时候不进行缓存
* @param id 文章id
* @return
*/
@Cacheable(key = "#id",unless = "#result.state==0")
public Article getArticle(Integer id) {
try {
//模拟耗时操作
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
final Article artcile = articleMapper.getArticleById(id);
System.out.println("--执行数据库查询操作"+count.incrementAndGet()+"次"+"id:"+id);
return artcile;
}
/**
* 通过id更新内容 清除以id作为键的缓存
*
* @param id
* @return
*/
@CacheEvict(key = "#id")
public Integer updateContentById(String contetnt, Integer id) {
Integer result = articleMapper.updateContentById(contetnt, id);
System.out.println("--执行更新操作id:--"+id);
return result;
}
/**
* 通过id移除文章
* @param id 清除以id作为键的缓存
* @return
*/
@CacheEvict(key = "#id")
public Integer removeArticleById(Integer id){
final Integer result = articleMapper.removeArticleById(id);
System.out.println("执行删除操作,id:"+id);
return result;
}
}
大概的流程,如下图所示:
org.springframework.cache.CacheManager
public class RedisGuavaCacheManager implements CacheManager {
private final Logger logger = LoggerFactory.getLogger(RedisGuavaCacheManager.class);
private ConcurrentMap cacheMap = new ConcurrentHashMap<>();
private CacheRedisGuavaProperties cacheRedisGuavaProperties;
private RedisTemplate
org.springframework.cache.support.AbstractValueAdaptingCache
,主要是重写redis和guava cache的更新策略。public class RedisGuavaCache extends AbstractValueAdaptingCache {
private final Logger logger = LoggerFactory.getLogger(RedisGuavaCache.class);
private String name;
private RedisTemplate stringKeyRedisTemplate;
private com.google.common.cache.Cache loadingCache;
private String cachePrefix;
private long defaultExpiration = 0;
private Map expires;
private String topic = "cache:redis:guava:topic";
private Map keyLockMap = new ConcurrentHashMap();
public RedisGuavaCache(boolean allowNullValues) {
super(allowNullValues);
}
public RedisGuavaCache(String name, RedisTemplate stringKeyRedisTemplate,
com.google.common.cache.Cache loadingCache,
CacheRedisGuavaProperties cacheRedisGuavaProperties) {
super(cacheRedisGuavaProperties.isCacheNullValues());
this.name = name;
this.stringKeyRedisTemplate = stringKeyRedisTemplate;
this.loadingCache = loadingCache;
this.cachePrefix = cacheRedisGuavaProperties.getCachePrefix();
this.defaultExpiration = cacheRedisGuavaProperties.getRedis().getDefaultExpiration();
this.expires = cacheRedisGuavaProperties.getRedis().getExpires();
this.topic = cacheRedisGuavaProperties.getRedis().getTopic();
}
@Override
public String getName() {
return this.name;
}
@Override
public Object getNativeCache() {
return this;
}
@SuppressWarnings("unchecked")
@Override
public T get(Object key, Callable valueLoader) {
Object value = lookup(key);
if (value != null) {
return (T) value;
}
ReentrantLock lock = keyLockMap.get(key.toString());
if (lock == null) {
logger.debug("create lock for key : {}", key);
lock = new ReentrantLock();
keyLockMap.putIfAbsent(key.toString(), lock);
}
try {
lock.lock();
value = lookup(key);
if (value != null) {
return (T) value;
}
value = valueLoader.call();
Object storeValue = toStoreValue(value);
put(key, storeValue);
return (T) value;
} catch (Exception e) {
throw new ValueRetrievalException(key, valueLoader, e.getCause());
} finally {
lock.unlock();
}
}
@Override
public void put(Object key, Object value) {
if (!super.isAllowNullValues() && value == null) {
this.evict(key);
return;
}
long expire = getExpire();
if (expire > 0) {
stringKeyRedisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
} else {
stringKeyRedisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
}
push(new CacheMessage(this.name, key));
loadingCache.put(key, value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
Object cacheKey = getKey(key);
Object prevValue = null;
// 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作
synchronized (key) {
prevValue = stringKeyRedisTemplate.opsForValue().get(cacheKey);
if (prevValue == null) {
long expire = getExpire();
if (expire > 0) {
stringKeyRedisTemplate.opsForValue()
.set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
} else {
stringKeyRedisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
}
push(new CacheMessage(this.name, key));
loadingCache.put(key, toStoreValue(value));
}
}
return toValueWrapper(prevValue);
}
@Override
public void evict(Object key) {
// 先清除redis中缓存数据,然后清除guava中的缓存,避免短时间内如果先清除guava缓存后其他请求会再从redis里加载到guava中
stringKeyRedisTemplate.delete(getKey(key));
push(new CacheMessage(this.name, key));
loadingCache.invalidate(key);
}
@Override
public void clear() {
// 先清除redis中缓存数据,然后清除guava中的缓存,避免短时间内如果先清除guava缓存后其他请求会再从redis里加载到guava中
Set keys = stringKeyRedisTemplate.keys(this.name.concat(":*"));
for (Object key : keys) {
stringKeyRedisTemplate.delete(key);
}
push(new CacheMessage(this.name, null));
loadingCache.invalidateAll();
}
@Override
protected Object lookup(Object key) {
Object cacheKey = getKey(key);
Object value = loadingCache.getIfPresent(key);
if (value != null) {
logger.debug("get cache from guava, the key is : {}", cacheKey);
return value;
}
value = stringKeyRedisTemplate.opsForValue().get(cacheKey);
if (value != null) {
logger.debug("get cache from redis and put in guava, the key is : {}", cacheKey);
loadingCache.put(key, value);
}
return value;
}
private Object getKey(Object key) {
return this.name.concat(":")
.concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
}
private long getExpire() {
long expire = defaultExpiration;
Long cacheNameExpire = expires.get(this.name);
return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
}
/**
* @description 缓存变更时通知其他节点清理本地缓存
*/
private void push(CacheMessage message) {
stringKeyRedisTemplate.convertAndSend(topic, message);
}
/**
* @description 清理本地缓存
*/
public void clearLocal(Object key) {
logger.debug("clear local cache, the key is : {}", key);
if (key == null) {
loadingCache.invalidateAll();
} else {
loadingCache.invalidate(key);
}
}
}
org.springframework.data.redis.connection.MessageListener
,由于多节点部署,本地缓存可能会出现不一致,这个时候需要监听redis中缓存的改变,这里底层用的是redis的发布订阅模式。public class CacheMessageListener implements MessageListener {
private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);
private RedisTemplate redisTemplate;
private RedisGuavaCacheManager redisGuavaCacheManager;
public CacheMessageListener(RedisTemplate redisTemplate,
RedisGuavaCacheManager redisGuavaCacheManager) {
super();
this.redisTemplate = redisTemplate;
this.redisGuavaCacheManager = redisGuavaCacheManager;
}
@Override
public void onMessage(Message message, byte[] pattern) {
CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
logger.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey());
redisGuavaCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
}
}
由于篇幅问题,这里的整合不一一指出,只是把核心代码给了。具体的可以看我的github。
public class CacheRedisGuavaService {
private final Logger logger = LoggerFactory.getLogger(CacheRedisGuavaService.class);
@Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager", sync = true)
public UserVO get(long id) {
logger.info("get by id from db");
UserVO user = new UserVO();
user.setId(id);
user.setName("name" + id);
user.setCreateTime(new Date());
return user;
}
@Cacheable(key = "'cache_user_name_' + #name", value = "userNameCache", cacheManager = "cacheManager")
public UserVO get(String name) {
logger.info("get by name from db");
UserVO user = new UserVO();
user.setId(new Random().nextLong());
user.setName(name);
user.setCreateTime(new Date());
return user;
}
@CachePut(key = "'cache_user_id_' + #userVO.id", value = "userIdCache", cacheManager = "cacheManager")
public UserVO update(UserVO userVO) {
logger.info("update to db");
userVO.setCreateTime(new Date());
return userVO;
}
@CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
public void delete(long id) {
logger.info("delete from db");
}
}
public class CacheRedisGuavaController {
@Resource
private CacheRedisGuavaService cacheRedisGuavaService;
@GetMapping("id/{id}")
public UserVO get(@PathVariable long id) {
return cacheRedisGuavaService.get(id);
}
@GetMapping("name/{name}")
public UserVO get(@PathVariable String name) {
return cacheRedisGuavaService.get(name);
}
@GetMapping("update/{id}")
public UserVO update(@PathVariable long id) {
UserVO user = cacheRedisGuavaService.get(id);
cacheRedisGuavaService.update(user);
return user;
}
@GetMapping("delete/{id}")
public void delete(@PathVariable long id) {
cacheRedisGuavaService.delete(id);
}
}
spring.redis.host=192.168.56.121
spring.redis.port=6379
spring.cache.multi.guava.expireAfterAccess=10000
spring.cache.multi.redis.defaultExpiration=60000
spring.cache.cache-names=userIdCache,userNameCache
测试controller,即可看到缓存的写入与更新。
本篇博客介绍了springBoot中缓存的一些使用方法,如何在开发中使用二级缓存?希望起到抛砖引玉的作用。
spring cache原理