自定義實現spring cache 整合 guava 、 redis 兩級緩存

在這裏插入圖片描述

前言

緩存在開發中是一個必不可少的優化點,近期在公司的項目重構中,關於緩存優化了很多點,比如在加載一些數據比較多的場景中,會大量使用緩存機制提高接口響應速度,簡介提升用戶體驗。關於緩存,很多人對它都是既愛又恨,愛它的是:它能大幅提升響應效率,恨的是它如果處理不好,沒有用好比如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中緩存的一些使用方法,如何在開發中使用二級緩存?希望起到拋磚引玉的作用。

參考

spring cache原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章