【黑马点评学习笔记 | 实战篇 】| 6-Redis消息队列

Bug如山勤为径,代码似海苦作舟。友友们好,这里是苦瓜大王。今天学习的是黑马点评项目实战篇------Redis消息队列部分的学习,之前我们对秒杀用Redisson进行了分布式锁的优化,又进行了异步秒杀优化,但是当时我们用的是JVM的阻塞队列,今天将使用Redis消息队列对异步秒杀进一步优化。笔记如下,后续会一直更新黑马点评学习过程中的笔记、问题等,请多多支持哦!

  • 之前学习了基于JVM的阻塞队列来完成异步秒杀,但是仍有缺陷
  1. 内存限制问题:在高并发情况下,大量订单需要创建时,可能导致超出JVM阻塞队列的上限
  2. 数据安全问题:JVM内存没有持久化安全机制,每当服务重启或者出现宕机的情况时,阻塞队列中的所有订单任务都会丢失
  • 这一篇就是在解决这两个问题,最佳方案是使用消息队列
  • 好处:解除耦合,提高效率

消息队列如何解决上面两个问题?

  • 消息队列是在JVM以外的独立服务,不受JVM内存的限制
  • 消息队列会对存储的数据进行持久化,服务重启和宕机之后数据也不受影响,并且会在消息投递给消费者之后做确认,如果没有得到确认,依然会存在消息队列里,下一次会继续投递让消费者处理,直到得到确认,确保消息至少被消费一次

常见的消息队列:

一、消息队列

  • Redis实现消息队列

1.基于List实现消息队列

  • 用BRPOP和BLPOP来实现,可以阻塞
bash 复制代码
#监听li这个队列,指定监听的阻塞时间为20秒
BRPOP l1 20
#往l1存入1、2
LPUSH l1 1 2 
#会弹出1
#再次获取
BRPOP l1 20
#会弹出2
#再次获取
BRPOP l1 20
#阻塞


2.基于PubSub的消息队列

  • 天生就是阻塞式的

  • 对可靠性要求较高的话,不建议使用PubSub

3.基于Stream的消息队列

  • 是一种全新的数据类型 ,是支持数据的持久化
  1. 发送消息
  2. 读取消息
bash 复制代码
#往s1中存入消息
> XADD s1 * name jack
1773903918442-0
#查看消息数量
> XLEN s1
1
#从第一条消息开始读取s1的1条消息
> XREAD count 1 streams s1 0
s1
1773903918442-0
name
jack
#读取s1的最新消息
> XREAD count 1 streams s1 $
#返回nil是因为都读过了,没有最新消息了
nil
#读最新消息,0代表永久阻塞,什么时候有新消息就什么时候结束
> xread count 1  block 0 streams s1 $
  • 可以反复读,读完了之后并不会删除消息
  1. 阻塞式循环读取最新消息
  • 可能会出现漏读消息的情况
  • 处理消息的过程中如果来了很多条消息,只能看到最新那一条
  • 下面将讲解更好的消息读取方案,防止漏读

二、Stream的消费者组模式

  • 命令

1. 创建消费者组

2. 从消费者组读取消息

  • ID配置:一般0是异常时候配置,消费了但是还没确认,>是正常情况,总是从下一个未消费的消息开始

3.确认消息

4.查看pending-list

bash 复制代码
#先插入1条数据
>XADD s1 * name jack
#创建消费者组
>XGROUP CREATE s1 s1_group 0 
#从消费者组中阻塞20秒读取下一个未被消费的信息,读出name jack
>XREADGROUP GROUP s1_group a1 COUNT 1 BLOCK 20000 STREAMS s1 >

>XREADGROUP GROUP s1_group a1 COUNT 1 BLOCK 20000 STREAMS s1 >
#此时再插入1条数据,就会读取出clour blue
>xadd s1 * clour blue

#从消费者组中阻塞20秒读取下一个未被消费的信息
>XREADGROUP GROUP s1_group a1 COUNT 1 BLOCK 20000 STREAMS s1 >
#此时再插入1条数据,就会读取出age 21
>xadd s1 * age 21

