消费组从stream中获取到消息后,会分配给自己组中其中的一个消费者进行消费,消费者消费完毕,需要给消费组返回ACK,表示这条消息已经消费完毕了。
当消费者从消费组获取到消息的时候,会先把消息添加到自己的pending消息列表,当消费者给消费组返回ACK的时候,就会把这条消息从pending队列删除。(每个消费者都有自己的pending消息队列)
消费者可能没有及时的返回ACK。例如消费者消费完毕后,宕机,没有及时返回ACK,此时就会导致这条消息占用2倍的内存(stream中保存一份, 消费者的的pending消息列表中保存一份)
关于Stream的基础姿势,可以先看看这篇贴帖子
如何在Springboot中使用Redis5的Stream
开始之前,通过Redis客户端模拟一点数据
1,新打开Redis客户端(我们称之为:生产端), 创建streamm,名称叫做:my_stream
XADD my_stream * hello world
随便添加一条消息,目的是为了初始化stream
2,创建一个消费组,名称叫做:my_group
XGROUP CREATE my_stream my_group $
3,再新启动一个Redis客户端(我们称之为:消费端1),使用消费组进行阻塞消费,指定消费者:my_consumer1
XREADGROUP GROUP my_group my_consumer1 BLOCK 0 STREAMS my_stream >
4,再新启动一个Redis客户端(我们称之为:消费端2),使用消费组进行阻塞消费,指定消费者:my_consumer2
XREADGROUP GROUP my_group my_consumer2 BLOCK 0 STREAMS my_stream >
5,通过生产端,推送3条消息
XADD my_stream * message1 Hello
XADD my_stream * message2 SpringBoot
XADD my_stream * message3 Community
生产端
消费端1
消费端2
可以看到,一共Push了3条消息,它们的ID分别是
- 1605524648266-0 (message1 )
- 1605524657157-0 (message2)
- 1605524665215-0 (message3)
现在的状况是,消费者1,消费了2条消息(message1和message3),消费者2,消费了1条消息(message2)。都是消费成功了的,但是它们都还没有进行ACK。
在客户端,消费者消费到一条消息后会立即返回,需要重新执行命令,来回到阻塞状态
ACK消息
现在我们打算,把消费者1,消费的那条message1
进行ACK
XACK my_stream my_group 1605524648266-0
获取指定消费组中,待确认(ACK)的消息
查看消费组的所有待确认消息统计
127.0.0.1:6379> XPENDING my_stream my_group
1) (integer) 2 # 消费组中,所有消费者的pending消息数量
2) "1605524657157-0" # pending消息中的,最小消息ID
3) "1605524665215-0" # pending消息中的,最大消息ID
4) 1) 1) "my_consumer1" # 消费者1
2) "1" # 有1条待确认消息
2) 1) "my_consumer2" # 消费者2
2) "1" # 有2条待确认消息
查看消费者1的待确认消息详情
127.0.0.1:6379> XPENDING my_stream my_group 0 + 10 my_consumer1
1) 1) "1605524665215-0" # 待ACK消息ID
2) "my_consumer1" # 所属消费者
3) (integer) 847437 # 消息自从被消费者获取后到现在过去的时间(毫秒) - idle time
4) (integer) 1 # 消息被获取的次数 - delivery counter
这条命令,表示查询消费组my_group
中消费者my_consumer1
的opending队列,开始ID是0,结束ID是最大,最多检索10个结果。
现在的情况就是,一共3条消息,消费者1消费了2条,ack了1条。消费者2消费了1条,没有ack。消费者1和2,各自的pending队列中都有一条未ack的消息
如何实现将未被成功消费的消息获取出来重新进行消费?之前的演示,目的都是为了造一些数据,所以是用的客户端命令,从这里开始,所有的演示,都会使用spring-data-redis
中的RedisTemplate
。
遍历消费者的pending列表,读取到未ACK的消息,直接进行ACK
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.PendingMessages;
import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import io.springboot.jwt.SpringBootJwtApplication;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringBootJwtApplication.