在 Spring Boot 中使用 Redis 进行缓存

本文是一篇译文,原文地址

系统运行变慢,是一种很普遍的问题。即使代码非常优秀,但是在高负载下,可能也会不堪一击。缓存,是一种快速的、廉价的提升性能以及响应事件的重要方式。

简而言之,缓存是一种性能策略。调用结果被放置在内存中,因此再下次使用时,将不必再重新执行。我们可以看到,如果请求的数据再缓存中(称之为缓存命中),我们将节省很多时间和资源。红色的数据快,代表了一种糟糕的情况,即缓存缺失,这种情况下,就需要加载并重新计算,这也将增加响应时间。

更简单的说,当数据到达后,先会被放置再空桶中,当缓存满了,清理过程将按照相关算法执行。如果某些数据的使用频率很高,或者满足选择算法,这些数据是安全的(不会被清理)。反之,另外一些数据将会被清理。在理想情况下,缓存被清理只是因为数据需要更新。使用 Spring 和 Redis ,我们将尝试创建一个简单的应用,并思考不同的影响因子如何对我们的缓存层产生影响。

从代码开始

我们设想一下,如果你在做一个社交网站,用户可以在网站上创作内容,阅读次数最多的文章,最好就能够放置在缓存中。Post的结构,大概就像下面的代码块一样,非常简单,但是够用了。

1
2
3
4
5
6
7
8
9
10
11
12
13

public class Post implements Serializable {

private String id;
private String title;
private String description;
private String image;
private int shares;
private Author author;
//getters setters and constructors

}

配置和依赖

Spring 需要通过 spring-boot-started-data-redis 解决缓存依赖。

1
2
3
spring.cache.type=redis
spring.redis.host=192.168.99.100
spring.redis.port=6379

缓存抽象

Spring 框架提供了抽象层,以提供通过注解的方式做缓存支持。它可以和其它的存实现如Redis、EhCache、Hazelcast、Infinispan 等一起工作。这种解耦是非常受欢迎的。

****@Cacheable–****在方法执行完后,进行缓存,下次使用相同的参数进行调用时,其结果将从缓存中加载。

注解还提供了缓存条件。在某些情况下,不是所有的数据都需要被缓存,比如,仅仅是希望最受欢迎的文章被缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@Cacheable(value = "post-single", key = "#id", unless = "#result.shares < 500")

@GetMapping("/{id}")
public Post getPostByID(@PathVariable String id) throws PostNotFoundException {
log.info("get post with id {}", id);
return postService.getPostByID(id);
}

@Cacheable(value = "post-top")
@GetMapping("/top")
public List<Post> getTopPosts() {
return postService.getTopPosts();
}

****@CachePut–****该注解允许更新缓存中的实体,也支持一些 Cacheable 注解中的配置选项。下面的代码中,可以更新 post,并从缓存中获取到新值。

1
2
3
4
5
6
7
@CachePut(value = "post-single", key = "#post.id")
@PutMapping("/update")
public Post updatePostByID(@RequestBody Post post) throws PostNotFoundException {
log.info("update post with id {}", post.getId());
postService.updatePost(post);
return post;
}

****@CacheEvict–****从缓存中移除该实体。支持从缓存中条件删除和全部删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@CacheEvict(value = "post-single", key = "#id")
@DeleteMapping("/delete/{id}")
public void deletePostByID(@PathVariable String id) throws PostNotFoundException {
log.info("delete post with id {}", id);
postService.deletePost(id);
}



@CacheEvict(value = "post-top")
@GetMapping("/top/evict")
public void evictTopPosts() {
log.info("Evict post-top");
}

****@EnableCaching–****该注解将确保文章处理器将检查所有的 bean,并试图找到目标方法,并创建代理,以拦截所有调用。

****@Caching–****允许同时使用多个通类型注解。比如,你可能需要使用不同的条件进行缓存。

****@CacheConfig–****Class级别的注解,允许未注解指定全局值,比如 cache name 或者 key 生成器。

Redis 缓存

Redis 是一款非常流行的内存数据存储框架,可以作为数据库、消息代理或者缓存。这里,只说缓存。

可以在 docker hub 中使用 docker pull redis,新的镜像将被下载到本地仓库(可以使用 docker images 来检查以下)。当然,你也可以单独安装它。将 Redis 作为缓存,我们还需要考虑一些重要的事情,比如最大内存、回收算法和持久性。

****最大内存–****默认的,对于 64 位系统,没有内存限制,对于 32 位系统,限制是 3GB .大内存可以存储更多数据,以增强命中率。内存大小对于命中率是个非常重要的指标,但是,也不是说内存越大命中率越高,达到一定限度后,命中率变化不大。

****驱逐算法–****当缓存到达了内存限制,旧的数据需要被驱逐。当考虑回收算法时,访问策略是关键。每种缓存策略都有适用的情况:

  • ****Last Recently Used(LRU)****追踪键值上次被使用的时间。如果数据只是被使用了一次,且空置了很长时间,那么下次回收算法运行时,将被清理。

  • ****Least Frequently Used(LFU)[Redis 4.0可用]–****将记录键值被使用的次数。使用次数最多的键值,活得越久。但是如果有些数据很久以前被经常使用,且一些新的键值最近被经常使用,这样就会带来一些问题(Redis 团队处理这类问题是将长期存活的键值在一段时间不用后,减少它的 counter)。

