Redis中消息队列的四种实现方式: List 方式、ZSet 方式、发布订阅者模式、Stream 方式
其中发布订阅者模式不支持消息持久化、而其他三种方式支持持久化,并且Stream方式支持消费者确认
list 方式
List类型实现的方式最为简单和直接
通过lpush、rpop 存入和读取实现消息队列的,如下图所示:
lpush可以把最新的消息存储到消息队列(List 集合)的首部
rpop可以读取消息队列的尾部,这样就实现了先进先出,如下图所示:
127.0.0.1:6379> lpush myMq "test1" "test2" "test3"
(integer) 3
127.0.0.1:6379> rpop myMq
"test1"
127.0.0.1:6379> lpush myMq "test4"
(integer) 3
127.0.0.1:6379> rpop myMq
"test2"
127.0.0.1:6379> rpop myMq
"test3"
127.0.0.1:6379> rpop myMq
"test4"
127.0.0.1:6379> rpop myMq
(nil)
127.0.0.1:6379>
lpush 可以一次放入多个element ,但是rpop一次却只能获取一个最先存放进来的数据.
List 做为消息队列的优缺点
- 优点 : 使用List实现消息队列的优点是消息可以被持久化
List可以借助Redis本身的持久化功能,AOF或者是RDB或混合持久化的方式用于把数据保存至磁盘,这样当Redis重启之后,消息不会丢失- 缺点, 但使用List同样存在一-定的问题 比如消息不支持重复消费、没有按照主题订阅的功能、不支持消费消息确认等.
ZSet 方式
ZSet实现消息队列的方式和List类似,利用zadd和zrangebyscore来实现存入和读取消息的
但ZSet的实现方式更为复杂一些,因为ZSet多了一个分值(score) 属性,我们可以使用它来实现更多的功能比如用它来存储时间戳,以此来实现延迟消息队列等.
ZSet同样具备持久化的功能,List 存在的问题它也同样存在,使用ZSet还不能存储相同元素的值
因为它是有序集合,有序集合的存储元素值是不能重复的
但分值可以重复,也就是说当消息值重复时,只能存储一条信息在ZSet中.
Redis 2.0之后新增了专门的发布和订阅的类型
- 发布消息 publish channel “message”
- 订阅消息 subscribe channel
发布订阅模式的优点很明显,但同样存在以下3个问题:
●无法持久化保存消息
如果Redis服务器宕机或重启,那么所有的消息将会丢失.
●发布订阅模式是“发后既忘”的工作模式
如果有订阅者离线重连之后就不能消费之前的历史消息
●不支持消费者确认机制,稳定性不能得到保证
例如当消费者获取到消息之后,还没来得及执行就宕机了
因为没有消费者确认机制,Redis 就会误以为消费者已经执行了
因此就不会重复发送未被正常消费的消息了
这样整体的Redis稳定性就被没有办法得到保障了
Stream类型
使用Stream的xadd和xrange来实现消息的存入和读取了
并且Stream提供了xack手动确认消息消费的命令,使用命令如下:
●早期版本中比较常用的实现消息队列的方式是List、 ZSet和发布订阅者模式
Stream方式实现消息队列属于附加题,属于面试中的加分项.
public class RedisListMq {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> bConsumer()).start();
producer();
}
/**
*一直读取,数据为空时 浪费资源
*/
public static void consumer() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
while (true) {
String myMq = jedis.rpop("test");
if (StrUtil.isNotEmpty(myMq)) {
System.out.println("test = " + myMq);
}
}
}
/**
* 阻塞读取
*/
public static void bConsumer() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
while (true) {
for (String mq : jedis.brpop(0, "test")) {
if (StrUtil.isNotEmpty(mq)) {
//会打印队列名称
//mq => test
//mq => hello word!
//mq => test
//mq => hello java!
//mq => test
//mq => hello python!
System.out.println("mq => " + mq);
}
}
}
}
public static void producer() throws InterruptedException {
Jedis jedis = new Jedis("127.0.0.1", 6379);
Thread.sleep(1000);
jedis.lpush("test","hello word!");
Thread.sleep(2000);
jedis.lpush("test","hello java!");
Thread.sleep(3000);
jedis.lpush("test","hello python!");
}
}
需要注意的是在List中需要使用brpop来读取消息,而不是rpop
这样可以解决没有任务时,while -直循环浪费系统资源的问题
使用jedis 实现stream
brpop中的b是blocking的意思,表示阻塞读
也就是当队列没有数据时,它会进入休眠状态,当有数据进入队列之后
它才会“苏醒”过来执行读取任务,这样就可以解决while循环一直执行消耗系统资源的问题了
maven 中的jedis的依赖版本如下
<!-- jedis redis 的另一种客户端 -->
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
注 这个版本勉强可以实现功能,但是会报错链接超时,一直没找到合适的版本
public class StreamGroupExample {
public static final String STREAM_KEY="myMq";
public static final String GROUP_NAME="g1 ";
public static final String CONSUMER_NAME="c1";
public static final String CONSUMER2_NAME="c2";
public static void main(String[] args) {
//生产者
producer();
// 创建分组
createGroup(STREAM_KEY,GROUP_NAME);
// 消费者1
new Thread(()->consumer1()).start();
// 消费者2
new Thread(()->consumer2()).start();
}
private static void consumer2() {
Jedis jedis = new Jedis("127.0.0.1", 6379,100000);
while (true) {
Map.Entry<String, StreamEntryID> entry = new AbstractMap.SimpleImmutableEntry<>(STREAM_KEY,
StreamEntryID.UNRECEIVED_ENTRY);
// 阻塞读取一条消息(最大阻塞时间120s)
List<Map.Entry<String, List<StreamEntry>>> list = jedis.xreadGroup(GROUP_NAME, CONSUMER2_NAME, 1,
120 * 1000, true, entry);
if (list!=null && list.size()==1) {
// 读取到消息
Map<String, String> content = list.get(0).getValue().get(0).getFields(); // 消息内容
System.out.println("Consumer 2 读取到消息 ID:" + list.get(0).getValue().get(0).getID() +
" 内容:" + JSONUtil.toJsonStr(content));
}
}
}
private static void consumer1() {
Jedis jedis = new Jedis("127.0.0.1", 6379,100000);
while (true) {
Map.Entry<String, StreamEntryID> entry = new AbstractMap.SimpleImmutableEntry<>(STREAM_KEY,
StreamEntryID.UNRECEIVED_ENTRY);
// 阻塞读取一条消息(最大阻塞时间120s)
Map<String, StreamEntryID> map = new HashMap<>();
map.put(entry.getKey(),entry.getValue());
// 阻塞读取一条消息(最大阻塞时间120s)
List<Map.Entry<String, List<StreamEntry>>> list = jedis.xreadGroup(GROUP_NAME, CONSUMER_NAME, 1,
120 * 1000, true, entry);
if (list!=null && list.size()==1) {
// 读取到消息
Map<String, String> content = list.get(0).getValue().get(0).getFields(); // 消息内容
System.out.println("Consumer 1 读取到消息 ID:" + list.get(0).getValue().get(0).getID() +
" 内容:" + JSONUtil.toJsonStr(content));
}
}
}
/**
* 创建分组
* @param streamKey
* @param groupName
*/
private static void createGroup(String streamKey, String groupName) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.xgroupCreate(streamKey,groupName,new StreamEntryID(),true);
}
private static void producer() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
Map<String, String> map = new HashMap<>();
map.put("data","redis");
StreamEntryID streamEntryID = jedis.xadd(STREAM_KEY, null, map);
System.out.println("添加id成功 => " + streamEntryID);
map.put("data","java");
StreamEntryID streamEntryID2 = jedis.xadd(STREAM_KEY, null, map);
System.out.println("添加id成功 => " + streamEntryID2);
}
}
jedis.xreadGroup()方法的第五个参数noAck表示是否自动确认消息
如果设置true收到消息会自动确认(ack)消息,否则需要手动确认