【微服务】优惠券(有限)秒杀的实现思路--使用Redis预扣减优惠券库存,结合Redisson锁保证并发安全,通过异步消息队列更新数据库库存

介绍

即在订单微服务中使用Redis预扣减优惠券库存,并结合Redisson分布式锁防止超卖情况,然后通过异步消息队列最终扣减数据库中的库存,我们可以按照以下步骤进行代码实现。这里假设你已经配置好了Redis、RabbitMQ(或其他消息队列)以及Redisson。

1. 引入必要的依赖

确保你的pom.xml文件中包含以下依赖:

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. Redisson分布式锁与库存预扣减

创建一个服务类来处理优惠券的预扣减逻辑,使用Redisson分布式锁确保并发安全。

java 复制代码
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class CouponService {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public boolean grabCoupon(String couponId) {
        String lockKey = "lock:coupon:" + couponId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                // 获取当前库存和逻辑过期时间
                String key = "coupon_stock:" + couponId;
                String stockStr = redisTemplate.opsForValue().get(key);
                if (stockStr == null || isExpired(stockStr)) {
                    return false; // 库存不足或已过期
                }

                // 解析库存信息
                CouponStockWithExpire couponStockWithExpire = parseCouponStock(stockStr);
                if (couponStockWithExpire.getStock() <= 0) {
                    return false; // 库存不足
                }

                // 扣减库存
                couponStockWithExpire.setStock(couponStockWithExpire.getStock() - 1);
                updateCouponStock(couponId, couponStockWithExpire);

                // 发送消息到队列以最终扣减数据库库存
                sendDeductStockMessage(couponId);
                return true;
            } else {
                return false; // 获取锁失败
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private boolean isExpired(String stockStr) {
        // 假设stockStr格式为"stock|expireTime"
        String[] parts = stockStr.split("\\|");
        long expireTime = Long.parseLong(parts[1]);
        return System.currentTimeMillis() > expireTime;
    }

    private CouponStockWithExpire parseCouponStock(String stockStr) {
        String[] parts = stockStr.split("\\|");
        int stock = Integer.parseInt(parts[0]);
        long expireTime = Long.parseLong(parts[1]);
        return new CouponStockWithExpire(stock, expireTime);
    }

    private void updateCouponStock(String couponId, CouponStockWithExpire couponStockWithExpire) {
        redisTemplate.opsForValue().set("coupon_stock:" + couponId,
                couponStockWithExpire.getStock() + "|" + couponStockWithExpire.getExpireTime());
    }

    private void sendDeductStockMessage(String couponId) {
        rabbitTemplate.convertAndSend("coupon.deduct.exchange", "coupon.deduct.routingKey", couponId);
    }
}

3. 消息消费者

创建一个消息消费者来监听队列并执行数据库中的库存扣减操作。

java 复制代码
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class CouponDeductListener {

    @RabbitListener(queues = "coupon.deduct.queue")
    public void handleDeductStock(String couponId) {
        // 在这里执行实际的数据库库存扣减逻辑
        System.out.println("Deducting stock for coupon: " + couponId);
        // 假设有一个方法deductStockInDatabase来扣减数据库中的库存
        deductStockInDatabase(couponId);
    }

    private void deductStockInDatabase(String couponId) {
        // 实现具体的数据库库存扣减逻辑
    }
}

4. 配置RabbitMQ

确保你的application.yml中正确配置了RabbitMQ连接信息和交换机、队列等信息:

yaml 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

  # 定义交换机和队列
rabbitmq:
  exchanges:
    coupon:
      name: coupon.deduct.exchange
      type: direct
  queues:
    coupon:
      name: coupon.deduct.queue
  bindings:
    coupon:
      destination: coupon.deduct.queue
      exchange: coupon.deduct.exchange
      routing-key: coupon.deduct.routingKey

5. 控制器层调用

在控制器中定义API接口供客户端调用:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private CouponService couponService;

    @PostMapping("/grabCoupon")
    public String grabCoupon(@RequestParam String couponId) {
        if (couponService.grabCoupon(couponId)) {
            return "Coupon grabbed successfully!";
        } else {
            return "Failed to grab coupon.";
        }
    }
}

6.定时任务清理过期优惠券Key

根据业务设置定时任务

使用Spring的@Scheduled注解创建一个定时任务,定期扫描并删除已过期的优惠券键。

java 复制代码
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
public class ExpiredCouponCleaner {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Scheduled(fixedRate = 604800000) // 每周执行一次
    public void cleanExpiredCoupons() {
        Set<String> keys = redisTemplate.keys("coupon_stock:*");
        for (String key : keys) {
            String stockStr = redisTemplate.opsForValue().get(key);
            if (isExpired(stockStr)) {
                redisTemplate.delete(key); // 删除过期的键
            }
        }
    }

    private boolean isExpired(String stockStr) {
        // 同grabCoupon方法中的isExpired实现
        String[] parts = stockStr.split("\\|");
        long expireTime = Long.parseLong(parts[1]);
        return System.currentTimeMillis() > expireTime;
    }
}

通过这种方式,我们实现了在订单微服务中使用Redis预扣减优惠券库存,并结合Redisson分布式锁确保并发安全性,随后通过异步消息队列最终扣减数据库中的库存。这种方法不仅提高了系统的响应速度和处理高并发能力,同时也确保了数据的一致性和避免了超卖问题的发生。

相关推荐
云飞云共享云桌面1 小时前
医疗器械设备研发:多人同时操作 SolidWorks,怎样依靠单台服务器替代多工作站
运维·服务器·网络·数据库·制造
知行合一。。。7 小时前
RAG--03--Milvus基本用法
数据库·oracle·milvus
TDengine (老段)7 小时前
TDengine 线程模型 — 网络、调度、执行
大数据·数据库·物联网·制造·时序数据库·tdengine·涛思数据
上学的小垃圾8 小时前
01-数据库系统概述
数据库
二进制漫游记9 小时前
FastAPI项目集成 Qdrant向量数据库+阿里云Embedding完整实战(工具封装+业务调用)
数据库·python·阿里云·embedding
2501_928996229 小时前
GPT-4o换DeepSeek迁移成本多少?中科热备解析API聚合平台技术账本
前端·数据库·人工智能
莫得感情 o9 小时前
Redis 05 · 持久化:RDB 与 AOF 怎么保证数据不丢
redis·缓存
curd_boy10 小时前
【Redis】Redis从缓存到AI向量平台
人工智能·redis·缓存
泡干脆面就番茄11 小时前
MySQL_子查询_分页查询与联合查询详解
数据库·mysql
莫得感情 o11 小时前
Redis 02 · 数据结构:String 到 Stream 怎么选
redis