#确认前两条消费过的2条消息
>XACK s1 s1_group 1773906757092-0 1773906757094-0 
#此时再插入1条数据
xadd s1 * sex man

#从消费者组中阻塞20秒读取下一个未被消费的消息,会读出sex man
>XREADGROUP GROUP s1_group a1 COUNT 1 BLOCK 20000 STREAMS s1 >
#确认消息
>XACK s1 s1_group 【id】

#查看pending---list
>XPENDING s1 s1_group - + 10
#从消费者组中阻塞20秒读取下一个消费了但是还没确认的消息,会读出age 21
>XREADGROUP GROUP s1_group a1 COUNT 1 BLOCK 20000 STREAMS s1 >
#确认消息
>XACK s1 s1_group 【id】

5.Java代码的基本思路

6.总结

  • 解决了内存限制 (不受JVM限制)、数据安全 (持久化机制)、消息漏读 (确认机制,确保消息至少消费一次)问题

  • stream可以满足中小型企业的需求,但是如果公司业务比较庞大,对消息队列的要求更加严格,建议使用更加专业的消息队列,如RabbitMQ等,因为stream的持久化依赖于redis的持久化并不是万无一失的,还是有消息丢失的风险,并且消息确认机制只支持消费者确认,而不支持生产者的确认机制,生产者在发消息的时候丢失了就无法处理,另外还有事务机制、多消费者下的事务有序性等要解决

三、秒杀优化------基于Stream消息队列实现异步秒杀

1. 创建Stream消息队列

bash 复制代码
#直接创建消费组顺便把队列创建了
> XGROUP CREATE stream.orders g1 0 MKSTREAM
OK

2. 改造Lua脚本

  • 修改脚本,认定有抢购资格之后,向stream.orders添加消息
lua 复制代码
-- 1.参数列表  
-- 1.1.优惠券id  
local voucherId = ARGV[1]  
-- 1.2.用户id  
local userId = ARGV[2]  
-- 1.3.订单id  
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 stockKeyif(tonumber(redis.call('get', stockKey)) <= 0) then  
-- 3.2.库存不足,返回1  
return 1  
end  
-- 3.2.判断用户是否下单 SISMEMBER orderKey userIdif(redis.call('sismember', orderKey, userId) == 1) then  
-- 3.3.存在,说明是重复下单,返回2  
return 2  
end  
-- 3.4.扣库存 incrby stockKey -1redis.call('incrby', stockKey, -1)  
-- 3.5.下单(保存用户)sadd orderKey userId  
redis.call('sadd', orderKey, userId)  
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)  
return 0

3. 改造业务逻辑

  • 修改VoucherOrderServiceImpl中的seckillVoucher方法
  1. 发消息
java 复制代码
public Result seckillVoucher(Long voucherId) {  
   //1.执行Lua脚本  
    //优惠券id  
    Long userId = UserHolder.getUser().getId();  
    //订单id  
    long orderId = redisIdWorker.nextId("order");  
    Long result = stringRedisTemplate.execute(  
            SECKILL_SCRIPT,  
            Collections.emptyList(),//脚本的key参数为空,所以传一个空集合  
            voucherId.toString(),//以字符串形式传,具体看execute的参数列表  
            userId.toString(),  
            String.valueOf(orderId)  
    );  
    //2.判断结果是否为零  
    if(result.intValue() != 0)  
        //2.1不为零,没有购买资格  
        return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");  
  
  
    //3.获取代理对象(因为子线程无法获取父线程的代理)  
    //获取代理对象(初始化)  
    proxy = (IVoucherOrderService) AopContext.currentProxy();  
    // 4. 返回订单id  
    return Result.ok(orderId);  
}
  1. 获取消息完成下单
  • VoucherOrderHandler线程代码
