随着时间的积累,应用的使用用户不断增加,数据规模也越来越大,往往数据库查询操作会成为影响用户使用体验的瓶颈,此时使用缓存往往是解决这一问题非常好的手段之一。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
参数。
单元测试类如下:
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数据库的查询:
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依赖,添加如下内容:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
第二步:在Spring Boot主类中增加@EnableCaching注解开启缓存功能,如下:
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching
@SpringBootApplication
public class DemospringbootApplication{
public static void main(String[] args) {
SpringApplication.run(Chapter51Application.class, args);
}
}
第三步:在数据访问接口中,增加缓存配置注解,如:
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);
}
第四步:再来执行一下单元测试,可以在控制台中输出了下面的内容
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。
import org.springframework.cache.CacheManager;
@Autowired
private CacheManager cacheManager;
使用debug模式运行单元测试,观察CacheManager中的缓存集users以及其中的User对象的缓存加深理解:
在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
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依赖
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
在Spring Boot的parent管理下,不需要指定具体版本,会自动采用Spring Boot中指定的版本号。
2,在src/main/resources目录下创建:ehcache.xml
<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实例:
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();
}
}
输出结果如下:
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 安装目录,执行以下命令:
// 将 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
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中增加相关依赖:
<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,配置文件中增加配置信息,以本地运行为例,比如:
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
这里只配了一个本地的redis,如果使用了redis集群,也就有了分布式的概念。
2.4,再执行测试用例,输出如下:
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/
RedisTemplate常用方法
Spring封装了一个比较强大的模板,也就是redisTemplate,方便在开发的时候操作Redis缓存。在Redis中可以存储String、List、Set、Hash、Zset。
RedisTemplate常用方法参考:https://blog.csdn.net/zzvar/article/details/118388897
举例:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import java.sql.SQLException;
@SpringBootApplication
@Slf4j
public class DemospringbootApplication implements CommandLineRunner {
@Autowired
RedisTemplate redisTemplate;
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
public void run(String... strings) throws SQLException {
String redisKey = "num";
Boolean hasKey = redisTemplate.hasKey(redisKey);
if (hasKey) {
Object num = redisTemplate.opsForValue().get(redisKey);
num = Integer.valueOf(num.toString()) + 1;
//在本来的基础上加1,然后存入Redis
redisTemplate.opsForValue().set(redisKey, num);
} else {
//设置num初始值为0
redisTemplate.opsForValue().set(redisKey, 0);
}
log.info("redisKey:{}, value:{}",redisKey, redisTemplate.opsForValue().get(redisKey));
}
}
使用Redis分布式锁
但是注意,当我们把这样的定时任务部署到生产环境时,为了更高的可用性,启动多个实例是必须的。当多线程有累加操作,最终我们的结果就会出现问题。解决该问题的方式很多种,比较通用的就是采用分布式锁的方式,让同类任务执行之前以分布式锁的方式来控制执行顺序:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.sql.SQLException;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
@Slf4j
public class DemospringbootApplication implements CommandLineRunner {
@Autowired
RedisTemplate redisTemplate;
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
public void run(String... strings) throws SQLException {
//1.设置加锁 没有的情况下 会返回true,说明持有到锁了;如果有了该key值,则返回false
// Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
String redisLockKey = "redisTaskLockKey";
Boolean taskLock = redisTemplate.opsForValue().setIfAbsent(redisLockKey, 1);
//2.设置锁的过期时间,防止死锁
redisTemplate.expire(redisLockKey,30*60, TimeUnit.MINUTES);
if(!taskLock){
//没有争抢(设置)到锁
log.info("没有争抢到锁");
return;
}
//3.争抢到锁后 再执行逻辑
try {
String redisKey = "num";
Boolean hasKey = redisTemplate.hasKey(redisKey);
if (hasKey) {
Object num = redisTemplate.opsForValue().get(redisKey);
num = Integer.valueOf(num.toString()) + 1;
//在本来的基础上加1,然后存入Redis
redisTemplate.opsForValue().set(redisKey, num);
} else {
//设置num初始值为0
redisTemplate.opsForValue().set(redisKey, 0);
}
log.info("redisKey:{}, value:{}",redisKey, redisTemplate.opsForValue().get(redisKey));
} catch (Exception e) {
log.error("Exception:{}", e.toString());
} finally {
//4.执行完业务逻辑后,释放锁资源 让其他的线程去争抢锁资源
redisTemplate.delete(redisLockKey);
}
}
}
使用jasypt加解密
在我们的服务中不可避免的需要使用到一些秘钥(数据库、redis等);另外打开application.properties或application.yml,比如mysql登陆密码,redis登陆密码以及第三方的密钥等等一览无余,这里介绍一个加解密组件,提高一些属性配置的安全性。
开发和测试环境还好,但生产如果采用明文配置讲会有安全问题,jasypt是一个通用的加解密库,我们可以使用它。
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
配置文件:
# 加密的密钥,测试环境可以采用在配置文件中配置,生产环境建议采用启动参数的形式传入
# 其他配置参数参考:com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties
jasypt.encryptor.password=you salt
下面的示例将密码加密后再存入redis,这样别人即使拿到redis没有我们的秘钥也无法解密:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.jasypt.encryption.StringEncryptor;
import java.sql.SQLException;
@SpringBootApplication
@Slf4j
public class DemospringbootApplication implements CommandLineRunner {
@Autowired
RedisTemplate redisTemplate;
@Autowired
private StringEncryptor encryptor;
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
public void run(String... strings) throws SQLException {
String redisKey = "password";
String value = "12345";
// 加密后存入redis
String encryptPwd = encryptor.encrypt(value);
redisTemplate.opsForValue().set(redisKey, encryptPwd);
//解密后使用
Object encrypt_value = redisTemplate.opsForValue().get(redisKey);
log.info("encrypt_value:{}", encrypt_value);
log.info("decrypt_value:{}", encryptor.decrypt(encrypt_value.toString()));
}
}
输出如下:
2023-09-17 10:32:18.251 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : String Encryptor custom Bean not found with name 'jasyptStringEncryptor'. Initializing Default String Encryptor
2023-09-17 10:32:18.260 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.algorithm, using default value: PBEWithMD5AndDES
2023-09-17 10:32:18.261 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.keyObtentionIterations, using default value: 1000
2023-09-17 10:32:18.261 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.poolSize, using default value: 1
2023-09-17 10:32:18.261 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.providerName, using default value: null
2023-09-17 10:32:18.261 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.providerClassName, using default value: null
2023-09-17 10:32:18.261 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.saltGeneratorClassname, using default value: org.jasypt.salt.RandomSaltGenerator
2023-09-17 10:32:18.262 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.ivGeneratorClassname, using default value: org.jasypt.salt.NoOpIVGenerator
2023-09-17 10:32:18.262 INFO 10064 --- [ main] c.u.j.encryptor.DefaultLazyEncryptor : Encryptor config not found for property jasypt.encryptor.stringOutputType, using default value: base64
2023-09-17 10:32:18.794 INFO 10064 --- [ main] c.e.d.DemospringbootApplication : encrypt_value:aeybIdLEcWbdt781Vz448w==
2023-09-17 10:32:18.796 INFO 10064 --- [ main] c.e.d.DemospringbootApplication : decrypt_value:12345
我们也可以看到一些其他的配置,可以通过如下配置类实现:
@Configuration
@EableEncryptableProperties
public class JasyptConfiguration() {
@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword("password");
config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setProviderName("SunJCE");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
}
使用RedisTemplate的发布订阅功能
发布订阅模式中有个重要的角色,一个是发布者Publisher,另一个订阅者Subscriber。本质上来说,发布订阅模式就是一种生产者消费者模式,Publisher负责生产消息,而Subscriber则负责消费它所订阅的消息。这种模式被广泛的应用于软硬件的系统设计中。比如:配置中心的一个配置修改之后,就是通过发布订阅的方式传递给订阅这个配置的订阅者来实现自动刷新的:
与观察者模式区别:
可以看到这里有一个非常大的区别就是:发布订阅模式在两个角色中间是一个中间角色来过渡的,发布者并不直接与订阅者产生交互。
回想一下生产者消费者模式,这个中间过渡区域对应的就是是缓冲区。因为这个缓冲区的存在,发布者与订阅者的工作就可以实现更大程度的解耦。发布者不会因为订阅者处理速度慢,而影响自己的发布任务,它只需要快速生产即可。而订阅者也不用太担心一时来不及处理,因为有缓冲区在,可以一点点排队来完成(也就是我们常说的“削峰填谷”效果)。
而我们所熟知的RabbitMQ、Kafka、RocketMQ这些中间件的本质其实就是实现发布订阅模式中的这个中间缓冲区。而Redis也提供了简单的发布订阅实现,当我们有一些简单需求的时候,也是可以一用的。
RedisTemplate模版类提供了发布订阅的方法。下面我们将在Spring Boot应用中,通过接口的方式实现一个消息发布者的角色,然后再写一个Service来实现消息的订阅(把接口传过来的消息内容打印处理)。
消息生产者:注入redisTemplate,用convertAndSend发送消息
消息监听类:redisConnection.subscribe进行订阅,实现MessageListener接口,重写onMessage方法。
方便起见都实现在应用主类里:
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或postman等其他工具调用接口curl localhost:8080/publish?message=hello
观察控制台,可以看到打印了收到的message参数
四、Kafaka
1 kafka介绍
Kafka 是一个分布式流媒体平台,类似于消息队列或企业消息传递系统。kafka官网:http://kafka.apache.org/.
消息中间件对比:
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
开发语言 | java | erlang | java | scala |
单机吞吐量 | 万级 | 万级 | 10万级 | 100万级 |
时效性 | ms | us | ms | ms级以内 |
可用性 | 高(主从) | 高(主从) | 非常高(分布式) | 非常高(分布式) |
功能特性 | 成熟的产品、较全的文档、各种协议支持好 | 并发能力强、性能好、延迟低 | MQ功能比较完善,扩展性佳 | 只支持主要的MQ功能,主要应用于大数据领域 |
性能较好,社区活跃度高,数据量没有那么大,优先选择功能比较完备的RabbitMQ | 可靠性要求很高的金融互联网领域,稳定性高,经历了多次阿里双11考验 | 追求高吞吐量,适合产生大量数据的互联网服务的数据收集业务 |
Kafka 基础架构:
Kafka对于zookeeper是强依赖,保存kafka相关的节点数据,所以安装Kafka之前必须先安装zookeeper
- producer:发布消息的对象称之为主题生产者(Kafka topic producer)
- partition:分区,每个主题可以创建多个分区,每个分区都由一系列有序和不可变的消息组成
- topic:Kafka将消息分门别类,每一类的消息称之为一个主题(Topic)
- consumer:订阅消息并处理发布的消息的对象称之为主题消费者(consumers)
- broker:已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker)。 消费者可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。
- record:消息,消息队列基础通信单位
- replica:副本,每个分区都有一个至多个副本存在,它的主要作用是存储保存数据,以日志(Log)对象的形式体现。副本又分为leader副本和follower副本
- offset:偏移量,每一个消息在日志文件中的位置都对应一个按序递增的偏移量,你可以理解为类似数组的存储形式
2 kafka安装与运行
(1)下载安装:
官网地址:https://kafka.apache.org/downloads
Kafka 使用 ZooKeeper 进行分布式协调,因此在安装 Kafka 之前需要先安装 ZooKeeper。Kafka 2.2.0 开始支持使用内置的ZooKeeper替代外部ZooKeeper, 所以之后的版本是不需要安装Zookeeper的,直接解压即可。
注:笔者使用JRE8和Kafka2.3.0配套, 如果JRE8配套太新的Kafka版本会有问题,导致启动时监听端口失败
(2)启动zookeeper
因为Kafka中的Broker注册,Topic注册,以及负载均衡都是在Zookeeper中管理,所以需要先启动内置的Zookeeper。打开之前下载的Kafka安装包,然后输入cmd,到命令行页面,或者直接win+r然后切换到Kafak的安装目录,通过如下命令启动Zookeeper:
.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties
当看到绑定到IP地址为0.0.0.0、端口号为2181的地址,表示ZooKeeper服务器监听在该地址,启动成功:
注:命令行窗口不能关闭,关闭后无法执行下面操作
(3)启动kafka:
新开一个命令行窗口,在之前的目录中输入启动命令
.\bin\windows\kafka-server-start.bat .\config\server.properties
注:命令行窗口不能关闭,关闭后无法执行下面操作
4)测试kafka:
在之前的目录中,新开一个命令行,进行创建名为“topic_test”的主题,其包含一个分区,只有一个副本,当打印出log表示创建成功:
.\bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic topic_test
创建一个生产者来产生数据:
.\bin\windows\kafka-console-producer.bat --broker-list localhost:9092 --topic topic_test
创建消费者消费数据:
.\bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic topic_test --from-beginning
然后在之前的生产者窗口中发送数据,消费者窗口即可消息数据,如下所示:
3 springboot集成kafka
第一步,依赖和配置
导入jar包依赖:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
application.properties配置:
spring.application.name=springboot-kafka-02
# 用于建立初始连接的broker地址
spring.kafka.bootstrap-servers=localhost:9092
# producer用到的key和value的序列化类
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.IntegerSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 默认的批处理记录数
spring.kafka.producer.batch-size=16384
# 32MB的总发送缓存
spring.kafka.producer.buffer-memory=33554432
# consumer用到的key和value的反序列化类
spring.kafka.consumer.key- deserializer=org.apache.kafka.common.serialization.IntegerDeserializer
spring.kafka.consumer.value- deserializer=org.apache.kafka.common.serialization.StringDeserializer
# consumer的消费组id
spring.kafka.consumer.group-id=spring-kafka-02-consumer
# 是否自动提交消费者偏移量
spring.kafka.consumer.enable-auto-commit=true
# 每隔100ms向broker提交一次偏移量
spring.kafka.consumer.auto-commit-interval=100ms
# 如果该消费者的偏移量不存在,则自动设置为最早的偏移量
spring.kafka.consumer.auto-offset-reset=earliest
第二步,服务器上启动kafka:
新开一个cmd命令行窗口,在klafka的目录中输入启动命令:
.\bin\windows\kafka-server-start.bat .\config\server.properties
第三步,编写生产类和消费者:
在应用主类中实现:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class DemospringbootApplication {
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
@RestController
static class KafkaProducer {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
// 发送消息
@GetMapping("/kafka/normal/{topic}/{message}")
public void sendMessage1(@PathVariable("topic") String topic, @PathVariable("message") String normalMessage) {
kafkaTemplate.send(topic, normalMessage);
}
}
@Slf4j
@Service
static public class KafkaConsumer {
// 消费监听
@KafkaListener(topics = {"topic_test"})
public void message1(ConsumerRecord<?, ?> record){
// 消费的哪个topic、partition的消息,打印出消息内容
log.info("点对点消费:"+record.topic()+"-"+record.partition()+"-"+record.value());
}
}
}
浏览器输入http://localhost:8080/kafka/normal/topic_test/hello
,可以看到打印:
2023-08-19 12:23:27.831 INFO 10216 --- [nio-8080-exec-1] o.a.k.clients.producer.KafkaProducer : [Producer clientId=producer-1] Instantiated an idempotent producer.
2023-08-19 12:23:27.848 INFO 10216 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser : Kafka version: 3.1.2
2023-08-19 12:23:27.848 INFO 10216 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser : Kafka commitId: f8c67dc3ae0a3265
2023-08-19 12:23:27.848 INFO 10216 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser : Kafka startTimeMs: 1692419007848
2023-08-19 12:23:27.860 INFO 10216 --- [ad | producer-1] org.apache.kafka.clients.Metadata : [Producer clientId=producer-1] Cluster ID: wXsiC0lKRjG0HGA6vs3TgQ
2023-08-19 12:23:27.861 INFO 10216 --- [ad | producer-1] o.a.k.c.p.internals.TransactionManager : [Producer clientId=producer-1] ProducerId set to 1 with epoch 0
2023-08-19 12:23:52.346 INFO 10216 --- [ntainer#0-0-C-1] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer-spring-kafka-02-consumer-1, groupId=spring-kafka-02-consumer] Successfully joined group with generation Generation{generationId=2, memberId='consumer-spring-kafka-02-consumer-1-18bdf562-f633-42e0-9670-04b453b87f7a', protocol='range'}
2023-08-19 12:23:52.350 INFO 10216 --- [ntainer#0-0-C-1] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer-spring-kafka-02-consumer-1, groupId=spring-kafka-02-consumer] Finished assignment for group at generation 2: {consumer-spring-kafka-02-consumer-1-18bdf562-f633-42e0-9670-04b453b87f7a=Assignment(partitions=[topic_test-0])}
2023-08-19 12:23:52.360 INFO 10216 --- [ntainer#0-0-C-1] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer-spring-kafka-02-consumer-1, groupId=spring-kafka-02-consumer] Successfully synced group in generation Generation{generationId=2, memberId='consumer-spring-kafka-02-consumer-1-18bdf562-f633-42e0-9670-04b453b87f7a', protocol='range'}
2023-08-19 12:23:52.361 INFO 10216 --- [ntainer#0-0-C-1] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer-spring-kafka-02-consumer-1, groupId=spring-kafka-02-consumer] Notifying assignor about the new Assignment(partitions=[topic_test-0])
2023-08-19 12:23:52.365 INFO 10216 --- [ntainer#0-0-C-1] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer-spring-kafka-02-consumer-1, groupId=spring-kafka-02-consumer] Adding newly assigned partitions: topic_test-0
2023-08-19 12:23:52.379 INFO 10216 --- [ntainer#0-0-C-1] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer-spring-kafka-02-consumer-1, groupId=spring-kafka-02-consumer] Setting offset for partition topic_test-0 to the committed offset FetchPosition{offset=3, offsetEpoch=Optional[0], currentLeader=LeaderAndEpoch{leader=Optional[cccppp:9092 (id: 0 rack: null)], epoch=absent}}
2023-08-19 12:23:52.380 INFO 10216 --- [ntainer#0-0-C-1] o.s.k.l.KafkaMessageListenerContainer : spring-kafka-02-consumer: partitions assigned: [topic_test-0]
2023-08-19 12:23:52.407 INFO 10216 --- [ntainer#0-0-C-1] .DemospringbootApplication$KafkaConsumer : 点对点消费:topic_test-0-hello
4 Apache Kafka VS Redis
-
数据流处理:Kafka是一个分布式的流处理平台,用于处理大规模的数据流。它可以处理实时数据流,支持高吞吐量和低延迟。Redis则是一个内存数据库,用于缓存数据和支持快速读写操作。
-
数据持久性:Kafka将数据持久化到磁盘中,以便在需要时进行检索和分析。Redis则将数据存储在内存中,以实现快速读写操作。但是,Redis也支持将数据持久化到磁盘中,以便在系统崩溃时恢复数据。
-
数据分发:Kafka使用发布-订阅模式,将数据分发到多个消费者。Redis则使用键值存储模式,可以将数据存储在多个节点上,以实现高可用性和负载均衡。
-
数据处理:Kafka支持流处理和批处理,可以对数据进行复杂的转换和分析。Redis则提供了一些基本的数据处理功能,如排序、过滤和聚合。
总的来说,Kafka适用于处理大规模的数据流,支持实时处理和复杂的数据转换和分析。Redis则适用于缓存数据和快速读写操作,支持高可用性和负载均衡。
五、问题记录
本地安装zookeeper测试时报错 Session 0x0 for server localhost/127.0.0.1:2181, closing socket connection:
原因:Kafka使用的3.5.1,使用JRE8和Kafka2.3.0配套就可以了, 如果JRE8配套太新的Kafka版本会有问题,导致启动时监听端口失败
参考:
https://blog.didispace.com/spring-boot-learning-25-5-5/
https://blog.csdn.net/q704013673/article/details/127123852
https://blog.csdn.net/he1234555/article/details/131238927