****持久性–****出于一些特殊的原因,你可能希望能够持久化缓存。在系统启动时,通常缓存是空的。但是,系统异常中断后,使用快照数据在系统启动时进行数据恢复是非常有用的。Redis 支持三种类型的持久化:

  • RDB 支持周期性快照,或者当数据写入到一定量时自动备份。少量的数据快照不会影响性能,但是,我们还是需要在快照周期和避免系统异常中断后的数据恢复之间,寻找一个平衡。

  • AOF每次进行写操作时,都会保留持久化日志。如果你需要这种持久化策略,可以配置 appendfsync

  • RDB 和 AOF 一起使用

每次 fork 或者执行类似 fsync 的操作,都将消耗资源。因此,如果不需要该功能,关闭所有持久化配置选项。

Redis 配置

上面提到的所有方面,都是可以在 redis.conf 完成的配置。分配多少内存或者使用什么回收算法,取决于你自己。回收算法可以是即时切换的(译者注:使用CONFIG SET指令)。但是,Reids 需要一些时间来为所有 key 设置合适的算法。

量化价值

命中率,描述了缓存的效果。低命中率,是对存储数据自然本性的一个反应,很容易让人掉入到过早优化的陷阱中(译者注:项目的早期阶段,先考虑架构的优化)。

1
2
3
4
5
λ: redis-cli info stats
...
keyspace_hits:142 #Successful lookups
keyspace_misses:26 #Failed lookups
...

Redis 传递了查找的大量信息,命中率可以通过下面的公式被计算出来:

1
2
hit_ratio  = (keyspace_hits)/(keyspace_hits + keyspace_misses)
miss_ratio = (keyspace_misses)/(keyspace_hits + keyspace_misses)

延时,在请求和响应之间,最长的延时时间。当缓存过程出问题时,将会看到明显的延时时间。导致这类问题的因素有很多,比如, VM 过载等。

1
2
λ: redis-cli --latency -h 127.0.0.1 -p 6379
min: 0, max: 16, avg: 0.15 (324531 samples)...

碎片率,Redis 通常会使用的内存空间大于在 maxmemory 声明的最大空间。

  • radio < 1.0 —内存不够用了,内存分配器实际需要的内存大于你指定的内存空间。旧的数据将会被 swap 到磁盘上(译注:使用虚拟内存)。
  • radio >~1.5 —分配的内存空间超过实际使用的内存空间。
    1
    2
    3
    4
    5
    6
    7
    8
    λ: redis-cli info memory
    ...
    used_memory_human:41.22M
    ...
    used_memory_rss_human:50.01M
    ...
    mem_fragmentation_ratio:1.21 #used_memory_rss/used_memory
    ...

驱逐 key,当缓存大小超过 maxmemory,Reids 将通过选择的驱逐策略清理数据。

1
2
3
4
λ: redis-cli info stats
...
evicted_keys:14 #14 keys removed since operation time
...

注意事项

旧数据,高动态的数据很快就会变得过期。如果没有更新或过期机制,在这种情境下,缓存就会失效。
内存越大,不代表命中率越高,当内存达到一定数量,命中率就不再增加了。在这种情况下,数据回收算法和数据本身就很重要了。
不要进行过早得优化,测试一下在没有缓存时的服务性能,也许,你的恐惧是多余的(译者注:不是任何情况下都需要使用 redis,如无必要,则不用?)。“过早的优化是噩梦的根源”。
隐藏较差的性能,对于一些运行较慢的服务来说,缓存通常不是解决问题的答案。在使用缓存之前,需要做好服务优化,因为当使用缓存后,一些可能的架构上的错误就被隐藏了。
不要对外部分享你的 redis 缓存,Redis 工作在单线程中,有时,出于一些扩展的目的,可能会向第三方分享你的 redis 缓存。一些比较重的指令,如 sort 等,可能会增加运行时间,阻塞缓存运行。这类性能问题的检查,可以使用 SLOWLOG指令。
****配置参数[最大使用内存空间]****如果考虑在缓存中,使用快照备份,那么,你可以使用的最大内存空间,应小于最大内存空间的一半。
监控,应该监控你的缓存,相比于INFO,MONITOR 可以显著的减少吞吐量。


译者添加过期删除策略,引用自理解Redis的内存回收机制

过期删除策略

删除达到过期时间的key。

1、定时删除
对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间。

2、惰性删除
当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。

在计算机科学中,懒惰删除(英文:lazy deletion)指的是从一个散列表(也称哈希表)中删除元素的一种方法。在这个方法中,删除仅仅是指标记一个元素被删除,而不是整个清除它。被删除的位点在插入时被当作空元素,在搜索之时被当作已占据。

3、定期删除
每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。

在Redis中,同时使用了定期删除和惰性删除。过期删除策略
删除达到过期时间的key。

1、定时删除
对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间。

2、惰性删除
当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。

在计算机科学中,懒惰删除(英文:lazy deletion)指的是从一个散列表(也称哈希表)中删除元素的一种方法。在这个方法中,删除仅仅是指标记一个元素被删除,而不是整个清除它。被删除的位点在插入时被当作空元素,在搜索之时被当作已占据。

3、定期删除
每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。

在Redis中,同时使用了定期删除和惰性删除。

以下是译者注:
可参考链接:
Spring-5-精品翻译:缓存抽象(Cache-Abstraction)
理解Redis的内存回收机制

© 2024 YueGS