java 复制代码
private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                    Consumer.from("g1", "c1"),
                    StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                    StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
                );
                // 2.判断订单信息是否为空
                if (list == null || list.isEmpty()) {
                    // 如果为null,说明没有消息,继续下一次循环
                    continue;
                }
                // 解析数据
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                // 3.创建订单
                createVoucherOrder(voucherOrder);
                // 4.确认消息 XACK
                stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
            } catch (Exception e) {
                log.error("处理订单异常", e);
                //处理异常消息
                handlePendingList();
            }
        }
    }

    private void handlePendingList() {
        while (true) {
            try {
                // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                    Consumer.from("g1", "c1"),
                    StreamReadOptions.empty().count(1),
                    StreamOffset.create("stream.orders", ReadOffset.from("0"))
                );
                // 2.判断订单信息是否为空
                if (list == null || list.isEmpty()) {
                    // 如果为null,说明pending-list中没有异常消息,结束循环
                    break;
                }
                // 解析数据
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                // 3.创建订单
                createVoucherOrder(voucherOrder);
                // 4.确认消息 XACK
                stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
            } catch (Exception e) {
                log.error("处理pendding-list订单异常", e);
                //担心处理太频繁
                try{
                    Thread.sleep(20);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 优化后VoucherOrderServiceImpl完整代码:
java 复制代码
@Slf4j  
@Service  
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {  
    /**  
     * 秒杀优惠券  
     * @param voucherId 优惠券id  
     * @return 订单id  
     */    @Resource  
    private ISeckillVoucherService seckillVoucherService;  
    @Resource  
    private RedisIdWorker redisIdWorker;  
    @Resource  
    private StringRedisTemplate stringRedisTemplate;  
    //注入redisson  
    @Resource  
    private RedissonClient redissonClient;  
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;  
    //在静态代码块里初始化  
//只会在类加载的时候执行一次,所以不会浪费资源  
    static {  
        SECKILL_SCRIPT = new DefaultRedisScript<>();  
        //为了避免硬编码,指定lua脚本的位置  
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));  
        //设置返回值类型为Long  
        SECKILL_SCRIPT.setResultType(Long.class);  
    }  
    /*创建阻塞队列  
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);*/    //创建线程池  
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();  
  
    //当前类初始化完毕之后就来执行  
    @PostConstruct  
    private void init() {  
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());  
    }  
  
    //创建一个runnable  
    private class VoucherOrderHandler implements Runnable {  
  
            @Override  
            public void run() {  
                while (true) {  
                    try {  
                        // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >                        
                        List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(  
                                Consumer.from("g1", "c1"),  
                                StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),  
                                StreamOffset.create("stream.orders", ReadOffset.lastConsumed())  
                        );  
                        // 2.判断订单信息是否为空  
                        if (list == null || list.isEmpty()) {  
                            // 如果为null,说明没有消息,继续下一次循环  
                            continue;  
                        }  
                        // 解析数据  
                        MapRecord<String, Object, Object> record = list.get(0);  
                        Map<Object, Object> value = record.getValue();  
                        VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);  
                        // 3.创建订单  
                        createVoucherOrder(voucherOrder);  
                        // 4.确认消息 XACK                        stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());  
                    } catch (Exception e) {  
                        log.error("处理订单异常", e);  
                        //处理异常消息  
                        handlePendingList();  
                    }  
                }  
            }  
  
            private void handlePendingList() {  
                while (true) {  
                    try {  
                        // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0                        
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(  
                                Consumer.from("g1", "c1"),  
                                StreamReadOptions.empty().count(1),  
                                StreamOffset.create("stream.orders", ReadOffset.from("0"))  
                        );  
                        // 2.判断订单信息是否为空  
                        if (list == null || list.isEmpty()) {  
                            // 如果为null,说明pending-list中没有异常消息,结束循环  
                            break;  
                        }  
                        // 解析数据  
                        MapRecord<String, Object, Object> record = list.get(0);  
                        Map<Object, Object> value = record.getValue();  
                        VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);  
                        // 3.创建订单  
                        createVoucherOrder(voucherOrder);  
                        // 4.确认消息 XACK                        stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());  
                    } catch (Exception e) {  
                        log.error("处理pendding-list订单异常", e);  
                        //担心处理太频繁  
                        try{  
                            Thread.sleep(20);  
                        }catch(Exception b){  
                            b.printStackTrace();  
                        }  
                    }  
                }  
            }  
        }  

    private void handleVoucheOrder(VoucherOrder voucherOrder) {  
        Long userId =voucherOrder.getId();  
        //锁定的范围是用户id  
        RLock lock = redissonClient.getLock("lock:order:" + userId);  
        //获取锁  
        //这一段只是兜底,其实不做也没问题  
        boolean isLock = lock.tryLock();  
        //判断是否获取成功  
        if (!isLock) {  
            //获取锁失败,输出错误  
            log.info("不允许重复下单");  
        }  
        try {  
            //拿到那个现成的代理对象  
            proxy.createVoucherOrder(voucherOrder);  
        } finally {  
            lock.unlock();  
        }  
    }  
    //成员变量,方便子线程获取  
   private IVoucherOrderService proxy;  
    public Result seckillVoucher(Long voucherId) {  
       //1.执行Lua脚本  
        //优惠券id  
        Long userId = UserHolder.getUser().getId();  
        //订单id  
        long orderId = redisIdWorker.nextId("order");  
        Long result = stringRedisTemplate.execute(  
                SECKILL_SCRIPT,  
                Collections.emptyList(),//脚本的key参数为空,所以传一个空集合  
                voucherId.toString(),//以字符串形式传,具体看execute的参数列表  
                userId.toString(),  
                String.valueOf(orderId)  
        );  
        //2.判断结果是否为零  
        if(result.intValue() != 0)  
            //2.1不为零,没有购买资格  
            return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");  
  
  
        //3.获取代理对象(因为子线程无法获取父线程的代理)  
        //获取代理对象(初始化)  
        proxy = (IVoucherOrderService) AopContext.currentProxy();  
        // 4. 返回订单id  
        return Result.ok(orderId);  
    }  
  

    @Transactional  
    public void createVoucherOrder(VoucherOrder voucherOrder) {  
        //异步的,所以不能通过ThreadLocal来获取了  
        Long userId =voucherOrder.getId();  
        //4.一人一单  
        //4.1 查询订单  
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();  
        //4.2 判断是否存在  
        if(count>0)  
            log.info("您已经购买过一次了");  
        //5. 扣减库存  
        boolean success = seckillVoucherService.update()  
                .setSql("stock= stock -1")  
                .eq("voucher_id", voucherOrder.getVoucherId())  
                .gt("stock",0)  
                .update(); //where id = ? and stock > 0  
        if(!success)  
            //扣减失败  
            log.info("库存不足");  
        //6.订单写入数据库  
        save(voucherOrder);  
    }  
}
  • 压测,查看在高并发情况下的功能、性能如何
  1. 保证了库存不会超卖、订单不会超出、一人一单
  2. 解决了集群下线程安全问题
  3. 性能不错

以上就是黑马点评实战篇------Redis消息队列部分的学习笔记,仅供参考,多多支持!🌹

相关推荐
yhole2 小时前
springboot 修复 Spring Framework 特定条件下目录遍历漏洞(CVE-2024-38819)
spring boot·后端·spring
BingoGo2 小时前
Laravel 13 正式发布 使用 Laravel AI 无缝平滑升级
后端·php
fy121632 小时前
GO 快速升级Go版本
开发语言·redis·golang
l软件定制开发工作室3 小时前
Spring开发系列教程(34)——打包Spring Boot应用
java·spring boot·后端·spring·springboot
随风,奔跑3 小时前
Spring MVC
java·后端·spring
sheeta19983 小时前
LeetCode 每日一题笔记 日期:2025.03.19 题目:3212.统计X和Y频数相等的子矩阵数量
笔记·leetcode·矩阵
美团技术团队3 小时前
美团 BI 在指标平台和分析引擎上的探索和实践
后端
czlczl200209253 小时前
Redis数据编码
数据库·redis·缓存
Wpa.wk3 小时前
pb协议接口测试
数据库·redis·缓存