【黑马点评学习笔记 | 实战篇 】| 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消息队列部分的学习笔记,仅供参考,多多支持!🌹

相关推荐
涡能增压发动积19 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o19 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
swg32132119 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
tyung19 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald19 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
一轮弯弯的明月19 小时前
贝尔数求集合划分方案总数
java·笔记·蓝桥杯·学习心得
M--Y20 小时前
Redis常用数据类型
数据结构·数据库·redis
殷紫川20 小时前
深入拆解 Java 内存模型:从原子性、可见性到有序性,彻底搞懂 happen-before 规则
java·后端
元宝骑士20 小时前
FIND_IN_SET使用指南:场景、优缺点与MySQL优化策略
后端·mysql
用户319523703477120 小时前
记一次 PostgreSQL WAL 日志撑爆磁盘的排查
后端