redis消息队列分3种
1.List : 不支持消息确认机制,不支持消息回朔
2.pubSub :不支持消息确认机制,不支持消息回朔,不支持消息持久化
3.stream :支持消息确认机制,支持消息回朔,支持消息持久化,支持消息阻塞
因此我们采用stream来处理消息队列
STREAM类型消息队列的XREADGOUP命令特点:
- 消息可回朔
- 可以多消费者争抢消息,加快消费速度
- 可以阻塞读取
- 没有消息漏读风险
- 有消息确认机制,保证消息至少被消费一次
伪代码
1.提前创建我们的消费者组
//需提前创建我们的消费者组 或者执行我们的redis指令为 : XGROP CREATE stream.orders g1 0 MKSTREAM
stringRedisTemplate.opsForStream().createGroup("stream.orders",ReadOffset.from("0"),"g1");
2.发送消息,可以采用我们的lua脚本。或者我们的java代码都可以。这里使用lua脚本,使我们的redis操作原子性
--把发消息的步骤放到我们的lua脚本里面 也可以通过java代码发送 这边需要处理该lua脚本
--发送消息 XADD stream.orders * k1 v1 k2 v2 ...
--1.1 优惠卷id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]
local orderId=ARGV[3]
--这里不止我们的发消息操作 可以是我们的扣减库存 判断用户是否重新下单 。。。
--2 数据 key
--2.1 库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2 订单key
local orderKey = 'seckill:order:' .. voucherId
--3 脚本业务
--3.1 判断库存是否充足 get stockKey
if (tonumber(redis.call('get', stockKey)) <= 0 ) then
-- 3.2 库存不足返回-1tonumber
return 1
end
-- 3.2 判断用户是否重复下单 SISMEMBER orderKey
if (redis.call('sismember',orderKey,userId) == 1) then
--3.3 存在说明是重复下单 返回2
return 2
end
--4 扣减库存 incrby stockKey -1
redis.call('incrby',stockKey,-1)
-- 5 下单保存用户 sadd orderKey userId
redis.call('sadd',orderKey , userId)
redis.call('XADD','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId)
return 0
3.由于我们使用的是lua脚本,因此我们需要解析该lua脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT ;
//提前加载lua脚本的信息 避免平凡io操作
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
private IVoucherOrderService proxy ;
@Override //改造 redis队列消息
public Result seckillVoucher(Long voucherId) {
UserDTO user = UserHolder.getUser();
//6.1 订单id
long orderId = redisIdWorker.nextId("order");
//1 执行lua脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
user.getId().toString(),
String.valueOf(orderId));
//2 判断结果为0
int r = result.intValue();
if (r != 0){
//3 不为0,代表没有购买资格
return Result.fail(r == 1 ?"库存不足":"该用户不能重复下单");
}
//获取代理对象 这部操作主要获取我们的代理类的对象去执行我们的事务方法子线程
proxy = (IVoucherOrderService) AopContext.currentProxy();
return Result.ok(orderId);
}
4.开启一个线程,去不断的获取我们的消息,进行消费。
获取到的消息,需要ACK确认消息已经确认消费。如果没有进行ACK,那么就会进入我们的peding-list异常消息中。如果发生异常,那么就会去到我们的peding-list进行处理异常消息。
//线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct //在当前类初始化完毕后才去执行
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable{
private static final String queueName = "stream.orders";
@Override
public void run() {
while (true){
try {
//1 获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.order >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
//2 判断消息是否获取成功
if (list == null || list.isEmpty()){
//2.1 获取失败 说明没有消息继续下一次循环
continue;
}
//3 解析消息中的数据
MapRecord<String, Object, Object> entries = list.get(0);
Map<Object, Object> value = entries.getValue();//键值对
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);//让其自动转换为我们需要的对象
//2.1 获取成功 可以处理我们的业务
dowork(voucherOrder);
//2 ACK确认消息已经确认消费 SACK stream.orders g1 id
stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",entries.getId());
} catch (Exception e) {
log.error("处理业务异常",e);
HandlePendingList();
e.printStackTrace();
}
//2 创建订单
}
}
//处理异常消息 ReadOffset.from("0")
private void HandlePendingList() {
while (true) {
try {
//1 获取pending-list中的消息队列中的业务信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.order 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.from("0"))
);
//2 判断消息是否获取成功
if (list == null || list.isEmpty()) {
//2.1 获取失败 说明pending-list没有异常消息继续下一次循环
break;
}
//3 解析消息中的订单
MapRecord<String, Object, Object> entries = list.get(0);
Map<Object, Object> value = entries.getValue();//键值对
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);//让其自动转换为我们需要的对象
//2.1 获取成功 可以重新处理我们的业务
dowork(voucherOrder);
//2 ACK确认消息已经确认消费 SACK stream.orders g1 id
stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", entries.getId());
} catch (Exception e) {
log.error("处理pending-list业务异常", e);
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
}
}
Redis指令
-
添加消息指令
xadd s1 * k1
[s1:表示我们的队列,k1 :表示内容] -
创建消息组指令
XGROP CREATE s1 g1 0 MKSTREAM
[s1:表示我们的队列名 g1:表示消费者的组名 0:表示我创建了这个组,如果队列里面有消息我们就消费,如果是$符号表示s1队列的消息,我们不要 MKSTREAM:表示队列不存在时自动创建] -
从消费者组内读取消息指令
XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
[g1:表示组名 c1:表示消费者,如果没有会自动创建 1:表示一次获取一个消息查询最大数量 2000:当没有消息的时候等待2秒 s1:表示监听的队列 >:表示从下一个未消费的消息开始 ] -
确认已消费的消息指令
XACK s1 g1 1660668751543-0
【s1: 队列 g1:组名 1660668751543-0 :需确认的消息】 -
查询未确认的消息指令
XPENDING s1 g1 - + 10
【s1:队列 g1:组名】 -
XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 > 0【0:获取未确认的消息,出现异常的消息】