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

本文涉及的产品
云数据库 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
相关文章
|
5天前
|
存储 缓存 NoSQL
【Go语言专栏】Go语言中的Redis操作与缓存应用
【4月更文挑战第30天】本文探讨了在Go语言中使用Redis进行操作和缓存应用的方法。文章介绍了Redis作为高性能键值存储系统,用于提升应用性能。推荐使用`go-redis/redis`库,示例代码展示了连接、设置、获取和删除键值对的基本操作。文章还详细阐述了缓存应用的步骤及常见缓存策略,包括缓存穿透、缓存击穿和缓存雪崩的解决方案。利用Redis和合适策略可有效优化应用性能。
|
8天前
|
存储 缓存 NoSQL
Redis多级缓存指南:从前端到后端全方位优化!
本文探讨了现代互联网应用中,多级缓存的重要性,特别是Redis在缓存中间件的角色。多级缓存能提升数据访问速度、系统稳定性和可扩展性,减少数据库压力,并允许灵活的缓存策略。浏览器本地内存缓存和磁盘缓存分别优化了短期数据和静态资源的存储,而服务端本地内存缓存和网络内存缓存(如Redis)则提供了高速访问和分布式系统的解决方案。服务器本地磁盘缓存因I/O性能瓶颈和复杂管理而不推荐用于缓存,强调了内存和网络缓存的优越性。
26 1
|
8天前
|
NoSQL Java 关系型数据库
【Redis系列笔记】分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
29 2
|
3天前
|
监控 NoSQL 算法
探秘Redis分布式锁:实战与注意事项
本文介绍了Redis分区容错中的分布式锁概念,包括利用Watch实现乐观锁和使用setnx防止库存超卖。乐观锁通过Watch命令监控键值变化,在事务中执行修改,若键值被改变则事务失败。Java代码示例展示了具体实现。setnx命令用于库存操作,确保无超卖,通过设置锁并检查库存来更新。文章还讨论了分布式锁存在的问题,如客户端阻塞、时钟漂移和单点故障,并提出了RedLock算法来提高可靠性。Redisson作为生产环境的分布式锁实现,提供了可重入锁、读写锁等高级功能。最后,文章对比了Redis、Zookeeper和etcd的分布式锁特性。
30 16
探秘Redis分布式锁:实战与注意事项
|
3天前
|
缓存 NoSQL Java
优化Redis缓存:解决性能瓶颈和容量限制
优化Redis缓存:解决性能瓶颈和容量限制
10 0
|
3天前
|
存储 缓存 NoSQL
Redis缓存满了怎么办?
选择哪种方法取决于您的应用需求和数据访问模式。需要根据实际情况来决定如何处理Redis缓存满的情况。
14 1
|
4天前
|
XML 存储 缓存
Spring缓存是如何实现的?如何扩展使其支持过期删除功能?
总之,Spring的缓存抽象提供了一种方便的方式来实现缓存功能,并且可以与各种缓存提供商集成以支持不同的过期策略。您可以根据项目的具体需求选择适合的方式来配置和扩展Spring缓存功能。
9 0
|
4天前
|
缓存 NoSQL Java
springboot业务开发--springboot集成redis解决缓存雪崩穿透问题
该文介绍了缓存使用中可能出现的三个问题及解决方案:缓存穿透、缓存击穿和缓存雪崩。为防止缓存穿透,可校验请求数据并缓存空值;缓存击穿可采用限流、热点数据预加载或加锁策略;缓存雪崩则需避免同一时间大量缓存失效,可设置随机过期时间。文章还提及了Spring Boot中Redis缓存的配置,包括缓存null值、使用前缀和自定义过期时间,并提供了改造代码以实现缓存到期时间的个性化设置。
|
4天前
|
NoSQL Java 大数据
介绍redis分布式锁
分布式锁是解决多进程在分布式环境中争夺资源的问题,与本地锁相似但适用于不同进程。以Redis为例,通过`setIfAbsent`实现占锁,加锁同时设置过期时间避免死锁。然而,获取锁与设置过期时间非原子性可能导致并发问题,解决方案是使用`setIfAbsent`的超时参数。此外,释放锁前需验证归属,防止误删他人锁,可借助Lua脚本确保原子性。实际应用中还有锁续期、重试机制等复杂问题,现成解决方案如RedisLockRegistry和Redisson。
|
4天前
|
缓存 NoSQL 搜索推荐
Redis缓存雪崩穿透等解决方案
本文讨论了缓存使用中的三个问题:缓存穿透、缓存击穿和缓存雪崩。为解决这些问题,提出了相应策略。对于缓存穿透,建议数据校验和缓存空值;缓存击穿可采用监控扩容、服务限流或加锁机制;缓存雪崩则需避免大量缓存同时过期,可设置随机过期时间。此外,文章还介绍了Spring Boot中Redis缓存配置,包括全局设置及自定义缓存过期时间的方法。