Redis16-实践-优惠秒杀2

文章目录

Redis16-实践-优惠秒杀2

5、分布式锁

(1)概述
  • 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
  • 分布式锁他应该满足的条件
    • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思
    • 互斥性:互斥是分布式锁的最基本的条件,使得程序串行执行
    • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
    • 高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能
    • 安全性:安全也是程序中必不可少的一环
(2)基本原理
  • 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
(3)实现方式对比
  • 分布式锁实现:分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种
  • Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见
  • Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
  • Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案,由于本套视频并不讲解zookeeper的原理和分布式锁的实现,所以不过多阐述
(4)Redis实现逻辑
  • 实现分布式锁时需要实现的两个基本方法:
    • 获取锁:
      • 互斥:确保只能有一个线程获取锁
      • 非阻塞:尝试一次,成功返回true,失败返回false
复制代码
- 释放锁:
    * 手动释放
    * 超时释放:获取锁时添加一个超时时间
  • 核心思路:我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key了,返回了1,如果结果是1,表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可。
(5)初步实现
  • 锁的基本接口
  • SimpleRedisLock:加锁逻辑,利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性
java 复制代码
private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标示
    String threadId = Thread.currentThread().getId()
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}
  • SimpleRedisLock:释放锁逻辑,放锁,防止删除别人的锁
java 复制代码
public void unlock() {
    //通过del删除锁
    stringRedisTemplate.delete(KEY_PREFIX + name);
}
  • 修改业务代码
java 复制代码
  @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象(新增代码)
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象
        boolean isLock = lock.tryLock(1200);
        //加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
    }
(6)锁误删问题
  • 问题:持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明
  • 解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。
(7)锁误删问题解决
  • 需求:修改之前的分布式锁实现,满足:
    • 在获取锁时存入线程标示(可以用UUID表示)
    • 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
      • 如果一致则释放锁
      • 如果不一致则不释放锁
  • 核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
  • 加锁代码:
java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
   // 获取线程标示
   String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}
  • 释放锁代码:
java 复制代码
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}
  • 有关代码实操说明:在我们修改完此处代码后,我们重启工程,然后启动两个线程,第一个线程持有锁后,手动释放锁,第二个线程 此时进入到锁内部,再放行第一个线程,此时第一个线程由于锁的value值并非是自己,所以不能释放锁,也就无法删除别人的锁,此时第二个线程能够正确释放锁,通过这个案例初步说明我们解决了锁误删的问题。
(8)原子性问题
  • 更为极端的误删逻辑说明:线程1现在持有锁之后,他正准备删除锁,而且判断锁确实是他自己的,此时他的锁到期了,此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生
(9)原子性问题解决
  • Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
  • Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
  • 这里重点介绍Redis提供的调用函数,语法如下:
lua 复制代码
redis.call('命令名称', 'key', '其它参数', ...)
  • 例如,我们要执行set name jack,则脚本是这样:
lua 复制代码
# 执行 set name jack
redis.call('set', 'name', 'jack')
  • 例如,我们要先执行set name Rose,再执行get name,则脚本如下:
lua 复制代码
# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name
  • 写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下
  • 例如,我们要执行 redis.call('set', 'name', 'jack') 这个脚本,语法如下:
  • 如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
  • 释放锁的业务流程是这样的
    • 获取锁中的线程标示
    • 判断是否与指定的标示(当前线程标示)一致
    • 如果一致则释放锁(删除)
    • 如果不一致则什么都不做
  • 最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样
lua 复制代码
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
  • Java代码:我们的RedisTemplate中,可以利用execute方法去执行lua脚本,参数对应关系就如下图股
java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

public void unlock() {
    // 调用lua脚本
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId());
}
经过以上代码改造后,我们就能够实现 拿锁比锁删锁的原子性动作了~

6、Redis优化秒杀

(1)异步秒杀思路
  • 流程及问题:当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,在这些操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的很慢
  • 优化方案:我们将耗时比较短的逻辑判断放入到redis中,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,当然这里边有两个难点
    • 第一个难点:是我们怎么在redis中去快速校验一人一单,还有库存判断
    • 第二个难点:是由于我们校验和tomct下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。
  • 实现思路:当用户下单之后,判断库存是否充足只需要到 Redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作。当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。
(2)Redis完成秒杀资格判断
  • 需求:
    • 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
    • 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
    • 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
  • VoucherServiceImpl
java 复制代码
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
    // 保存秒杀库存到Redis中
    //SECKILL_STOCK_KEY 这个变量定义在RedisConstans中
    //private static final String SECKILL_STOCK_KEY ="seckill:stock:"
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
  • 完整lua表达式
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 stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.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
  • VoucherOrderServiceImpl:当以上lua表达式执行完毕后,剩下的就是根据步骤3,4来执行我们接下来的任务了
