玩转Spring Cache --- 整合分布式缓存Redis Cache(使用Lettuce、使用Spring Data Redis)【享学Spring】(下)

简介: 玩转Spring Cache --- 整合分布式缓存Redis Cache(使用Lettuce、使用Spring Data Redis)【享学Spring】(下)
RedisCacheWriter

RedisCacheWrite它有Spring内建唯一实现类DefaultRedisCacheWriter,并且这个类是内建的非public的:


// @since 2.0
public interface RedisCacheWriter {
  // 两个静态方法用于创建一个 有锁/无锁的RedisCacheWriter
  // 它内部自己实现了一个分布式锁的效果~~~~  Duration可以在去获取锁没获取到的话,睡一会再去获取(避免了频繁对redis的无用访问)
  static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
    return new DefaultRedisCacheWriter(connectionFactory);
  }
  static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
    return new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50));
  }
  @Nullable
  byte[] get(String name, byte[] key);
  // 注意:这两个put方法,都是带有TTL的,因为Redis是支持过期时间的嘛
  // 多疑依托于此方法,我们其实最终可以定义出支持TTL的缓存注解,下篇博文见
  void put(String name, byte[] key, byte[] value, @Nullable Duration ttl);
  @Nullable
  byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl);
  void remove(String name, byte[] key);
  void clean(String name, byte[] pattern);
}
// @since 2.0
class DefaultRedisCacheWriter implements RedisCacheWriter {
  // 一切远程操作的链接,都来自于链接工厂:RedisConnectionFactory 
  private final RedisConnectionFactory connectionFactory;
  private final Duration sleepTime;
  DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory) {
    this(connectionFactory, Duration.ZERO);
  }
  DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) {
    this.connectionFactory = connectionFactory;
    this.sleepTime = sleepTime;
  }
  // 拿到redis连接,执行命令。  当然执行之前,去获取锁
  private <T> T execute(String name, Function<RedisConnection, T> callback) {
    RedisConnection connection = connectionFactory.getConnection();
    try {
      checkAndPotentiallyWaitUntilUnlocked(name, connection);
      return callback.apply(connection);
    } finally {
      connection.close();
    }
  }
  private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
    if (!isLockingCacheWriter()) {
      return;
    }
    try {
      // 自旋:去查看锁 若为true(表示锁还cun'z)
      while (doCheckLock(name, connection)) {
        Thread.sleep(sleepTime.toMillis());
      }
    } catch (InterruptedException ex) {
      // Re-interrupt current thread, to allow other participants to react.
      Thread.currentThread().interrupt();
      throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name), ex);
    }
  }
  ... // 关于它内部的lock、unlock处理,有兴趣的可以看看此类,因为不是本文重点,所以此处不说明了
  // 其余方法,都是调用了execute方法
  @Override
  public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
    execute(name, connection -> {
      // 只有ttl合法才会执行。注意:单位MILLISECONDS  是毫秒值
      if (shouldExpireWithin(ttl)) {
        connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
      } else {
        connection.set(key, value);
      }
      return "OK";
    });
  }
  @Override
  public byte[] get(String name, byte[] key) {
    return execute(name, connection -> connection.get(key));
  }
  // putIfAbsent是根据内部的分布式锁来实现的
  @Override
  public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { 
    ... 
  }
  @Override
  public void remove(String name, byte[] key) {
    execute(name, connection -> connection.del(key));
  }
  // 注意批量删,第二参数是pattern
  @Override
  public void clean(String name, byte[] pattern) {
    ...
  }
  ...
}


整体上看,DefaultRedisCacheWriter的实现还是比较复杂的,它是2.x重写的实现。

1.x版本的RedisCache类的实现没有这么复杂,因为它依托于RedisOperations去实现,复杂度都在RedisOperations本身了。


2.x版本把注解操作缓存和RedisTempate操作缓存完全分离开了,也就是说注解缓存再也不用依赖于RedisTempate了~


为何能使用RedisCacheWriter代替RedisOperations

使用过1.x版本的都知道,创建一个RedisCacheManager实例的时候,都必须想准备一个RedisTempate实例,因为它强依赖于它。

但是如上2.0的配置可知,它现在只依赖于RedisConnectionFactory而不用再管RedisOperations了。


为何Spring要大费周章的重写一把呢?此处我可以大胆猜测一下:


  1. Cache抽象(缓存注解)只会操作k-v,不会涉足复杂的数据结构
  2. RedisOperations功能强大,能够操作任意类型。所以放在操作Cache上显得非常的笨重
  3. 重写的RedisCacheWriter完全独立不依赖,运用在Cache上,能够使得缓存注解保持非常高的独立性。并且此接口也非常的轻


Demo示例


说了这么多,是时候来些实战的代码了。这里我自己给出一个Demo供以参考:

