Giter Club home page Giter Club logo

multilevel-cache-spring-boot-starter's Introduction

为什么多级缓存

缓存的引入是现在大部分系统所必须考虑的

  • redis 作为常用中间件,虽然我们一般业务系统(毕竟业务量有限)不会遇到如下图 在随着 data-size 的增大和数据结构的复杂的造成性能下降,但网络 IO 消耗会成为整个调用链路中不可忽视的部分。尤其在 微服务架构中,一次调用往往会涉及多次调用 例如pig oauth2.0 的 client 认证

  • Caffeine 来自未来的本地内存缓存,性能比如常见的内存缓存实现性能高出不少详细对比

综合所述:我们需要构建 L1 Caffeine JVM 级别内存 , L2 Redis 内存。

设计难点

目前大部分应用缓存都是基于 Spring Cache 实现,基于注释(annotation)的缓存(cache)技术,存在的问题如下:

  • Spring Cache 仅支持 单一的缓存来源,即:只能选择 Redis 实现或者 Caffeine 实现,并不能同时使用。
  • 数据一致性:各层缓存之间的数据一致性问题,如应用层缓存和分布式缓存之前的数据一致性问题。
  • 缓存过期:Spring Cache 不支持主动的过期策略

业务流程

如何使用

版本 支持
3.0.0 适配 SpringBoot3.x
1.0.1 适配 SpringBoot2.x
    1. 引入依赖
<dependency>
    <groupId>com.pig4cloud.plugin</groupId>
    <artifactId>multilevel-cache-spring-boot-starter</artifactId>
    <version>${lastVersion}</version>
</dependency>
    1. 开启缓存支持
@EnableCaching
public class App {
	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}
}
    1. 目标接口声明 Spring Cache 注解
@Cacheable(value = "get",key = "#key")
@GetMapping("/get")
public String get(String key){
    return "success";
}

性能比较

为保证性能 redis 在 127.0.0.1 环路安装

  • OS: macOS Mojave
  • CPU: 2.3 GHz Intel Core i5
  • RAM: 8 GB 2133 MHz LPDDR3
  • JVM: corretto_11.jdk
Benchmark Mode Cnt Score Units
多级实现 thrpt 2 2716.074 ops/s
默认 redis thrpt 2 1373.476 ops/s

代码原理

    1. 自定义 CacheManager 多级缓存实现
public class RedisCaffeineCacheManager implements CacheManager {

	@Override
	public Cache getCache(String name) {
		Cache cache = cacheMap.get(name);
		if (cache != null) {
			return cache;
		}
		cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(), cacheConfigProperties);
		Cache oldCache = cacheMap.putIfAbsent(name, cache);
		log.debug("create cache instance, the cache name is : {}", name);
		return oldCache == null ? cache : oldCache;
	}
}
    1. 多级读取、过期策略实现
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
	protected Object lookup(Object key) {
		Object cacheKey = getKey(key);

    // 1. 先调用 caffeine 查询是否存在指定的值
		Object value = caffeineCache.getIfPresent(key);
		if (value != null) {
			log.debug("get cache from caffeine, the key is : {}", cacheKey);
			return value;
		}

    // 2. 调用 redis 查询在指定的值
		value = stringKeyRedisTemplate.opsForValue().get(cacheKey);

		if (value != null) {
			log.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
			caffeineCache.put(key, value);
		}
		return value;
	}
}
    1. 过期策略,所有更新操作都基于 redis pub/sub 消息机制更新
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
	@Override
	public void put(Object key, Object value) {
		push(new CacheMessage(this.name, key));
	}

	@Override
	public ValueWrapper putIfAbsent(Object key, Object value) {
				push(new CacheMessage(this.name, key));
	}

	@Override
	public void evict(Object key) {
		push(new CacheMessage(this.name, key));
	}

	@Override
	public void clear() {
		push(new CacheMessage(this.name, null));
	}

	private void push(CacheMessage message) {
		stringKeyRedisTemplate.convertAndSend(topic, message);
	}
}
    1. MessageListener 删除指定 Caffeine 的指定值
public class CacheMessageListener implements MessageListener {

	private final RedisTemplate<Object, Object> redisTemplate;

	private final RedisCaffeineCacheManager redisCaffeineCacheManager;

