caffeine_redis二级缓存实现Spring接口
背景
项目最近在加缓存,因为项目中没有插入或更新操作,所有数据更新都是数据人员通过流程更新。所以使用切面自定义注解实现是比较合适的。参考:juejin.cn/post/722320…
现在在来实现通用型的二级缓存,就是项目里面查询、插入、更新什么都有。并且是实现 Spring 规范的接口。
介绍
在学习的过程中, 发现 JSR107/JCache 缓存,都没听说过,还是学习的时候了解下的。现在就不细讲了,主打的就是一个 Spring 整合。提供一系列接口规范,所有厂商都来实现它即可。和 JDBC 驱动一个样。
下图是 Spring Cache 抽象相关类的层次结构图:
- CacheManager:缓存管理器,管理各种缓存(Cache)组件
- Cache:为缓存的组件规范定义,包含缓存的各种操作。它有很多实现,比如 RedisCache、CaffeineCache等。
实现
既然上面的层次结构图清楚了,那么我们就逐个实现即可。
因为 Spring 中提供了 AbstractValueAdaptingCache 抽象接口,对 Cache 做了简单的封装,所以我们不用实现 Cache 接口,继承 AbstractValueAdaptingCache 就好了。
继承 AbstractValueAdaptingCache
因为涉及到 Cache、Redis 整合,所以属性肯定有这两个。然后为了在 yml 配置文件中可配置,写了个Properties 类
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
private Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);
private String cacheName;
private RedisTemplate<Object, Object> redisTemplate;
private Cache<Object, Object> caffeineCache; //使用 Caffeine 作为本地缓存
private RedisCaffeineProperties redisCaffeineProperties;
protected RedisCaffeineCache(boolean allowNullValues) {
super(allowNullValues);
}
public RedisCaffeineCache(String cacheName,
RedisTemplate<Object, Object> redisTemplate,
Cache<Object, Object> caffeineCache,
RedisCaffeineProperties redisCaffeineProperties) {
super(redisCaffeineProperties.getAllowNull());
this.cacheName = cacheName;
this.redisTemplate = redisTemplate;
this.caffeineCache = caffeineCache;
this.redisCaffeineProperties = redisCaffeineProperties;
}
xxxx
lookup
使用 @Cacheable 注解的时候,会执行这个查询逻辑。描述起来很简单,实现也不难
- 从本地缓存 Caffeine 取数,有就直接返回
- 从 Redis 取数,有就插入本地缓存,并返回
- 都没有则运行程序本来的逻辑,并执行 put 方法
可以说 lookup 与 put 方法是相辅相成的。
@Override
protected Object lookup(Object key) {
// 先从caffeine中查找
Object obj = caffeineCache.getIfPresent(key);
if (Objects.nonNull(obj)) {
logger.info("get data from caffeine");
return obj; //不用fromStoreValue,否则返回的是null,会再查数据库
}
//再从redis中查找
String redisKey = this.cacheName + ":" + key;
obj = redisTemplate.opsForValue().get(redisKey);
if (Objects.nonNull(obj)) {
logger.info("get data from redis");
caffeineCache.put(key, obj);
}
return obj;
}
put
当有更新操作或者像上面那样两个缓存都没有数据的时候会调用 put 方法。
- 先保存 caffeine ,再保存 Redis ,如果为 null 就保存在 caffeine 不用保存 Redis ,防止缓存穿透。
- 通知其他节点更新缓存。
@Override
public void put(Object key, Object value) {
if (!isAllowNullValues() && Objects.isNull(value)) {
logger.error("the value NULL will not be cached");
return;
}
//使用 toStoreValue(value) 包装,解决 caffeine 不能存null的问题
//caffeineCache.put(key,value);
caffeineCache.put(key, toStoreValue(value));
if (Objects.isNull(value)){ //null 对象不用存 redis 了
logger.info("null 对象不用存 redis 了");
return;
}
String redisKey = this.cacheName + ":" + key;
redisTemplate.opsForValue().set(redisKey, toStoreValue(value),
redisCaffeineProperties.getRedisExpire(), TimeUnit.SECONDS);
//发送信息通知其他节点更新一级缓存
//同样,空对象不会给其他节点发送信息
redisTemplate.convertAndSend(MessageConfig.TOPIC, cacheMassage);
}
evict
删除操作会调用,就是直接删除当前缓存数据,并通知其他节点删除
@Override
public void evict(Object key) {
redisTemplate.delete(this.cacheName + ":" + key);
caffeineCache.invalidate(key);
redisTemplate.convertAndSend(MessageConfig.TOPIC, cacheMassage);
}
clear
和 evit 一样。只不过它删除所有的缓存。
这里需要注意 redis.keys 命令一般生产环境是禁用的,所以我们需要使用 scan 替换下。
@Override
public void clear() {
//如果是正式环境,避免使用keys命令 ;而且一般正式环境不让用
Set<Object> keys = redisTemplate.keys(this.cacheName.concat(":*"));
for (Object key : keys) {
redisTemplate.delete(String.valueOf(key));
}
caffeineCache.invalidateAll();
redisTemplate.convertAndSend(MessageConfig.TOPIC, cacheMassage);
}
get
使用注解时不走这个方法,实际走父类的 get 方法。
且这个方法,父类要求实现此接口的时候需要自己保证同步
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
ReentrantLock lock = new ReentrantLock();
lock.lock();//加锁 这里推荐在 try 块前面马上加锁
try {
Object obj = lookup(key);
if (Objects.nonNull(obj)) {
return (T) obj;
}
//没有找到 ,执行程序逻辑
obj = valueLoader.call();
//放入缓存
put(key, obj);
return (T) obj;
} catch (Exception e) {
logger.error(e.getMessage());
} finally {
lock.unlock();
}
return null;
}
实现 CacheManager
缓存管理器的实现,当 debug 的时候就会发现,每次注解起作用都会先走这个类里面的方法,后面才会走 Cache 里的方法。这当然也是显而易见,因为他是 Cache 的管理器,先从管理器里面拿到 Cache 才能进行具体的操作。
@Configuration
public class RedisCaffeineCacheManager implements CacheManager {
Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
private RedisCaffeineProperties redisCaffeineProperties;
XXXX
getCache
- 使用一个 ConcurrentHashMap 保存已经创建好的 Cache
- 没有的话,就调用 Cache 创建一个
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if (Objects.nonNull(cache)) {
return cache;
}
cache = new RedisCaffeineCache(name, redisTemplate, createCaffeineCache(), redisCaffeineProperties);
Cache oldCache = cacheMap.putIfAbsent(name, cache);
return oldCache == null ? cache : oldCache;
}
分布式
因为有本地缓存,不像 Redis 那样,天然满足分布式。当有变动的时候,我们还需要通知其他节点做相应的变动。
MessageConfig
- 里面重写了 RedisTemplate,否则保存的数据不可视化,很难受。
- RedisMessageListenerContainer 注册了监听容器,并实现了监听到变动后的逻辑
@Configuration
public class MessageConfig {
public static final String TOPIC="cache.msg";
@Bean
RedisMessageListenerContainer container(MessageListenerAdapter listenerAdapter,
RedisConnectionFactory redisConnectionFactory){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic(TOPIC));
return container;
}
@Bean
MessageListenerAdapter adapter(RedisMessageReceiver receiver){
return new MessageListenerAdapter(receiver,"receive");
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
测试
简单搞个 Controller 测试下各个注解搭配使用是否正常。
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private TestMapper testMapper;
@GetMapping("get/{id}")
@Cacheable(cacheNames = "test",key = "#id")
public Object get(@PathVariable("id") Long id){
return testMapper.getById(id);
}
@GetMapping("update/{name}")
@CachePut(cacheNames = "test",key = "#name")
public void updateOrder(@PathVariable("name") String name){
testMapper.updateById(name);
}
@GetMapping("delete/{id}")
@CacheEvict(cacheNames = "test",key = "#id")
public void deleteOrder(@PathVariable("id") Long id) {
testMapper.deleteById(id);
}
@GetMapping("query/{id}")
public Object getOrderById2(@PathVariable("id") Long id) {
RedisCaffeineCacheManager cacheManager = SpringContextUtil.getBean(RedisCaffeineCacheManager.class);
Cache cache = cacheManager.getCache("test");
Object order = cache.get(id, (Callable<Object>) () -> {
System.out.println("get data from database");
return testMapper.getOrderById2(id);
});
return order;
}
总结
- 继承 AbstractValueAdaptingCache 类实现本地缓存的具体操作
- 实现 CacheManager 管理 Cache
- 使用 Redis 的发布订阅功能,实现简单的各节点通信,项目中因为对数据丢失不敏感,且项目中不会存在更新动作,所以使用 Redis 即可。
转载自:https://juejin.cn/post/7223692719507931173