MySQL版本
1.数据准备
#抢购活动表
DROP TABLE IF EXISTS `voucher`;
CREATE TABLE `voucher` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`voucher_id` int(11) NULL DEFAULT NULL,
`amount` int(11) NULL DEFAULT NULL,
`start_time` datetime(0) NULL DEFAULT NULL,
`end_time` datetime(0) NULL DEFAULT NULL,
`is_valid` int(11) NULL DEFAULT NULL,
`create_date` datetime(0) NULL DEFAULT NULL,
`update_date` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
#订单表
DROP TABLE IF EXISTS `voucher_order`;
CREATE TABLE `voucher_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(100) NULL DEFAULT NULL,
`voucher_id` int(11) NULL DEFAULT NULL,
`diner_id` int(11) NULL DEFAULT NULL,
`status` tinyint(1) NULL DEFAULT NULL COMMENT '订单状态:-1=已取消 0=未支付 1=已支付 2=已消费 3=已过期',
`order_type` int(11) NULL DEFAULT NULL COMMENT '订单类型:0=正常订单 1=抢购订单',
`create_date` datetime(0) NULL DEFAULT NULL,
`update_date` datetime(0) NULL DEFAULT NULL,
`is_valid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
插入抢购活动
http://localhost:8080/seckill/addSeckillVoucher
{
"voucherId":1,
"amount":100,
"startTime":"2023-01-30 08:00:00",
"endTime":"2023-03-30 08:00:00"
}
2.超卖(库存为负数)
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
//参数校验
if (Objects.isNull(voucherOrderParams)) {
return ServerResponse.createByErrorMessage("参数错误");
}
Integer voucherId = voucherOrderParams.getVoucherId();
if (Objects.isNull(voucherId) || voucherId < 0) {
return ServerResponse.createByErrorMessage("请选择需要抢购的代金券");
}
Integer dinerId = voucherOrderParams.getDinerId();
if (Objects.isNull(dinerId) || dinerId < 0) {
return ServerResponse.createByErrorMessage("食客参数错误");
}
//判断此代金券是否加入抢购
Voucher voucher = voucherMapper.findVoucherByVoucherId(voucherId);
if (Objects.isNull(voucher)) {
return ServerResponse.createByErrorMessage("该代金券并未有抢购活动");
}
//判断是否有效
if (voucher.getIsValid() == 0) {
return ServerResponse.createByErrorMessage("该活动已结束");
}
//判断是否开始、结束
Date now = new Date();
if (now.before(voucher.getStartTime())) {
return ServerResponse.createByErrorMessage("该抢购还未开始");
}
if (now.after(voucher.getEndTime())) {
return ServerResponse.createByErrorMessage("该抢购已结束");
}
//判断是否卖完
/**
* 超卖问题产生原因
* 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
* 会扣120次库存,同一个人产生120个订单
*/
if (voucher.getAmount() < 1) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
/**
* 一个人多单问题产生原因
* 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
* 会扣20次库存,同一个人产生20个订单
*/
int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
if (voucherOrderCountFromDb > 0) {
return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
}
//扣库存
int count = voucherMapper.decreaseStock(voucherId);
if (count == 0) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//下单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setDinerId(dinerId);
voucherOrder.setVoucherId(voucherId);
String orderNo = UUID.randomUUID().toString();
voucherOrder.setOrderNo(orderNo);
voucherOrder.setOrderType(1);
voucherOrder.setStatus(0);
int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
if (saveCount == 0) {
return ServerResponse.createByErrorMessage("用户抢购失败");
}
return ServerResponse.createBySuccessMessage("抢购成功");
}
3.一人多单问题
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
//......
//判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
/**
* 一个人多单问题产生原因
* 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
* 会扣20次库存,同一个人产生20个订单
*/
int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
if (voucherOrderCountFromDb > 0) {
return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
}
//扣库存
int count = voucherMapper.decreaseStock(voucherId);
if (count == 0) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//下单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setDinerId(dinerId);
voucherOrder.setVoucherId(voucherId);
String orderNo = UUID.randomUUID().toString();
voucherOrder.setOrderNo(orderNo);
voucherOrder.setOrderType(1);
voucherOrder.setStatus(0);
int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
if (saveCount == 0) {
return ServerResponse.createByErrorMessage("用户抢购失败");
}
//......
}
Redis版本
1.数据准备
#订单表
DROP TABLE IF EXISTS `voucher_order`;
CREATE TABLE `voucher_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(100) NULL DEFAULT NULL,
`voucher_id` int(11) NULL DEFAULT NULL,
`diner_id` int(11) NULL DEFAULT NULL,
`status` tinyint(1) NULL DEFAULT NULL COMMENT '订单状态:-1=已取消 0=未支付 1=已支付 2=已消费 3=已过期',
`order_type` int(11) NULL DEFAULT NULL COMMENT '订单类型:0=正常订单 1=抢购订单',
`create_date` datetime(0) NULL DEFAULT NULL,
`update_date` datetime(0) NULL DEFAULT NULL,
`is_valid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
插入抢购活动
http://localhost:8080/seckill/addSeckillVoucher
{
"voucherId":1,
"amount":100,
"startTime":"2023-01-30 08:00:00",
"endTime":"2023-03-30 08:00:00"
}
3.超卖(库存为负数)
(1).问题分析
@Transactional(rollbackFor = Exception.class)
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
//......
//判断是否卖完
/**
* 超卖问题产生原因
* 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
* 会扣120次库存,同一个人产生120个订单
*/
if (voucher.getAmount() < 1) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
/**
* 一个人多单问题产生原因
* 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
* 会扣20次库存,同一个人产生20个订单
*/
int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
if (voucherOrderCountFromDb > 0) {
return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
}
//扣库存
/**
* 库存扣为负数问题产生原因
* 先查后扣两步操作,不是原子操作
* 使用Redis+lua解决超卖问题
*/
long count = redisTemplate.opsForHash().increment(key,"amount",-1);
//下单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setDinerId(dinerId);
voucherOrder.setVoucherId(voucherId);
String orderNo = UUID.randomUUID().toString();
voucherOrder.setOrderNo(orderNo);
voucherOrder.setOrderType(1);
voucherOrder.setStatus(0);
int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
if (saveCount == 0) {
return ServerResponse.createByErrorMessage("用户抢购失败");
}
return ServerResponse.createBySuccessMessage("抢购成功");
}
(2).解决方案
-- KEYS[1]指的voucher:1,KEYS[2]指的是amount
if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then
-- 获取库存
local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));
if (stock > 0) then
redis.call('hincrby', KEYS[1], KEYS[2], -1);
return stock;
end;
return 0;
end;
@Configuration
public class RedisTemplateConfiguration {
@Bean
public DefaultRedisScript<Long> stockScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//放在和application.yml 同层目录下
redisScript.setLocation(new ClassPathResource("stock.lua"));
redisScript.setResultType(Long.class);
return redisScript;
}
}
@Transactional(rollbackFor = Exception.class)
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
//......
//判断是否卖完
/**
* 超卖问题产生原因
* 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
* 会扣120次库存,同一个人产生120个订单
*/
if (voucher.getAmount() < 1) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
/**
* 一个人多单问题产生原因
* 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
* 会扣20次库存,同一个人产生20个订单
* 扣库存和下订单上锁解决一人多单问题
*/
int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
if (voucherOrderCountFromDb > 0) {
return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
}
//扣库存
/**
* 库存扣为负数问题产生原因
* 先查后扣两步操作,不是原子操作
* 使用Redis+lua解决超卖问题
*/
List<String> keys = new ArrayList<>();
keys.add(key);
keys.add("amount");
Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
if (amount == 0) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//下单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setDinerId(dinerId);
voucherOrder.setVoucherId(voucherId);
String orderNo = UUID.randomUUID().toString();
voucherOrder.setOrderNo(orderNo);
voucherOrder.setOrderType(1);
voucherOrder.setStatus(0);
int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
if (saveCount == 0) {
return ServerResponse.createByErrorMessage("用户抢购失败");
}
return ServerResponse.createBySuccessMessage("抢购成功");
}
3.一人多单问题
(1).问题分析
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
//......
//判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
/**
* 一个人多单问题产生原因
* 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
* 会扣20次库存,同一个人产生20个订单
*/
int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
if (voucherOrderCountFromDb > 0) {
return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
}
//扣库存
List<String> keys = new ArrayList<>();
keys.add(key);
keys.add("amount");
Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
if (count == 0) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//下单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setDinerId(dinerId);
voucherOrder.setVoucherId(voucherId);
String orderNo = UUID.randomUUID().toString();
voucherOrder.setOrderNo(orderNo);
voucherOrder.setOrderType(1);
voucherOrder.setStatus(0);
int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
if (saveCount == 0) {
return ServerResponse.createByErrorMessage("用户抢购失败");
}
//......
}
(2).解决方案
@Transactional(rollbackFor = Exception.class)
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
//......
//判断是否卖完
/**
* 超卖问题产生原因
* 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
* 会扣120次库存,同一个人产生120个订单
*/
if (voucher.getAmount() < 1) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
/**
* 一个人多单问题产生原因
* 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
* 会扣20次库存,同一个人产生20个订单
* 扣库存和下订单上锁解决一人多单问题
*/
int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
if (voucherOrderCountFromDb > 0) {
return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
}
//Redisson分布式锁,锁一个账号只能购买一次
String lockName = RedisKeyConstant.lock_key.getKey() + dinerId + ":" + voucherId;
long expireTime = voucher.getEndTime().getTime() - now.getTime();
RLock lock = redissonClient.getLock(lockName);
try {
//Redisson分布式锁处理
boolean isLocked = lock.tryLock(expireTime, TimeUnit.MILLISECONDS);
if (isLocked) {
//扣库存
/**
* 库存扣为负数问题产生原因
* 先查后扣两步操作,不是原子操作
* 使用Redis+lua解决超卖问题
*/
List<String> keys = new ArrayList<>();
keys.add(key);
keys.add("amount");
Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
if (amount == 0) {
return ServerResponse.createByErrorMessage("该券已经卖完了");
}
//下单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setDinerId(dinerId);
voucherOrder.setVoucherId(voucherId);
String orderNo = UUID.randomUUID().toString();
voucherOrder.setOrderNo(orderNo);
voucherOrder.setOrderType(1);
voucherOrder.setStatus(0);
int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
if (saveCount == 0) {
return ServerResponse.createByErrorMessage("用户抢购失败");
}
}
}catch (Exception e) {
//手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//Redisson解锁
lock.unlock();
}
return ServerResponse.createBySuccessMessage("抢购成功");
}
Jmeter使用
1.刷单
(1).添加线程组
(2).添加HTTP请求
(3).添加HTTP请求头信息
(4).查看结果树
(5).保存测试计划并进行测试
2.500用户并发
(1).添加线程组
(2).添加HTTP请求
dinnerId变量需要从CSV文件中读取。
{
"voucherId":1,
"dinerId":${dinerId}
}
(3).新建CSV文件
第一行为定义在请求体的变量。
(4).添加CSV数据文件设置
(5).添加HTTP请求头信息
(6).查看结果树
(7).保存测试计划并进行测试