	@Override
	public void onMessage(Message message, byte[] pattern) {
		CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
				cacheMessage.getCacheName(), cacheMessage.getKey());
		redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
	}
}

源码地址

https://github.com/pig-mesh/multilevel-cache-spring-boot-starter

multilevel-cache-spring-boot-starter's People

Contributors

aeizzz avatar chunmenglu avatar flyinwind1 avatar lishangbu avatar lltx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

multilevel-cache-spring-boot-starter's Issues

关于RedisCaffeineCache里的get(Object key, Callable<T> valueLoader)方法

public <T> T get(Object key, Callable<T> valueLoader) {
        ....
	ReentrantLock lock = keyLockMap.get(key.toString());
	if (lock == null) {
		log.debug("create lock for key : {}", key);
		lock = new ReentrantLock();
		keyLockMap.putIfAbsent(key.toString(), lock);
	}
	try {
		lock.lock();
		....
	}
	catch (Exception e) {
		throw new ValueRetrievalException(key, valueLoader, e.getCause());
	}
	finally {
		lock.unlock();
	}
}

关于下方这几句是否有线程安全问题:
lock = new ReentrantLock();
keyLockMap.putIfAbsent(key.toString(), lock);
lock.lock();

假设线程T1、T2同时获取lock为null,且都进入了
if(lock == null) 方法体。
T1、T2分别执行了lock = new ReentrantLock(); 假设T1为lock1,T2位lock2。
接着T1成功把lock1放入keyLockMap,T2放入时就会失败。
再下方的代码lock.lock(),T1和T2分别拿着自己的lock同时执行了需要同步的代码块。

下面是我的认为的解决方法

public <T> T get(Object key, Callable<T> valueLoader) {
        ....
	ReentrantLock lock = keyLockMap.get(key.toString());
	if (lock == null) {
		log.debug("create lock for key : {}", key);
		lock = new ReentrantLock();
		ReentrantLock  templock = keyLockMap.putIfAbsent(key.toString(), lock);
                lock = templock == null ? lock : templock;
	}
	try {
		lock.lock();
		....
	}
	catch (Exception e) {
		throw new ValueRetrievalException(key, valueLoader, e.getCause());
	}
	finally {
		lock.unlock();
	}
}

No CacheResolver specified

No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
at org.springframework.cache.interceptor.CacheAspectSupport.afterSingletonsInstantiated(CacheAspectSupport.java:224)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:914)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)

默认使用全局过期时间,报错

private void doPut(Object key, Object value) {
	Duration expire = getExpire();
	value = toStoreValue(value);
	if (!expire.isNegative()) {
		stringKeyRedisTemplate.opsForValue().set(getKey(key), value, expire);
	}
	else {
		stringKeyRedisTemplate.opsForValue().set(getKey(key), value);
	}

	push(new CacheMessage(this.name, key));

	caffeineCache.put(key, value);
}

在doPut方法中,默认不设置时间则采用全局过期时间,全局过期时间设置为0,默认不过期,但在这里走的是stringKeyRedisTemplate.opsForValue().set(getKey(key), value, expire);这条语句,导致报错。

建议添加根据key前缀删除缓存

  • 缓存名称:cacheName:key1-key2-key3
  • 例子:查询多条集合数据、与更新一条数据
    • 根据cacheName:key1-key2-key3(id-类型-数量)返回集合数据
    • 根据cacheName:key1-key2(id-类型)更新一条数据
  • 建议添加更新完一条数据后,设置所有缓存key前缀为cacheName:key1-key2的缓存数据失效功能

CacheMessageListner可能会将当前节点缓存删除

虽然测试中出现的概率几乎为0,但是处理的逻辑确实有可能出现这种情况:
在代码逻辑中:

push(new CacheMessage(this.name, key))
caffeineCache.put(key, value)

push(new CacheMessage(this.name, key))在CacheMessageListener的回调方法中,并没有过滤掉当前节点,可能会导致当前节点的本地caffeine缓存也删除,当然,这个情况可以说几乎不存在(因为走网络和走内存几乎是走内存快),但是更加严谨的做法,应该是处理消息时,避开当前节点

为什么redis不用hash类型存储呢?

按理说 cacheManage使用方式,用hash存储更契合,与caffeine里面的类型也更兼容
过期时间的问题,caffeine中设置的是整个cacheName的过期时间,而redis设置的是每个key的过期时间,是否会有问题呢

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.