介绍
即在订单微服务中使用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分布式锁确保并发安全性,随后通过异步消息队列最终扣减数据库中的库存。这种方法不仅提高了系统的响应速度和处理高并发能力,同时也确保了数据的一致性和避免了超卖问题的发生。