java 复制代码
@Override
public Result seckillVoucher(Long voucherId) {
    //获取用户
    Long userId = UserHolder.getUser().getId();
    long orderId = redisIdWorker.nextId("order");
    // 1.执行lua脚本
    Long result = stringRedisTemplate.execute(
            SECKILL_SCRIPT,
            Collections.emptyList(),
            voucherId.toString(), userId.toString(), String.valueOf(orderId)
    );
    int r = result.intValue();
    // 2.判断结果是否为0
    if (r != 0) {
        // 2.1.不为0 ,代表没有购买资格
        return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
    }
    //TODO 保存阻塞队列
    // 3.返回订单id
    return Result.ok(orderId);
}
(3)基于阻塞队列实现秒杀优化
  • VoucherOrderServiceImpl:修改下单动作,现在我们去下单时,是通过lua表达式去原子执行判断逻辑,如果判断我出来不为0 ,则要么是库存不足,要么是重复下单,返回错误信息,如果是0,则把下单的逻辑保存到队列中去,然后异步执行
  • 基于阻塞队列的异步秒杀存在哪些问题?
    • 内存限制问题
    • 数据安全问题
java 复制代码
//异步处理线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

//在类初始化之后执行,因为当这个类初始化好了之后,随时都是有可能要执行的
@PostConstruct
private void init() {
   SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
// 用于线程池处理的任务
// 当初始化完毕后,就会去从对列中去拿信息
 private class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while (true){
                try {
                    // 1.获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2.创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
               }
        }
     
       private void handleVoucherOrder(VoucherOrder voucherOrder) {
            //1.获取用户
            Long userId = voucherOrder.getUserId();
            // 2.创建锁对象
            RLock redisLock = redissonClient.getLock("lock:order:" + userId);
            // 3.尝试获取锁
            boolean isLock = redisLock.lock();
            // 4.判断是否获得锁成功
            if (!isLock) {
                // 获取锁失败,直接返回失败或者重试
                log.error("不允许重复下单!");
                return;
            }
            try {
                //注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效
                proxy.createVoucherOrder(voucherOrder);
            } finally {
                // 释放锁
                redisLock.unlock();
            }
    }
     //a
    private BlockingQueue<VoucherOrder> orderTasks =new  ArrayBlockingQueue<>(1024 * 1024);

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        long orderId = redisIdWorker.nextId("order");
        // 1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );
        int r = result.intValue();
        // 2.判断结果是否为0
        if (r != 0) {
            // 2.1.不为0 ,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        // 2.3.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 2.4.用户id
        voucherOrder.setUserId(userId);
        // 2.5.代金券id
        voucherOrder.setVoucherId(voucherId);
        // 2.6.放入阻塞队列
        orderTasks.add(voucherOrder);
        //3.获取代理对象
         proxy = (IVoucherOrderService)AopContext.currentProxy();
        //4.返回订单id
        return Result.ok(orderId);
    }
     
      @Transactional
    public  void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
           log.error("用户已经购买过了");
           return ;
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足");
            return ;
        }
        save(voucherOrder);
 
    }

7、Redis消息队列

(1)认识消息队列
  • 什么是消息队列:字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
    • 消息队列:存储和管理消息,也被称为消息代理(Message Broker)
    • 生产者:发送消息到消息队列
    • 消费者:从消息队列获取消息并处理消息
  • 消息队列好处:实现解耦,举一个生活中的例子就是:快递员(生产者)把快递放到快递柜里边(Message Queue)去,我们(消费者)从快递柜里边去拿东西,这就是一个异步,如果耦合,那么这个快递员相当于直接把快递交给你,这事固然好,但是万一你不在家,那么快递员就会一直等你,这就浪费了快递员的时间。
  • 秒杀场景:我们下单之后,利用redis去进行校验下单条件,再通过队列把消息发送出去,然后再启动一个线程去消费这个消息,完成解耦,同时也加快我们的响应速度。
  • 选型:这里我们可以使用一些现成的mq,比如kafka,rabbitmq等等,但是呢,如果没有安装mq,我们也可以直接使用redis提供的mq方案,降低我们的部署和学习成本。
  • Redis提供了三种不同的方式来实现消息队列:
    • list结构:基于List结构模拟消息队列
    • PubSub:基本的点对点消息模型
    • Stream:比较完善的消息队列模型