@Service
public class CacheDemoServiceImpl implements CacheDemoService {
    @Cacheable(cacheNames = "demoCache", key = "#id")
    @Override
    public Object getFromDB(Integer id) {
        System.out.println("模拟去db查询~~~" + id);
        return "hello cache...";
    }
}
@EnableCaching // 使用了CacheManager,别忘了开启它  否则无效
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
    // 配置一个CacheManager 来支持缓存注解
    @Bean
    public CacheManager cacheManager() {
        // 1.x是这么配置的:仅供参考
        //RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        //cacheManager.setDefaultExpiration(ONE_HOUR * HOURS_IN_ONE_DAY);
        //cacheManager.setUsePrefix(true);
        // --------------2.x的配置方式--------------
        // 方式一:直接create
        //RedisCacheManager redisCacheManager = RedisCacheManager.create(redisConnectionFactory());
        // 方式二:builder方式(推荐)
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1)) //Duration.ZERO表示永不过期(此值一般建议必须设置)
                //.disableKeyPrefix() // 禁用key的前缀
                //.disableCachingNullValues() //禁止缓存null值
                //=== 前缀我个人觉得是非常重要的,建议约定:注解缓存一个统一前缀、RedisTemplate直接操作的缓存一个统一前缀===
                //.prefixKeysWith("baidu:") // 底层其实调用的还是computePrefixWith() 方法,只是它的前缀是固定的(默认前缀是cacheName,此方法是把它固定住,一般不建议使用固定的)
                //.computePrefixWith(CacheKeyPrefix.simple()); // 使用内置的实现
                .computePrefixWith(cacheName -> "caching:" + cacheName) // 自己实现,建议这么使用(cacheName也保留下来了)
                ;
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory())
                // .disableCreateOnMissingCache() // 关闭动态创建Cache
                //.initialCacheNames() // 初始化时候就放进去的cacheNames(若关闭了动态创建,这个就是必须的)
                .cacheDefaults(configuration) // 默认配置(强烈建议配置上)。  比如动态创建出来的都会走此默认配置
                //.withInitialCacheConfigurations() // 个性化配置  可以提供一个Map,针对每个Cache都进行个性化的配置(否则是默认配置)
                //.transactionAware() // 支持事务
                .build();
        return redisCacheManager;
    }


配置好后,运行单测:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {
    @Autowired
    private CacheDemoService cacheDemoService;
    @Autowired
    private CacheManager cacheManager;
    @Test
    public void test1() {
        cacheDemoService.getFromDB(1);
        cacheDemoService.getFromDB(1);
        System.out.println("----------验证缓存是否生效----------");
        Cache cache = cacheManager.getCache("demoCache");
        System.out.println(cache);
        System.out.println(cache.get(1, String.class));
    }
}


打印结果如下:


模拟去db查询~~~1
----------验证缓存是否生效----------
org.springframework.data.redis.cache.RedisCache@788ddc1f
hello cache...


并且缓存中有值如下(注意key有统一前缀):


image.png


就这样非常简单的,Redis分布式缓存就和Spring Cache完成了集成,可以优雅的使用三大缓存注解去操作了。对你有所帮助


备注:DefaultRedisCacheWriter使用的写入都是操作的Bytes,所以不会存在乱码问题~


总结


Redis作为当下互联网应用中使用最为广泛、最为流行的分布式缓存产品,相信本文叙述的应该是绝大多数小伙伴的场景吧。既然它使用得这么普遍,你是否想过怎么和你的同事拉开差距呢?


其实我这里有个不太成熟的小建议:盘它。只有你踩的坑多了,碰到的事多了,你才有足够的资本说你比你的同事懂得多,熟练得多,能够解决他们解决不了的问题。

至于有的小伙伴提到的想让每一个缓存注解也支持自定义过期时间,我觉得能有这个想法是很不错的,至于如何实现,请待下文分解~


相关文章
|
6月前
|
NoSQL Java 数据库连接
《深入理解Spring》Spring Data——数据访问的统一抽象与极致简化
Spring Data通过Repository抽象和方法名派生查询,简化数据访问层开发,告别冗余CRUD代码。支持JPA、MongoDB、Redis等多种存储,统一编程模型,提升开发效率与架构灵活性,是Java开发者必备利器。(238字)
|
6月前
|
存储 Java 关系型数据库
Spring Boot中Spring Data JPA的常用注解
Spring Data JPA通过注解简化数据库操作,实现实体与表的映射。常用注解包括:`@Entity`、`@Table`定义表结构;`@Id`、`@GeneratedValue`配置主键策略;`@Column`、`@Transient`控制字段映射;`@OneToOne`、`@OneToMany`等处理关联关系;`@Enumerated`、`@NamedQuery`支持枚举与命名查询。合理使用可提升开发效率与代码可维护性。(238字)
608 1
|
6月前
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
667 25
|
6月前
|
缓存 运维 监控
Redis 7.0 高性能缓存架构设计与优化
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Redis 7.0高性能缓存架构,探索函数化编程、多层缓存、集群优化与分片消息系统,用代码在二进制星河中谱写极客诗篇。
1157 3
|
7月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
325 1
Redis专题-实战篇二-商户查询缓存
|
7月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
存储 JSON Java
829 0
|
7月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
644 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
7月前
|
SQL Java 数据库连接
Spring Data JPA 技术深度解析与应用指南
本文档全面介绍 Spring Data JPA 的核心概念、技术原理和实际应用。作为 Spring 生态系统中数据访问层的关键组件,Spring Data JPA 极大简化了 Java 持久层开发。本文将深入探讨其架构设计、核心接口、查询派生机制、事务管理以及与 Spring 框架的集成方式,并通过实际示例展示如何高效地使用这一技术。本文档约1500字,适合有一定 Spring 和 JPA 基础的开发者阅读。
728 0
|
8月前
|
缓存 监控 Linux
Linux系统清理缓存(buff/cache)的有效方法。
总结而言,在大多数情形下你不必担心Linux中buffer与cache占用过多内存在影响到其他程序运行;因为当程序请求更多内存在没有足够可用资源时,Linux会自行调整其占有量。只有当你明确知道当前环境与需求并希望立即回收这部分资源给即将运行重负载任务之前才考虑上述方法去主动干预。
2211 10
下一篇
开通oss服务