优惠券秒杀项目
一、添加优惠券的同时,将优惠券信息,以及用户列表放到redis中
@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 中,包括库存、开始时间和结束时间,使用 hash
Map<String, String> voucherData = new HashMap<>();
voucherData.put("stock", String.valueOf(voucher.getStock()));
voucherData.put("beginTime", String.valueOf(voucher.getBeginTime().toInstant(ZoneOffset.UTC).toEpochMilli()));
voucherData.put("endTime", String.valueOf(voucher.getEndTime().toInstant(ZoneOffset.UTC).toEpochMilli()));
stringRedisTemplate.opsForHash().putAll("seckill:voucher:" + voucher.getId(), voucherData);
// 加一个 set 集合,用来放抢到优惠券的用户 ID,防止重复抢
stringRedisTemplate.opsForSet().add("seckill:voucher:users:" + voucher.getId(), "0");
}
使用Redis生成订单ID:
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
//如果不存在,会自动创建然后开始自增
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
二、使用lua查看是否满足购买条件
2.1 编写lua脚本
-- KEYS 和 ARGV 是 Redis 提供的两种参数类型
-- KEYS[1] 是秒杀活动的哈希表键,包含库存、开始时间和结束时间
-- KEYS[2] 是记录用户ID的集合,防止重复购买
-- ARGV[1] 是用户ID,用于检查该用户是否已经购买过
-- ARGV[2] 是当前时间的时间戳(秒),用于与秒杀活动的开始和结束时间进行比较
-- 获取秒杀活动的信息
local stock = redis.call('hget', KEYS[1], 'stock')
local beginTime = redis.call('hget', KEYS[1], 'beginTime')
local endTime = redis.call('hget', KEYS[1], 'endTime')
-- 当前时间
local currentTime = tonumber(ARGV[2])
-- 检查活动是否在有效时间内
beginTime = tonumber(beginTime)
endTime = tonumber(endTime)
if currentTime < beginTime then
return -4 -- 活动尚未开始
end
if currentTime > endTime then
return -5 -- 活动已经结束
end
-- 检查库存是否足够
stock = tonumber(stock)
if stock <= 0 then
return -2 -- 库存不足
end
-- 获取用户ID
local userId = ARGV[1]
-- 检查用户是否已经购买过
local userExists = redis.call('sismember', KEYS[2], userId)
if userExists == 1 then
return -3 -- 用户已经购买过
end
-- 扣减库存
redis.call('hincrby', KEYS[1], 'stock', -1)
-- 记录用户购买信息
redis.call('sadd', KEYS[2], userId)
-- 成功,返回 1
return 1
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
ISeckillVoucherService seckillVoucherService;
@Autowired
RedisIdWorker redisIdWorker;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
// 秒杀 Lua 脚本, 当类加载的时候就执行
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
@Override
public Result seckillVoucher(Long voucherId) {
// 1. 使用 Redis 的 Lua 脚本,查看是否满足购买条件,包括时间、库存、用户是否已经购买
String voucherKey = "seckill:voucher:" + voucherId;
String userKey = "seckill:voucher:users:" + voucherId;
long currentTimeMillis = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli();
long userId = 1L; // 从用户会话中获取
// 2. 调用 Lua 脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT, // Lua 脚本
Arrays.asList(voucherKey, userKey), // Key 列表
String.valueOf(userId), // 参数列表:用户 ID 转为字符串
String.valueOf(currentTimeMillis) // 参数列表:当前时间转为字符串
);
// 3. 处理 Lua 脚本返回的结果
if (result != null) {
int r = result.intValue();
switch (r) {
case -2:
return Result.fail("库存不足!");
case -3:
return Result.fail("您已购买过该优惠券!");
case -4:
return Result.fail("秒杀活动尚未开始!");
case -5:
return Result.fail("秒杀活动已经结束!");
case 1:
// 成功抢购,将订单信息发送到 RabbitMQ 队列中
VoucherOrder voucherOrder = createOrder(voucherId, userId);
rabbitTemplate.convertAndSend("seckill.direct", "order.routing.key", voucherOrder);
return Result.ok("抢购成功,订单正在处理中!");
default:
return Result.fail("未知错误!");
}
}
return Result.fail("操作失败!");
}
// 创建订单
private VoucherOrder createOrder(Long voucherId, Long userId) {
VoucherOrder order = new VoucherOrder();
order.setVoucherId(voucherId);
order.setUserId(userId);
order.setId(redisIdWorker.nextId("order")); // 生成订单 ID
return order;
}
}
package com.hmdp.consumer;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.service.IVoucherOrderService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class OrderConsumer {
@Autowired
private RedissonClient redissonClient;
@Autowired
private IVoucherOrderService voucherOrderService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "seckill.order.queue"),
exchange = @Exchange(name = "seckill.direct", type = ExchangeTypes.DIRECT),
key = "order.routing.key"
))
public void processOrder(VoucherOrder order) {
String lockKey = "order:lock:" + order.getId();
RLock lock = redissonClient.getLock(lockKey);
try {
boolean isLockAcquired = lock.tryLock(10, 5, TimeUnit.SECONDS);
if (isLockAcquired) {
try {
voucherOrderService.save(order);
} finally {
lock.unlock();
}
} else {
// 无法获取锁,可能日志记录
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 错误处理
} catch (Exception e) {
// 错误处理
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hmdp</groupId>
<artifactId>hm-dianping</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hm-dianping</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--redission-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
server:
port: 8081
spring:
application:
name: hmdp
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC
username: root
password: root
redis:
host: 192.168.0.103
port: 6379
password: 123
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
rabbitmq:
host: 192.168.0.103 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /seckill # 虚拟主机
username: seckill # 用户名
password: 123123 # 密码
jackson:
default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
type-aliases-package: com.hmdp.entity # 别名扫描包
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.hmdp: debug
org.springframework.amqp: DEBUG