随着时间的积累,应用的使用用户不断增加,数据规模也越来越大,往往数据库查询操作会成为影响用户使用体验的瓶颈,此时使用缓存往往是解决这一问题非常好的手段之一。Spring 3开始提供了强大的基于注解的缓存支持,可以通过注解配置方式低侵入的给原有Spring应用增加缓存功能,提高数据访问性能。
一、进程内缓存的使用与Cache注解详解
下面使用Spring Data JPA访问MySQL一文的案例为基础。这个案例中包含了使用Spring Data JPA访问User数据的操作,我们为其添加缓存,来减少对数据库的IO,以达到访问加速的作用。
我们先对该工程做一些简单的改造。
application.properties文件中新增spring.jpa.show-sql=true
,开启hibernate对sql语句的打印。如果是1.x版本,使用spring.jpa.properties.hibernate.show_sql=true
参数。
单元测试类如下:
JAVA
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
public void test() throws Exception {
userRepository.save(new User(50, "EEE", "5000"));
// 测试findByName, 查询姓名为EEE的User
Assert.assertEquals("5000", userRepository.findByUsername("EEE").getPassword());
Assert.assertEquals("5000", userRepository.findByUsername("EEE").getPassword());
}
}
执行结果可以看到两次findByName查询都执行了两次SQL,都是对MySQL数据库的查询:
java
Hibernate: insert into user (password, username, id) values (?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from user user0_ where user0_.username=?
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from user user0_ where user0_.username=?
引入缓存
第一步:在pom.xml中引入cache依赖,添加如下内容:
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
第二步:在Spring Boot主类中增加@EnableCaching注解开启缓存功能,如下:
java
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching
@SpringBootApplication
public class DemospringbootApplication{
public static void main(String[] args) {
SpringApplication.run(Chapter51Application.class, args);
}
}
第三步:在数据访问接口中,增加缓存配置注解,如:
java
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {
@Cacheable
User findByName(String name);
}
第四步:再来执行一下单元测试,可以在控制台中输出了下面的内容
java
Hibernate: insert into user (password, username, id) values (?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from user user0_ where user0_.username=?
我们可以看到,在调用第二次findByName函数时,没有再执行select语句,也就直接减少了一次数据库的读取操作。
为了可以更好的观察缓存的存储,我们可以在单元测试中注入CacheManager。
java
import org.springframework.cache.CacheManager;
@Autowired
private CacheManager cacheManager;
使用debug模式运行单元测试,观察CacheManager中的缓存集users以及其中的User对象的缓存加深理解:
在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
java
Generic
JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
EhCache 2.x
Hazelcast
Infinispan
Couchbase
Redis
Caffeine
Simple
除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。我们也可以通过debug调试查看cacheManager对象的实例来判断当前使用了什么缓存。
当我们不指定具体其他第三方实现的时候,Spring Boot的Cache模块会使用ConcurrentHashMap来存储。而实际生产使用的时候,因为我们可能需要更多其他特性,往往就会采用其他缓存框架,所以接下来我们会介绍几个常用优秀缓存的整合与使用。
参考:https://blog.didispace.com/spring-boot-learning-21-5-1/
二、使用EhCache及缓存集群
1,在pom.xml中引入ehcache依赖
java
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
在Spring Boot的parent管理下,不需要指定具体版本,会自动采用Spring Boot中指定的版本号。
2,在src/main/resources目录下创建:ehcache.xml
java
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<cache name="users"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
</cache>
</ehcache>
完成上面的配置之后,在测试用例中加一句CacheManager的输出,再通过debug模式运行单元测试,观察此时CacheManager已经是EhCacheManager实例:
java
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.cache.CacheManager;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {
@Autowired
private CacheManager cacheManager;
@Autowired
private UserRepository userRepository;
@Test
public void test() throws Exception {
System.out.println("CacheManager type : " + cacheManager.getClass());
userRepository.save(new User(50, "EEE", "5000"));
// 测试findByName, 查询姓名为EEE的User
Assert.assertEquals("5000", userRepository.findByUsername("EEE").getPassword());
Assert.assertEquals("5000", userRepository.findByUsername("EEE").getPassword());
userRepository.deleteAllInBatch();
}
}
输出结果如下:
java
CacheManager type : class org.springframework.cache.ehcache.EhCacheCacheManager
Hibernate: select user0_.id as id1_0_0_, user0_.password as password2_0_0_, user0_.username as username3_0_0_ from user user0_ where user0_.id=?
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: insert into user (password, username, id) values (?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from user user0_ where user0_.username=?
Hibernate: delete from user
参考:
https://blog.didispace.com/spring-boot-learning-21-5-2/
https://blog.didispace.com/spring-boot-learning-21-5-3/
三、使用集中式缓存Redis
虽然EhCache已经能够适用很多应用场景,但是由于EhCache是进程内的缓存框架,在集群模式下时,各应用服务器之间的缓存都是独立的,因此在不同服务器的进程间会存在缓存不一致的情况。即使EhCache提供了集群环境下的缓存同步策略,但是同步依然是需要一定的时间,短暂的缓存不一致依然存在。
在一些要求高一致性(任何数据变化都能及时的被查询到)的系统和应用中,就不能再使用EhCache来解决了,这个时候使用集中式缓存就可以很好的解决缓存数据的一致性问题。接下来我们就来学习一下,如何在Spring Boot的缓存支持中使用Redis实现数据缓存。
1,redis下载地址:https://github.com/tporadowski/redis/releases
通过 CMD 命令行工具进入 Redis 安装目录,执行以下命令:
java
// 将 Redis 服务注册到 Windows 服务中
redis-server.exe --service-install redis.windows.conf --loglevel verbose
// 启动Redis服务
redis-server --service-start
//启动客户端
redis-cli
// 测试客户端和服务端是否成功连接,输出PING命令,若返回PONG则证明成功连接
127.0.0.1:6379> ping
PONG
// 清除redies缓存
127.0.0.1:6379> flushdb
OK
ps:在Windows系统中,Redis的配置文件名为redis.windows.conf,主要参数如下:
- 配置Redis监听的端口号,可以通过修改"port"字段来实现,默认端口号为6379。
- 配置Redis的密码,可以通过修改"requirepass"字段来实现。如果不需要密码验证,可以将该字段注释掉或者设置为空。
- 配置Redis的持久化方式,可以通过修改"save"字段来实现。该字段的值表示Redis在多长时间内执行多少次写操作后,自动将数据同步到磁盘上。
- 配置Redis的日志文件路径,可以通过修改"logfile"字段来实现。
- 配置Redis的最大内存限制,可以通过修改"maxmemory"字段来实现。该字段的值表示Redis最多可以使用的内存大小,超过该值后Redis会自动删除一些键值对以释放内存
2,项目改造:
2.1,将User 实体类继承 Serializable
要缓存的 Java 对象必须实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis
java
package com.example.demospringboot;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.io.Serializable;
@Entity
@Data
@NoArgsConstructor
public class User implements Serializable {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
}
2.2,pom.xml中增加相关依赖:
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.3,配置文件中增加配置信息,以本地运行为例,比如:
java
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=100ms
2.4,再执行测试用例,输出如下:
java
CacheManager type : class org.springframework.data.redis.cache.RedisCacheManager
Hibernate: select user0_.id as id1_0_0_, user0_.password as password2_0_0_, user0_.username as username3_0_0_ from user user0_ where user0_.id=?
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: insert into user (password, username, id) values (?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from user user0_ where user0_.username=?
Hibernate: delete from user
可以看到:
第一行输出的 CacheManager type 变为了 org.springframework.data.redis.cache.RedisCacheManager
第二次查询的时候,没有输出SQL语句,所以是走的缓存获取
参考:
https://blog.didispace.com/spring-boot-learning-21-5-4/
使用Redis的发布订阅功能
发布订阅模式中有个重要的角色,一个是发布者Publisher,另一个订阅者Subscriber。本质上来说,发布订阅模式就是一种生产者消费者模式,Publisher负责生产消息,而Subscriber则负责消费它所订阅的消息。这种模式被广泛的应用于软硬件的系统设计中。比如:配置中心的一个配置修改之后,就是通过发布订阅的方式传递给订阅这个配置的订阅者来实现自动刷新的:
与观察者模式区别:
可以看到这里有一个非常大的区别就是:发布订阅模式在两个角色中间是一个中间角色来过渡的,发布者并不直接与订阅者产生交互。
回想一下生产者消费者模式,这个中间过渡区域对应的就是是缓冲区。因为这个缓冲区的存在,发布者与订阅者的工作就可以实现更大程度的解耦。发布者不会因为订阅者处理速度慢,而影响自己的发布任务,它只需要快速生产即可。而订阅者也不用太担心一时来不及处理,因为有缓冲区在,可以一点点排队来完成(也就是我们常说的"削峰填谷"效果)。
而我们所熟知的RabbitMQ、Kafka、RocketMQ 这些中间件的本质其实就是实现发布订阅模式中的这个中间缓冲区。而Redis也提供了简单的发布订阅实现,当我们有一些简单需求的时候,也是可以一用的。
下面我们将在Spring Boot应用中,通过接口的方式实现一个消息发布者的角色,然后再写一个Service来实现消息的订阅(把接口传过来的消息内容打印处理)。方便起见都实现在应用主类里:
java
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
@SpringBootApplication
public class DemospringbootApplication {
private static String CHANNEL = "space";
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
@RestController
static class RedisController {
private RedisTemplate<String, String> redisTemplate;
public RedisController(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@GetMapping("/publish")
public void publish(@RequestParam String message) {
// 发送消息
redisTemplate.convertAndSend(CHANNEL, message);
}
}
@Slf4j
@Service
static class MessageSubscriber {
public MessageSubscriber(RedisTemplate redisTemplate) {
RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection();
redisConnection.subscribe(new MessageListener() {
@Override
public void onMessage(Message message, byte[] bytes) {
// 收到消息的处理逻辑
log.info("Receive message : " + message);
}
}, CHANNEL.getBytes(StandardCharsets.UTF_8));
}
}
}
通过curl或其他工具调用接口curl localhost:8080/publish?message=hello
观察控制台,可以看到打印了收到的message参数
Apache Kafka VS Redis
-
数据流处理:Kafka是一个分布式的流处理平台,用于处理大规模的数据流。它可以处理实时数据流,支持高吞吐量和低延迟。Redis则是一个内存数据库,用于缓存数据和支持快速读写操作。
-
数据持久性:Kafka将数据持久化到磁盘中,以便在需要时进行检索和分析。Redis则将数据存储在内存中,以实现快速读写操作。但是,Redis也支持将数据持久化到磁盘中,以便在系统崩溃时恢复数据。
-
数据分发:Kafka使用发布-订阅模式,将数据分发到多个消费者。Redis则使用键值存储模式,可以将数据存储在多个节点上,以实现高可用性和负载均衡。
-
数据处理:Kafka支持流处理和批处理,可以对数据进行复杂的转换和分析。Redis则提供了一些基本的数据处理功能,如排序、过滤和聚合。
总的来说,Kafka适用于处理大规模的数据流,支持实时处理和复杂的数据转换和分析。Redis则适用于缓存数据和快速读写操作,支持高可用性和负载均衡。
参考: