redisson的使用及LUA脚本实现分布式秒杀

1.redisson实现分布式锁(推荐)

redisson官网:Redisson: Easy Redis Java client and Real-Time Data Platform

Redisson是一个基于Redis的Java客户端,它不仅提供了对Redis基本操作的支持,而且是一个功能丰富的分布式协调服务客户端。Redisson致力于简化在分布式环境中的开发工作,它实现了许多分布式服务,如分布式锁、分布式计数器、分布式队列、分布式映射等。 以下是一些Redisson的主要特点: 高级客户端:Redisson提供了与传统Java集合类似的API,比如Map、List、Set、Queue、Deque、Topic、Multimap、SortedSet等,使得开发者能够以几乎无感知的方式使用分布式数据结构。 分布式服务:支持各种分布式服务,如分布式锁、信号量、读写锁、原子整数、计数器、延迟队列、事件发布订阅、任务调度等。 高可用性:通过Redis Sentinel或Redis Cluster支持高可用性,能够在节点故障时自动切换。 客户端负载均衡:支持多节点连接,自动进行客户端负载均衡。 序列化:内置多种序列化方式,包括Jackson、Avro、Gson等,方便数据交换。 非侵入式:Redisson不需要额外的配置或代理层,可以直接集成到现有的Java应用中。 性能优化:通过使用Netty框架,Redisson实现了高效的网络通信,提供了低延迟和高吞吐量。 Lua脚本支持:可以使用Lua脚本在服务器端执行复杂操作,确保操作的原子性。 由于其丰富的功能和易于使用的API,Redisson成为Java开发者在构建分布式系统时的一个流行选择。

使用

XML 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.2</version>
</dependency>
java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonClientConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
//        如果是集群模式可以使用config.useClusterServers()来设置集群模式
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}
java 复制代码
/**
 * <p>
 * 服务实现类
 * </p>
 * create cws
 * @since 2024-05-18
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    SeckillVoucherMapper seckillVoucherMapper;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    RedissonClient redissonClient;

    /**
     * 抢票
     *
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
//        1.获取优惠卷信息
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(voucherId);
// 2.判断秒杀是否开启
//       2.1获取当前时间
        LocalDateTime now = LocalDateTime.now();
        if (seckillVoucher.getBeginTime().isAfter(now)) {
            return Result.fail("秒杀未开启");
        }
//        3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(now)) {
            return Result.fail("秒杀已结束");
        }
//       4. 判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();

//        创建锁对象
//        SimpleRedisLock lock = new SimpleRedisLock("order:"+userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("order:" + userId);
        boolean res = lock.tryLock();
//        获取锁
        if(!res){
            return Result.fail("不能重新下单~");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
//            释放锁
            lock.unlock();
        }
    }
    @Transactional(rollbackFor = Exception.class)
    public  Result createVoucherOrder(Long voucherId) {
        //       5.判断用于是否已经领取
        Long userId = UserHolder.getUser().getId();
        if (query().eq("user_id", userId).eq("voucher_id", voucherId).count() > 0) {
            return Result.fail("用户已经领取过");
        }
//        6.扣减库存
        boolean res = seckillVoucherMapper.updateStock(voucherId);
        if (!res) {
            return Result.fail("库存不足");
        }
//        7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        long orderId = RedisWorker.nextId("order");
        voucherOrder.setId(orderId);
        save(voucherOrder);
        return Result.ok(orderId);
    }
}

通过源码我们可以发现 lock.tryLock()在无参的情况下,等待时间为-1也就像不等待,等待释放时间30s。

redisson可重入机制(原理)

我从源码分析

java 复制代码
//点击lock.tryLock()方法的实现

//核心
  <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }

从上面代码中我们可以发现底层也是使用了LUA脚本。

java 复制代码
"if (redis.call('exists', KEYS[1]) == 0) then " 
 "redis.call('hincrby', KEYS[1], ARGV[2], 1); " 
"redis.call('pexpire', KEYS[1], ARGV[1]); " 
"return nil; " 
"end; " 
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " 
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " 
"redis.call('pexpire', KEYS[1], ARGV[1]); " 
"return nil; " 
"end; " 
"return redis.call('pttl', KEYS[1]);"

#解释
检查键(锁)是否存在(redis.call('exists', KEYS[1]) == 0):
如果不存在,说明当前没有其他线程持有锁,执行以下操作:
使用hincrby在哈希表中增加一个字段(对应线程ID),值为1,表示获取锁。
使用pexpire设置键的过期时间(根据传入的leaseTime和unit计算得到的毫秒值)。
若键已存在,检查字段(当前线程ID)是否已存在于哈希表中(redis.call('hexists', KEYS[1], ARGV[2]) == 1):
如果存在,说明当前线程已经持有锁,执行以下操作:
使用hincrby增加字段的值(表示重置锁的计数)。
再次使用pexpire更新键的过期时间。
如果以上条件都不满足,说明锁被其他线程持有,返回键的剩余存活时间(return redis.call('pttl', KEYS[1]))。

实现流程图

主要使用redis的哈希结构实现,每次获取锁value加一,释放一次value减一,当value等于0时删除锁。

2.异步秒杀

将判断秒杀库存及校验一人一单交给redis去做出来,主要就大大降低了对数据库的压力。那么怎么在redis里面处理这两个业务呢?这里我们使用redis里面的string结构存储秒杀的库存,用Set结构去存储下单人员保证唯一性。这两个操作必要满足原子性所以我们这里还是使用LUA脚本去实现。

具体实现流程

具体实现代码

在添加秒杀优惠卷时我们添加将秒杀库存添加到redis

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
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
    }

修改业务

java 复制代码
  @Resource
    RedissonClient redissonClient;
    private static final DefaultRedisScript<Long> un_lock;
    //    加载脚本
    static {
        un_lock = new DefaultRedisScript<>();
        un_lock.setLocation(new ClassPathResource("secJi.lua"));
        un_lock.setResultType(Long.class);
    }
    /**
     * 优化
     */
   @Override
    public Result seckillVoucher(Long voucherId) {
       String userId = UserHolder.getUser().getId().toString();

//       lua代码所需的key
       List<String> keyList = new ArrayList<>();
       keyList.add(SECKILL_STOCK_KEY+voucherId.toString());
       keyList.add(SECKILL_ORDER_KEY+voucherId.toString());
//       1.执行LAU脚本
       Long result = stringRedisTemplate.execute(
               un_lock, keyList, userId
       );
       int res = result.intValue();
       if(res!=0){
           return Result.fail(res==1?"库存不足":"重复下单");
       }
//       TODO 将订单交给阻塞队列
       //       订单号
       long order = RedisWorker.nextId("order");

       return  Result.ok(order);
   }

LUA脚本

Lua 复制代码
-- 获取库存 KEYS[1] 也就是seckill:stock:xx
local stock = redis.call('get', KEYS[1])
-- 判断set中是否存在用户   KEYS[2] 订单id:seckill:order:xx    ARGV[1] 为用户
local userId = redis.call('sismember', KEYS[2], ARGV[1])
--判断库存是否大于0
if tonumber(stock) <= 0 then
    --库存不足返回1
    return 1
end
--判断用户是否重复下单
if userId == 1 then
    --用户重复下单返回2
    return 2
end
--库存减一
redis.call('decr', KEYS[1])
--将用户添加到set中
redis.call('sadd', KEYS[2], ARGV[1])
return 0

完整代码

Lua 复制代码
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    // 注入SeckillVoucherMapper
    @Resource
    SeckillVoucherMapper seckillVoucherMapper;

    // 注入StringRedisTemplate用于Redis字符串操作
    @Resource
    StringRedisTemplate stringRedisTemplate;

    // 注入RedissonClient用于Redis分布式锁
    @Resource
    RedissonClient redissonClient;

    // 定义Redis脚本,用于秒杀解锁操作
    private static final DefaultRedisScript<Long> un_lock;

    static {
        un_lock = new DefaultRedisScript<>();
        un_lock.setLocation(new ClassPathResource("secJi.lua"));
        un_lock.setResultType(Long.class);
    }

    // 创建一个定长的阻塞队列,用于存储代金券订单任务
    private BlockingQueue<VoucherOrder> orderTaskQueue = new ArrayBlockingQueue<>(1024 * 1024);

    // 创建单线程执行器,用于处理代金券订单
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    // 子线程中无法直接获取Service实例,因此在主线程中获取代理对象
    private IVoucherOrderService proxy;

    /**
     * 初始化方法,启动订单处理线程。
     */
    @PostConstruct
    void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    // 秒杀代金券订单的处理线程
    private class VoucherOrderHandler implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    // 从订单任务队列中获取一个代金券订单
                    VoucherOrder voucherOrder = orderTaskQueue.take();
                    // 处理订单
                    HandleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理代金券订单错误: {}", e);
                }
            }
        }
    }

    /**
     * 处理代金券订单,包括创建订单和释放分布式锁。
     * @param voucherOrder 代金券订单对象
     */
    private void HandleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        // 创建Redis分布式锁
        RLock lock = redissonClient.getLock("order:" + userId);
        boolean res = lock.tryLock();
        if (!res) {
            log.error("不能重新下单");
        }
        try {
            // 调用创建代金券订单方法
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            // 释放分布式锁
            lock.unlock();
        }
    }

    /**
     * 用户参与秒杀代金券活动。
     * 
     * @param voucherId 代金券ID
     * @return 返回秒杀结果,成功返回订单ID,失败返回错误信息
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        String userId = UserHolder.getUser().getId().toString();
        // 准备Lua脚本所需的key
        List<String> keyList = new ArrayList<>();
        keyList.add(SECKILL_STOCK_KEY + voucherId.toString());
        keyList.add(SECKILL_ORDER_KEY + voucherId.toString());
        // 执行Lua脚本进行库存检查和下单操作
        Long result = stringRedisTemplate.execute(
                un_lock, keyList, userId
        );
        int res = result.intValue();
        if (res != 0) {
            return Result.fail(res == 1 ? "库存不足" : "重复下单");
        }
        // 构建代金券订单并提交到阻塞队列
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        long orderId = RedisWorker.nextId("order");
        voucherOrder.setId(orderId);
        orderTaskQueue.add(voucherOrder);
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }
    /**
     * 创建代金券订单,包括核对用户是否已领取、扣减库存和创建订单操作。
     * 
     * @param voucherOrder 代金券订单对象
     */
    @Override
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        // 核对用户是否已领取相同代金券
        Long userId = voucherOrder.getUserId();
        Long voucherId = voucherOrder.getVoucherId();
        if (query().eq("user_id", userId).eq("voucher_id", voucherId).count() > 0) {
            log.error("用户已经领取过");
        }
        // 扣减库存
        boolean res = seckillVoucherMapper.updateStock(voucherId);
        if (!res) {
            log.error("库存不足");
        }
        // 创建订单
        save(voucherOrder);
    }
}

这样实现虽然确保了性能方面的提升,但是在极端情况下例如jvm宕机了那么这里的阻塞队列的数据将会丢失,导致mysql与redis的数据原子性出现了异常。我们下列使用消息队列去解决该问题。

3.redis消息队列

基于List结构模拟的消息队列

BRPOP与LPUSH

优缺点:

优点:

  • 李勇Redis存储,不受限于JVM内存上限

  • 基于Redis的持久化机制,数据库安全性有保证

  • 可以满足消息有序性

缺点:

  • 无法避免消丢失

  • 只支持消费者

基于Stream的消息队列

Stream的基本概念:

  • 消息与ID:Stream中的基本单位是消息(message),每条消息都有一个唯一标识符(ID),这个ID是一个递增的整数或是一个由数字和字母组成的字符串,确保了消息的顺序性。消息ID可以用来定位和范围读取消息。

  • 字段(field)和值(value):每个消息可以包含多个键值对形式的字段和值,这使得消息内容更加丰富和结构化。

  • 消费者组(Consumer Group):Stream支持消费者组的概念,一个消息可以被多个消费者组订阅,每个消费者组内的消费者可以独立地消费消息,实现了消息的广播和分发。消费者组内支持消息的确认机制,确保消息不会丢失。

  • 持久性和可靠性:Stream中的消息默认是持久化的,即使Redis服务器重启,消息也不会丢失。同时,通过消费者确认机制可以实现消息的可靠处理。

  • 读取偏移量:消费者可以通过指定消息ID或偏移量来读取消息,支持范围读取、读取新消息或未确认消息等多种模式。

  • 消息限流:Stream可以通过XPENDING命令查看消费者组的状态,包括已处理和未处理的消息数量,从而实现流量控制和监控。

应用场景:

  • 实时日志处理:作为日志收集和处理管道,支持高并发的日志记录和分析。

  • 实时数据流处理:在金融交易、物联网(IoT)等场景中,处理连续的数据流。

  • 消息队列和事件驱动架构:构建高度可扩展和解耦的微服务系统。

  • 用户活动追踪:记录和分析用户行为,如点击流分析。

  • 缓存更新通知:作为数据库更新的通知机制,实现数据同步。

  • Stream类型通过其灵活的设计和强大的功能集,成为了现代分布式系统中数据传输和处理的重要组件。

发送消息指令

Lua 复制代码
#生成者
Windows:0>XADD s1 * k1 v1
"1716112531587-0"
Windows:0>XLEN s1  #查看长度
"1"

#消费者
Windows:0>XREAD COUNT 1 STREAMS s1 0
1) 1) "s1"
   2) 1) 1) "1716112531587-0"
         2) 1) "k1"
            2) "v1"
       
#消费者阻塞等待  BLOCK
Windows:0>XREAD COUNT 1 BLOCK 0 STREAMS s1 $  
1) 1) "s1"
   2) 1) 1) "1716112915859-0"
         2) 1) "k1"
            2) "v1"
 #COUNT 1 表示每次读取消息的最大数量
 #BLOCK 0 开启阻塞 0为永久阻塞
 # STREAMS s1  对应的队列消息
 # $ 表示从最新消息开始读取

消费者组

Redis Stream的消费者组(Consumer Groups)是实现消息处理的关键特性。它们允许将Stream中的消息分配给一组消费者,而不是单个消费者。消费者组允许消息的并发处理,同时也提供了消息的可靠传递和幂等性。 以下是一些关于消费者组的关键点:

  • 创建消费者组:使用XGROUP CREATE命令可以创建一个新的消费者组,指定Stream的名称、消费者组的名称以及起始ID。起始ID通常是$表示从Stream的最新消息开始,或者是一个特定的ID表示从历史消息开始。

  • 消息分配:当消息被写入Stream时,它们被分配给消费者组。每个消费者组内部,消息被轮询分配给组内的消费者。默认情况下,每个消费者只看到其他消费者未消费的消息,这样可以避免消息被多个消费者重复处理。

  • 消息确认:消费者使用XREADGROUP或XACK命令来读取和确认消息。当消费者确认消息时,该消息被视为已处理并可以从Stream的主数据结构中移除(除非配置了NOACK选项)。未确认的消息保留在pending entries list(PEL)中,等待确认。

  • 消费者状态:XPENDING命令用于查询消费者组中未确认的消息,包括它们的ID、消费者名和等待时间。

  • 幂等性:通过消费者组,消息只被一个消费者处理一次,即使消费者崩溃并重新连接,未确认的消息也不会被重新分配,除非使用XREADGROUP的COUNT参数或IDLE时间设置来重新分配。

  • 消费者心跳:消费者通过PING命令发送心跳来保持其活跃状态,防止未确认的消息被重新分配。

  • 消费者组清理:使用XGROUP DELCONSUMER可以删除消费者组中的消费者,而XGROUP SETID可以将消费者组的读取位置重置到某个ID,用于处理消息丢失或需要重新处理的情况。

  • 消费者组的设计使得Redis Stream能够支持复杂的消息处理场景,如消息的可靠传递、回溯处理以及在多个消费者之间公平地分配工作负载。

创建消费者组

bash 复制代码
#创建消费组
Windows:0>XGROUP CREATE s1 l1 0  #s1表示队列名称  l1表示消费组名称 0 表示队列中的第一个消息
"OK"


#消费者
Windows:0>XREADGROUP GROUP l1 c1 COUNT 1 BLOCK 200 STREAMS s1 > 
# l1 表示消费组名称  c1消费者名称(不写默认分配)   COUNT 1读取多少条  BLOCK 200等待多少时间(毫秒)  STREAMS s1指定队列名称
1) 1) "s1"
   2) 1) 1) "1716112531587-0"
         2) 1) "k1"
            2) "v1"
Windows:0>XREADGROUP GROUP l1 c2 COUNT 1 BLOCK 200 STREAMS s1 > 
1) 1) "s1"
   2) 1) 1) "1716112773920-0"
         2) 1) "k2"
            2) "v2"
#使用XACK指令确认的消息
Windows:0>XACK s1 l1 1716112531587-0 1716112773920-0  #组名
"2"

#查看pendingList里面为确认消息
XPENDING  s1 l1 - + 10   #s1队列名称 l1表示消费组名称  -+表示读取全部   读取10条  

Windows:0>XREADGROUP GROUP l1 c2 COUNT 1 BLOCK 200 STREAMS s1 > 
1) 1) "s1"
   2) 1) 1) "1716112915859-0"
         2) 1) "k1"
            2) "v1"
#上面那条消息未必确认,所以可以使用XPENDING查询出来
Windows:0>XPENDING s1 l1 - + 10
1) 1) "1716112915859-0"
   2) "c2"
   3) "1674"
   4) "1"

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

  • 消息可回溯

  • 可以多消费者争抢消息,加快消费速度

  • 可以阻塞读取

  • 没有消息漏读的风险

  • 有消息确认机制,保证消息至少被消费一次

4.使用Stream结构实现队列结合业务秒杀

java 复制代码
import static com.hmdp.utils.RedisConstants.SECKILL_ORDER_KEY;
import static com.hmdp.utils.RedisConstants.SECKILL_STOCK_KEY;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @since 2021-12-22
 */
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {


    @Resource
    SeckillVoucherMapper seckillVoucherMapper;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    RedissonClient redissonClient;


    private static final DefaultRedisScript<Long> script;

    //    加载脚本
    static {
        script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("seckill.lua"));
        script.setResultType(Long.class);
    }


    //    创建线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();


    //    在子线程中是无法获取嗲了对象的,所以我们在主线程中获取
    private IVoucherOrderService proxy;

    @PostConstruct
    void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

        private class VoucherOrderHandler implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
//                    1.获取消息队列中的订单消息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >  
                    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()) {
                        //  3.如果获取失败,说明没有消息,继续下一次循环
                        continue;
                    }
//                    解析队列中的数据
                    MapRecord<String, Object, Object> entries = list.get(0);
                    Map<Object, Object> body = entries.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(body, new VoucherOrder(), true);
                    //  创建订单
                    HandleVoucherOrder(voucherOrder);
//                   确认消息
                    stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", entries.getId());
                } catch (Exception e) {
                    log.error("队列子线程错误{}", e);
                    handlePendingList();
                }
            }
        }

            private void handlePendingList() {
                while (true) {
                    try {
                        List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                                Consumer.from("g1", "c1"),
                                StreamReadOptions.empty().count(1),
                                StreamOffset.create("stream.orders", ReadOffset.from("0"))
                        );
                        if (list == null || list.isEmpty()) {
                            //  如果获取失败,说明没有消息,继续下一次循环
                            break;
                        }
                        MapRecord<String, Object, Object> entries = list.get(0);
                        Map<Object, Object> body = entries.getValue();
                        VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(body, new VoucherOrder(), true);
                        //  创建订单
                        HandleVoucherOrder(voucherOrder);
                        //  确认消息
                        stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", entries.getId());
                    } catch (Exception e) {
                        log.error("处理pendingList失败", e);
                        try {
                            Thread.sleep(20);
                        } catch (InterruptedException interruptedException) {
                            interruptedException.printStackTrace();
                        }
                    }
                }
            }
        }


    private void HandleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        //        创建锁对象
        RLock lock = redissonClient.getLock("order:" + userId);
        boolean res = lock.tryLock();
