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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 玩转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作为当下互联网应用中使用最为广泛、最为流行的分布式缓存产品,相信本文叙述的应该是绝大多数小伙伴的场景吧。既然它使用得这么普遍,你是否想过怎么和你的同事拉开差距呢?


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

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


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
119 5
|
2月前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
74 8
|
2月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
64 16
|
2月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
46 5
|
2月前
|
消息中间件 NoSQL Java
Spring Boot整合Redis
通过Spring Boot整合Redis,可以显著提升应用的性能和响应速度。在本文中,我们详细介绍了如何配置和使用Redis,包括基本的CRUD操作和具有过期时间的值设置方法。希望本文能帮助你在实际项目中高效地整合和使用Redis。
77 2
|
2月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
59 0
|
8月前
|
XML Java 数据库连接
Spring Boot的数据访问之Spring Data JPA以及Hibernate的实战(超详细 附源码)
Spring Boot的数据访问之Spring Data JPA以及Hibernate的实战(超详细 附源码)
198 0
|
4月前
|
Java 数据库连接 API
【Java笔记+踩坑】Spring Data JPA
从常用注解、实体类和各层编写方法入手,详细介绍JPA框架在增删改查等方面的基本用法,以及填充用户名日期、分页查询等高级用法。
|
5月前
|
Java Spring 数据库
怎样动动手指就能实现数据操作?Spring Data JPA背后的魔法揭秘
【8月更文挑战第31天】在Java开发中,数据库交互至关重要。传统的JDBC操作繁琐且难维护,而Spring Data JPA作为集成JPA的数据访问层解决方案,提供了CRUD等通用操作接口,显著减少代码量。通过继承`JpaRepository`,开发者能轻松实现数据的增删改查,甚至复杂查询和分页也不再困难。本文将通过示例详细介绍如何利用Spring Data JPA简化数据访问层的开发,提升代码质量和可维护性。
52 0
|
5月前
|
存储 Java 数据库