(2)基于List实现消息队列
  • 基于List结构模拟消息队列:
    • 消息队列(Message Queue),字面意思就是存放消息的队列。而Redis的list数据结构是一个双向链表,很容易模拟出队列效果。
    • 队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。
    • 不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。
  • 基于List的消息队列有哪些优缺点?
    • 优点:
      • 利用Redis存储,不受限于JVM内存上限
      • 基于Redis的持久化机制,数据安全性有保证
      • 可以满足消息有序性
    • 缺点:
      • 无法避免消息丢失
      • 只支持单消费者
(3)基于PubSub的消息队列
  • PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
    • SUBSCRIBE channel channel :订阅一个或多个频道
    • PUBLISH channel msg :向一个频道发送消息
    • PSUBSCRIBE patternpattern :订阅与pattern格式匹配的所有频道
  • 基于PubSub的消息队列有哪些优缺点?
    • 优点:采用发布订阅模型,支持多生产、多消费
    • 缺点:
      • 不支持数据持久化
      • 无法避免消息丢失
      • 消息堆积有上限,超出时数据丢失
(4)基于Stream的消息队列
  • Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
  • 发送消息的命令:
  • 例如:
  • 读取消息的方式之一:XREAD
  • 例如,使用XREAD读取第一个消息:
  • XREAD阻塞方式,读取最新的消息:
  • 在业务开发中,我们可以循环的调用XREAD阻塞方式来查询最新消息,从而实现持续监听队列的效果,伪代码如下
  • STREAM类型消息队列的XREAD命令特点:
    • 消息可回溯
    • 一个消息可以被多个消费者读取
    • 可以阻塞读取
    • 有消息漏读的风险
(5)基于Stream的消息队列-消费者组
  • 消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
  • 创建消费者组:
复制代码
- key:队列名称
- groupName:消费者组名称
- ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
- MKSTREAM:队列不存在时自动创建队列
  • 其它常见命令:
    • 删除指定的消费者组
java 复制代码
XGROUP DESTORY key groupName
复制代码
-  给指定的消费者组添加消费者
java 复制代码
XGROUP CREATECONSUMER key groupname consumername
复制代码
-  删除消费者组中的指定消费者
java 复制代码
XGROUP DELCONSUMER key groupname consumername
复制代码
- 从消费者组读取消息:
java 复制代码
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
复制代码
    * group:消费组名称
    * consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
    * count:本次查询的最大数量
    * BLOCK milliseconds:当没有消息时最长等待时间
    * NOACK:无需手动ACK,获取到消息后自动确认
    * STREAMS key:指定队列名称
    * ID:获取消息的起始ID:
        + ">":从下一个未消费的消息开始
        + 其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始
  • 消费者监听消息的基本思路:
  • STREAM类型消息队列的XREADGROUP命令特点:
    • 消息可回溯
    • 可以多消费者争抢消息,加快消费速度
    • 可以阻塞读取
    • 没有消息漏读的风险
    • 有消息确认机制,保证消息至少被消费一次
(6)三种消息队列比较
(7)基于Redis的Stream结构作为消息队列实现异步秒杀下单
  • 需求:
    • 创建一个Stream类型的消息队列,名为stream.orders
    • 修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId
    • 项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单
  • 修改lua表达式,新增3.6
  • VoucherOrderServiceImpl
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,说明没有异常消息,结束循环
                    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订单异常", e);
                try{
                    Thread.sleep(20);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}
相关推荐
cfm_291432 分钟前
高并发系统缓存全解
java·缓存
要开心吖ZSH2 小时前
本地缓存方案选择指南:volatile、ConcurrentHashMap、Caffeine 怎么选?
缓存·caffeine·volatile·本地缓存
范什么特西2 小时前
redis题目面渣重点
数据库·redis·缓存
今天的砖头有点烫手啊3 小时前
接口太慢?Spring Boot 缓存体系 @Cacheable 全链路拆解
spring boot·后端·缓存
szephyr4 小时前
腾讯云 ADP 智能体的 Skills 版本回滚总是回到旧配置,是缓存没清还是版本管理没开?
java·缓存·腾讯云
程序员良辰6 小时前
【TongWeb8】使用 Crontab 定时重启和检测 TongWeb 服务
java·中间件·tomcat
油丶酸萝卜别吃8 小时前
Redis 布隆过滤器快速实现
数据库·redis·缓存
IT白小白8 小时前
每日问答中间件篇
中间件
MC皮蛋侠客8 小时前
TDengine C++ 系列(8):流式计算与最新值缓存——库内实时处理
c++·缓存·tdengine
MC皮蛋侠客1 天前
Redis 系列(八):缓存设计模式与一致性——从 Cache Aside 到防雪崩
redis·缓存·设计模式