likes
comments
collection
share

caffeine_redis二级缓存实现Spring接口

作者站长头像
站长
· 阅读数 19

背景

项目最近在加缓存,因为项目中没有插入或更新操作,所有数据更新都是数据人员通过流程更新。所以使用切面自定义注解实现是比较合适的。参考:juejin.cn/post/722320…

现在在来实现通用型的二级缓存,就是项目里面查询、插入、更新什么都有。并且是实现 Spring 规范的接口。

介绍

在学习的过程中, 发现 JSR107/JCache 缓存,都没听说过,还是学习的时候了解下的。现在就不细讲了,主打的就是一个 Spring 整合。提供一系列接口规范,所有厂商都来实现它即可。和 JDBC 驱动一个样。

下图是 Spring Cache 抽象相关类的层次结构图:

caffeine_redis二级缓存实现Spring接口

  • 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

  1. 使用一个 ConcurrentHashMap 保存已经创建好的 Cache
  2. 没有的话,就调用 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

  1. 里面重写了 RedisTemplate,否则保存的数据不可视化,很难受。
  2. 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 即可。

完整代码:gitee.com/ncharming/r…