//        获取锁
        if (!res) {
            log.error("不能重新下单");
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        } finally {
//            释放锁
            lock.unlock();
        }
    }


        @Override
    public Result seckillVoucher(Long voucherId) {
        String userId = UserHolder.getUser().getId().toString();
        long orderId = RedisWorker.nextId("order");


//       1.执行LAU脚本
        Long result = stringRedisTemplate.execute(
                script, Collections.emptyList(),
                voucherId.toString(), userId, String.valueOf(orderId)
        );
        int res = result.intValue();
        if (res != 0) {
            return Result.fail(res == 1 ? "库存不足" : "重复下单");
        }
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        //       1.判断用于是否已经领取
        Long userId = voucherOrder.getUserId();
        Long voucherId = voucherOrder.getVoucherId();
        if (query().eq("user_id", userId).eq("voucher_id", voucherId).count() > 0) {
            log.error("用户已经领取过");
        }
//        2.扣减库存
        boolean res = seckillVoucherMapper.updateStock(voucherId);
        if (!res) {
            log.error("库存不足");
        }
//        3.创建订单
        save(voucherOrder);
    }
}
Lua 复制代码
---
--- Generated by Luanalysis
--- Created by cws.
--- DateTime: 2024/5/19 15:17
---
-- 优惠卷id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]
-- 订单id
local orderId= ARGV[3]

local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. userId

-- 判断库存是否充足
if(tonumber(redis.call('get',stockKey)) <= 0) then
    return 1
end

-- 判断用户是否重复下单
if(redis.call('sismember',orderKey,userId) == 1) then
    return 2
end
-- 扣减库存
redis.call('incrby',stockKey,-1)
-- 添加到已售订单集合
redis.call('sadd',orderKey,userId)
-- 发送消息到队列中
redis.call('xadd','stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

如何启动时报错:io.lettuce.core.RedisCommandExecutionException: NOGROUP No such key 'stream.orders' or consumer group 'g1' in XREADGROUP with GROUP option

"ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically."

在redis客户端执行:

bash 复制代码
Windows:0>XGROUP CREATE stream.orders g1 0 MKSTREAM
"OK"

这里主要是使用了redis的消息队列,其实还可以使用RabbitMQ、RocketMQ、kafka等去整合实现,后面会更新企业级秒杀业务~~~

相关推荐
凡人的AI工具箱2 分钟前
40分钟学 Go 语言高并发:Pipeline模式(一)
开发语言·后端·缓存·架构·golang
achaoyang11 分钟前
【Python中while循环】
开发语言·python
呆呆小雅13 分钟前
C# 封装
java·开发语言·c#
蒜蓉大猩猩20 分钟前
Vue.js - 组件化编程
开发语言·前端·javascript·vue.js·前端框架·ecmascript
南鸳61033 分钟前
Scala:根据身份证号码,输出这个人的籍贯
开发语言·后端·scala
Mr.Demo.36 分钟前
[RabbitMQ] 保证消息可靠性的三大机制------消息确认,持久化,发送方确认
分布式·rabbitmq
eclipsercp1 小时前
PyQt5:Python GUI开发的超级英雄
开发语言·python·qt
小扳1 小时前
微服务篇-深入了解使用 RestTemplate 远程调用、Nacos 注册中心基本原理与使用、OpenFeign 的基本使用
java·运维·分布式·后端·spring·微服务·架构
军训猫猫头1 小时前
44.扫雷第二部分、放置随机的雷,扫雷,炸死或成功 C语言
c语言·开发语言
北巷!!1 小时前
宇信科技JAVA笔试(2024-11-26日 全部AK)
java·开发语言·科技