自定义实现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原理

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值