前言
緩存在開發中是一個必不可少的優化點,近期在公司的項目重構中,關於緩存優化了很多點,比如在加載一些數據比較多的場景中,會大量使用緩存機制提高接口響應速度,簡介提升用戶體驗。關於緩存,很多人對它都是既愛又恨,愛它的是:它能大幅提升響應效率,恨的是它如果處理不好,沒有用好比如LRU這種策略,沒有及時更新數據庫的數據就會導致數據產生滯後,進而產生用戶的誤讀。
spring cache 常用註解
- @CacheConfig
這個註解的的主要作用就是全局配置緩存,比如配置緩存的名字(cacheNames),只需要在類上配置一次,下面的方法就默認以全局配置爲主,不需要二次配置,節省了部分代碼。
- @Cacheable
這個註解是最重要的,主要實現的功能再進行一個讀操作的時候。就是先從緩存中查詢,如果查找不到,就會走數據庫的執行方法,這是緩存的註解最重要的一個方法,基本上我們的所有緩存實現都要依賴於它。它具有的屬性爲cacheNames:緩存名字,condtion:緩存的條件,unless:不緩存的條件。可以指定SPEL表達式來實現,也可以指定緩存的key,緩存的內部實現一般都是key,value形式,類似於一個Map(實際上cacheable的緩存的底層實現就是concurrenHashMap),指定了key,那麼緩存就會以key作爲鍵,以方法的返回結果作爲值進行映射。
- @CacheEvict
這個註解主要是配合@Cacheable一起使用的,它的主要作用就是清除緩存,當方法進行一些更新、刪除操作的時候,這個時候就要刪除緩存。如果不刪除緩存,就會出現讀取不到最新緩存的情況,拿到的數據都是過期的。它可以指定緩存的key和conditon,它有一個重要的屬性叫做allEntries默認是false,也可以指定爲true,主要作用就是清除所有的緩存,而不以指定的key爲主。
- @CachePut
這個註解它總是會把數據緩存,而不會去每次做檢查它是否存在,相比之下它的使用場景就比較少,畢竟我們希望並不是每次都把所有的數據都給查出來,我們還是希望能找到緩存的數據,直接返回,這樣能提升我們的軟件效率。
- @cache
這個註解它是上面的註解的綜合體,包含上面的三個註解(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;
}
}
整合兩級緩存(guava、redis)
大概的流程,如下圖所示:
- 重寫
org.springframework.cache.CacheManager
public class RedisGuavaCacheManager implements CacheManager {
private final Logger logger = LoggerFactory.getLogger(RedisGuavaCacheManager.class);
private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>();
private CacheRedisGuavaProperties cacheRedisGuavaProperties;
private RedisTemplate<Object, Object> stringKeyRedisTemplate;
private boolean dynamic = true;
private Set<String> cacheNames;
public RedisGuavaCacheManager(CacheRedisGuavaProperties cacheRedisGuavaProperties,
RedisTemplate<Object, Object> stringKeyRedisTemplate) {
super();
this.cacheRedisGuavaProperties = cacheRedisGuavaProperties;
this.stringKeyRedisTemplate = stringKeyRedisTemplate;
this.dynamic = cacheRedisGuavaProperties.isDynamic();
this.cacheNames = cacheRedisGuavaProperties.getCacheNames();
}
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if (cache != null) {
return cache;
}
if (!dynamic && !cacheNames.contains(name)) {
return cache;
}
cache = new RedisGuavaCache(name, stringKeyRedisTemplate, guavaCache(name), cacheRedisGuavaProperties);
Cache oldCache = cacheMap.putIfAbsent(name, cache);
logger.debug("create cache instance, the cache name is : {}", name);
return oldCache == null ? cache : oldCache;
}
public com.google.common.cache.Cache<Object, Object> guavaCache(String cacheName) {
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
long expireAfterAccess = cacheRedisGuavaProperties.getGuava().getExpireAfterAccess();
Map<String, Long> expires = cacheRedisGuavaProperties.getGuava().getExpires();
Long cacheNameExpire = expires.get(cacheName);
long expire = cacheNameExpire == null ? expireAfterAccess : cacheNameExpire;
if (expire > 0) {
cacheBuilder.expireAfterAccess(cacheRedisGuavaProperties.getGuava().getExpireAfterAccess(),
TimeUnit.MILLISECONDS);
}
if (cacheRedisGuavaProperties.getGuava().getExpireAfterWrite() > 0) {
cacheBuilder.expireAfterWrite(cacheRedisGuavaProperties.getGuava().getExpireAfterWrite(),
TimeUnit.MILLISECONDS);
}
int initialCapacity = cacheRedisGuavaProperties.getGuava().getInitialCapacity();
Map<String, Long> capacityMap = cacheRedisGuavaProperties.getGuava().getCapacityMap();
Long capacity = capacityMap.get(cacheName);
long capacityResult = capacity == null ? initialCapacity : capacity;
if (capacityResult > 0) {
cacheBuilder.initialCapacity(cacheRedisGuavaProperties.getGuava().getInitialCapacity());
}
if (cacheRedisGuavaProperties.getGuava().getMaximumSize() > 0) {
cacheBuilder.maximumSize(cacheRedisGuavaProperties.getGuava().getMaximumSize());
}
if (cacheRedisGuavaProperties.getGuava().getRefreshAfterWrite() > 0) {
cacheBuilder.refreshAfterWrite(cacheRedisGuavaProperties.getGuava().getRefreshAfterWrite(),
TimeUnit.MILLISECONDS);
}
return cacheBuilder.build();
}
@Override
public Collection<String> getCacheNames() {
return this.cacheNames;
}
public void clearLocal(String cacheName, Object key) {
Cache cache = cacheMap.get(cacheName);
if (cache == null) {
return;
}
RedisGuavaCache redisGuavaCache = (RedisGuavaCache) cache;
redisGuavaCache.clearLocal(key);
}
}
- 重寫
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<Object, Object> stringKeyRedisTemplate;
private com.google.common.cache.Cache<Object, Object> loadingCache;
private String cachePrefix;
private long defaultExpiration = 0;
private Map<String, Long> expires;
private String topic = "cache:redis:guava:topic";
private Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap<String, ReentrantLock>();
public RedisGuavaCache(boolean allowNullValues) {
super(allowNullValues);
}
public RedisGuavaCache(String name, RedisTemplate<Object, Object> stringKeyRedisTemplate,
com.google.common.cache.Cache<Object, Object> 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> T get(Object key, Callable<T> 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<Object> 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<Object, Object> redisTemplate;
private RedisGuavaCacheManager redisGuavaCacheManager;
public CacheMessageListener(RedisTemplate<Object, Object> 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。
演示
- service 代碼編寫,跟使用spring cache 一樣,跟着之前的套路是一樣的。只不過這裏多的是本地緩存的更新策略。
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");
}
}
- controller層
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中緩存的一些使用方法,如何在開發中使用二級緩存?希望起到拋磚引玉的作用。