这个基于redis stream 的工具类是基于RedisClient的客户端创建的,因为当初开始创建的时候考虑到可能需要分布式缓存,所以也创建了一个Redisson,但没用上。
1.目前几个其他的客户端优缺点在哪:
1、redisTemplate是基于某个具体实现的再封装,比如说springBoot1.x时,具体实现是jedis;而到了springBoot2.x时,具体实现变成了lettuce。封装的好处就是隐藏了具体的实现,使调用更简单,但是有人测试过jedis效率要10-30倍的高于redisTemplate的执行效率,所以单从执行效率上来讲,jedis完爆redisTemplate。redisTemplate的好处就是基于springBoot自动装配的原理,使得整合redis时比较简单。
2、jedis作为老牌的redis客户端,采用同步阻塞式IO,采用线程池时是线程安全的。优点是简单、灵活、api全面,缺点是某些redis高级功能需要自己封装,同时,低版本的Jedis不支持stream操作,注意检查版本!!
3、lettuce作为新式的redis客户端,基于netty采用异步非阻塞式IO,是线程安全的,优点是提供了很多redis高级功能,例如集群、哨兵、管道等,缺点是api抽象,学习成本高。lettuce好是好,但是jedis比他生得早。
4、redission作为redis的分布式客户端,同样基于netty采用异步非阻塞式IO,是线程安全的,优点是提供了很多redis的分布式操作和高级功能,缺点是api抽象,学习成本高,同时Redisson对字符串操作的支持较差,只是简单需求没必要用这个。
2.那用RedisClient的理由:
它是Spring Data Redis 中的一个核心接口。提供了一种高级抽象来与 Redis 数据库进行交互。通过连接工厂(ConnectionFactory)创建与 Redis 的连接。提供了一些高级操作,如事务处理和发布/订阅。因为需要用到消息推送消费,所以选择用RedisClient。
3.那我们平常最常用的RedisTemplate和RedisClient区别又在哪?
RedisTemplate和RedisClient都是Spring Data Redis的一部分,它们提供了一种Java对象操作Redis的模板类。RedisTemplate是RedisTemplate类,它继承自AbstractRedisTemplate类,主要封装了Redis的字符串类型操作相关的内容,包括序列化、反序列化、存储、读取等。RedisTemplate中提供了对Redis的CRUD操作的一些封装,使用起来比较简单,你只需要注入一个Redis连接对象(如Jedis连接对象),就可以通过各种方法(如读写数据的方法)直接对Redis进行操作了。
RedisClient是RedisClientInterface的子接口,Spring Data Redis对Redis的访问核心是RedisTemplate,而RedisTemplate底层实现是通过RedisConnection连接Redis进行操作的。
总之,RedisTemplate和RedisClient都是Spring Data Redis的一部分,它们提供了一种Java对象操作Redis的模板类。其中,RedisTemplate主要封装了Redis的字符串类型操作相关的内容,提供了对Redis的CRUD操作的一些封装;而RedisClient是RedisClientInterface的子接口,底层实现是通过RedisConnection连接Redis进行操作的。
4.上代码:
public class RedisStreamUtil {
private RedisCommands<String, String> redisCommands;
private RedissonClient redissonClient;
@Value("${spring.redis.host}") String host;
@Value("${spring.redis.port}") int port;
@Value("${spring.redis.password}") String password;
// 创建一个本地缓存
@Autowired
CacheManager caffeineManage;
// 配置redisCommands
public RedisStreamUtil(@Value("${spring.redis.host}") String host,
@Value("${spring.redis.port}") int port,
@Value("${spring.redis.password}") String password) {
RedisURI redisUri = RedisURI.create(host, port);
redisUri.setPassword(password);
RedisClient redisClient = RedisClient.create(redisUri);
StatefulRedisConnection<String, String> connection = redisClient.connect();
this.redisCommands = connection.sync();
this.redisCommands.select(15);
// this.redisCommands.setTimeout(Duration.ofDays(0));
// Redisson配置
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port);
config.useSingleServer().setPassword(password);
// 创建Redisson客户端
redissonClient = Redisson.create(config);
}
// 创建一个 RedisCommands 实例
public RedisCommands<String, String> getRedisCommands() {
return this.redisCommands;
}
//在redisclient中,重复进行同一个创建消费组命令,会报一个消费者已存在的错误,在某些redis客户端是不会报这个错误的,所以这里需要进行一个判断
//1.生成消费者组
public void generateGroup(String consumerGroup, String streamKey) {
//当不存在consumerGroup这个消费者组时
if (isAliveGroup(consumerGroup, streamKey)) {
// 创建消费者组,偏移量为0
redisCommands.xgroupCreate(XReadArgs.StreamOffset.from(streamKey, "0-0"), consumerGroup);
}
}
//2.生成消息并进行消费
public Boolean consumeAndProcessMessages(String streamKey, String consumerGroup, Map<String, String> dataMap) {
boolean result = true;
// 生成消息并写入Redis Stream
generateMessageAndWriteToStream(streamKey, dataMap);
// 消费消息
result = consumeMessageFromStream(streamKey, consumerGroup);
return result;
}
//生成消息并写入Redis Stream
public void generateMessageAndWriteToStream(String streamKey, Map<String, String> dataMap) {
redisCommands.xadd(streamKey, dataMap);
}
// 消费消息
public Boolean consumeMessageFromStream(String streamKey, String consumerGroup) {
List<StreamMessage<String, String>> myConsumer = redisCommands.xreadgroup(
Consumer.from(consumerGroup, "Consumer"),
//阻塞等待为无限等待,最大读取数量为10000,>意思是从最新消息开始读取
XReadArgs.Builder.count(10000).block(Duration.ofDays(0)),
XReadArgs.StreamOffset.from(streamKey, ">")
);
if (myConsumer.isEmpty()) {
throw new EmptyStackException();
}
for (StreamMessage<String, String> entry : myConsumer) {
String id = entry.getId();
Map<String, String> data = entry.getBody();
for (Map.Entry<String, String> a : data.entrySet()) {
//业务需要,这里进行的是一个对本地缓存的删除
deleteLocalDatabase(a.getValue());
}
//对消息进行消费确认
redisCommands.xack(streamKey, consumerGroup, id);
//删除这条消息
// long xdel = redisCommands.xdel(streamKey, id.toString());
// if(xdel!=1){
// return false;
// }
}
return true;
}
//这个方法就是直接返回消费流中值
public String consumeApplicationMessageFromStream(String streamKey, String consumerGroup) {
System.out.println("等待中");
List<StreamMessage<String, String>> myConsumer = redisCommands.xreadgroup(
Consumer.from(consumerGroup, "Consumer"),
XReadArgs.Builder.count(10000).block(Duration.ZERO),
XReadArgs.StreamOffset.from(streamKey, ">")
);
if (myConsumer.isEmpty()) {
return null;
}
String value = null;
for (StreamMessage<String, String> entry : myConsumer) {
String id = entry.getId();
Map<String, String> data = entry.getBody();
for (Map.Entry<String, String> a : data.entrySet()) {
value = a.getValue();
}
redisCommands.xack(streamKey, consumerGroup, id);
long xdel = redisCommands.xdel(streamKey, id);
if(xdel!=1){
return null;
}
}
return value;
}
//判断流是否存在
private boolean isStreamExists(String streamKey) {
Long exists = redisCommands.exists(streamKey);
return exists != null && exists > 0;
}
//但在redisclient中,没有一个直接判断流中是否存在某个消费组的命令,所以需要通过获取流中streamkey中消费者组信息来间接获取其中的消费组名称,这里因为定的逻辑只需要一个消费者组,所以可以直接对返回的数据进行分割取值,如果有多个消费者组,可以反其道行之,尝试删除一个不存在的消费者组中的消费者,如果消费者组不存在,则该命令会返回一个错误,如果报错,就证明这个消费者组不存在哈哈
//判断流中是否已存在消费者组
public Boolean isAliveGroup(String consumerGroup, String streamKey) {
// 1. 如果流不存在,创建流
if (!isStreamExists(streamKey)) {
HashMap<String, String> temp = new HashMap<>();
//这一部分判断流是否存在貌似不太需要,重复创建流和重复创建消费组一样,有的客户端不会报错
//因为没有办法创建一个空流,所以需要往里加一个消息初始化后再删除这个消息
//1.1对流进行初始化
temp.put("start", "stratvlaue");
String xadd = redisCommands.xadd(streamKey, temp);
redisCommands.xdel(streamKey, xadd);
}
//2. 获得流中的所有消费组
List<Object> response = null;
try {
response = redisCommands.xinfoGroups(streamKey);
} catch (RedisCommandExecutionException e) {
// 处理命令执行异常
return false;
}
if (response.isEmpty()) {
return true; // 没有消费者组,返回true
}
//3.判断消费组是否已重复
String str = response.get(0).toString();
str = str.substring(1, str.length() - 1);
String[] values = str.split(", ");
List<String> list = new ArrayList<>();
//3.1将返回的数据中的消费者组取出来
for (String value : values) {
list.add(value.trim()); // 添加到集合中,并去除首尾的空格
}
return !list.contains(consumerGroup);
}
//当需要加锁操作时可以用这个方法,重写run逻辑即可
public void protectedOperation(String lockKey, Runnable operation) {
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock();
operation.run();
} finally {
lock.unlock();
}
}
// 根据id 删除Redis缓存
public void deleteRedisCache(String key) {
redisCommands.del(key);
}
//根据key删除本地缓存
public Boolean deleteLocalDatabase(String id) {
//连接的指定的缓存实例
//这个缓存实例是自己指定生成的
//其他场景生成了本地缓存,指定存在这个实例中,所以删除时需要指定删除哪个实例中的缓存
Cache dicData = caffeineManage.getCache("dicData");
//删除对应key
boolean b = dicData.evictIfPresent(id);
return b;
}