黑马点评day05-Redis消息队列

基于JVM的阻塞队列实现的异步秒杀问题:

内存限制问题

数据安全问题

1、消息队列引入

使用队列的好处在于 **解耦:**所谓解耦,举一个生活中的例子就是:快递员(生产者)把快递放到快递柜里边(Message Queue)去,我们(消费者)从快递柜里边去拿东西,这就是一个异步,如果耦合,那么这个快递员相当于直接把快递交给你,这事固然好,但是万一你不在家,那么快递员就会一直等你,这就浪费了快递员的时间,所以这种思想在我们日常开发中,是非常有必要的。
这种场景在我们秒杀中就变成了:我们下单之后,利用redis去进行校验下单条件,再通过队列把消息发送出去,然后再启动一个线程去消费这个消息,完成解耦,同时也加快我们的响应速度。

2、基于Redis的List结构模拟消息队列(5种基本数据类型之一)

List:双向链表

基于List的消息队列有哪些优缺点?
优点:

* 利用Redis存储,不受限于JVM内存上限
* 基于Redis的持久化机制,数据安全性有保证   做数据存储的数据结构都支持持久化
* 可以满足消息有序性

缺点:

* 无法避免消息丢失
* 只支持单消费者

3、基于Redis的LPubSub结构消息队列(专门的发布订阅机制)

 

PSUBSCRIBE:pattern

 

基于PubSub的消息队列有哪些优缺点?
优点:

* 采用发布订阅模型,支持多生产、多消费

缺点:

     发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息
* 不支持数据持久化  本身就是订阅发布机制,不同于基础数据存储数据类型,故而不支持持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃
* 无法避免消息丢失   无消费者订阅,channel的消息会丢失
* 消息堆积有上限,超出时数据丢失  如果有消费者监听,消息会存储至消费者的缓存区域(有上限)

4、基于Redis的Stream数据类型消息队列(也是一个数据类型,故而支持持久化)

 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

消息读完不会被删除

 

 

 

 

 若block=0 则表示一直阻塞

STREAM类型消息队列的XREAD命令特点:

* 消息可回溯   不会丢失,永久保存于队列中
* 一个消息可以被多个消费者读取
* 可以阻塞读取  添加block指定时间
* 有消息漏读的风险  使用$时,由于只会读取最新的,导致之前的漏读

 

5、基于Redis的Stream的消息队列-消费者组

消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
消息标示机制确保消息拿到会被消费,即使消费者拿到消息后宕机,重启后任然可以从标示处消费
消息确认机制,每个消费者都有自己的pendingList,确保每个消息至少被消费一次

 

 

消费者组方法:

 

 

 

 

消息确认:

 

 

查看指定组里的pendingList,可同时指定消费者

 

 

 

从pendingList读取未确认消息,只需把XReadGROUP ID 改为 0

 

 

消费者监听消息的基本思路:

 

 

STREAM类型消息队列的XREADGROUP命令特点:

* 消息可回溯
* 可以多消费者争抢消息,加快消费速度
* 可以阻塞读取
* 没有消息漏读的风险
* 有消息确认机制,保证消息至少被消费一次

三种不同类型消息队列对比:

 

 

修改秒杀下单优化案例:

 

 

手动创建消费者组 g1 和消息队列 stream.order:

127.0.0.1:6379> XGROUP CREATE stream.order g1 0 MKSTREAM
OK

向luajiaoben中添加:

-- 3.6.发送消息到队列 XADD stream.order * k1 v1 k2 v2 ...
redis.call("xadd", "stream.order", "*", "userId", userId, "voucherId", voucherId, "id", orderId)

修改voucherOrderHandler类,新建handlePendingList方法:

/** 
* 秒杀优化后代码 
* 基于redis的stream消息队列 
*/
private class voucherOrderHandler implements Runnable {
    String queuename = "stream.order";    
    @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(queuename, 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 voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);                
                // 3.创建订单                
                handleVoucherOrder(voucherOrder);                
                // 4.确认消息 XACK key group id                
                stringRedisTemplate.opsForStream().acknowledge(queuename, "g1", record.getId());                            
                } catch (Exception e) {
                log.error("处理订单异常", e);                
                //处理异常消息                
                handlePendingList();            
              }
        }
    }

    public void handlePendingList() {
        while (true) {
            try {
                // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 STREAMS s1 0                
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),                        
                        StreamReadOptions.empty().count(1),                        
                        StreamOffset.create(queuename, 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 voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);                
                // 3.创建订单                
                handleVoucherOrder(voucherOrder);                
                // 4.确认消息 XACK key group id                
                stringRedisTemplate.opsForStream().acknowledge(queuename, "g1", record.getId());                
                //处理异常消息            
                } catch (Exception e) {
                log.error("处理pendding-list订单异常", e);                
                try {
                    Thread.sleep(20);                
                    } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);                
                  }
            }
        }
    }
}

修改seckillVoucher():

/**     
* 秒杀优化后代码     
* 基于Redis的stream         
* @param voucherId     
* @return     
*/    
@Override    
public Result seckillVoucher(Long voucherId) {
//        获取用户id        
    Long userId = UserHolder.getUser().getId();
//        订单id        
    long orderId = redisIdWorker.nextId("order");
//        1.执行lua脚本        
    Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,                
    Collections.emptyList(),                
    voucherId.toString(), userId.toString(), String.valueOf(orderId));
//        2.判断结果是否为0        
    int r = result.intValue();        
    if (r != 0) {
            // 2.1 不为0,代表没有购买资格            
            return Result.fail(r == 1 ? "库存不足" : "重复下单");        
            }
//        3.获取代理对象        
        proxy = (IVoucherOrderService) AopContext.currentProxy();        
        return Result.ok(orderId);    
}

测试:成功实现下单操作,数据库成功减一

 

 

 

秒杀(超卖、一人一单)总结:

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Scrapy-Redis队列是指使用Scrapy-Redis库将Scrapy的队列替换为Redis数据库的一种方法。通过将队列存储在Redis中,可以实现分布式爬虫的功能。具体来说,Scrapy-Redis库使用Redis数据库来存储爬取的URL,并在多个爬虫节点之间共享这些URL。这样,不同的爬虫节点可以从Redis中获取URL并进行爬取,实现了分布式爬取的效果。\[2\] 在使用Scrapy-Redis时,需要安装相应的模块,包括redisredis-py-cluster、scrapy-redis和scrapy-redis-cluster等模块。这些模块的版本需要满足Scrapy-Redis的要求,例如redis的版本需要是2.10.6,redis-py-cluster的版本需要是1.3.6,scrapy-redis的版本需要是0.6.8,scrapy-redis-cluster的版本需要是0.4。\[3\] 通过使用Scrapy-Redis队列,可以实现分布式爬虫的高效运行,提高爬取效率和并发能力。 #### 引用[.reference_title] - *1* *3* [Scrapy-Redis入门实战](https://blog.csdn.net/pengjunlee/article/details/89853550)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [scrapy-redis 更改队列和分布式爬虫](https://blog.csdn.net/qq_40279964/article/details/87903435)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值