秒杀业务的优化思路:
基于阻塞队列的异步秒杀存在哪些问题?
@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);
stringRedisTemplate.opsForValue()
.set(RedisConstants.SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString());
}
---1.参数列表
--- 1.1 优惠券id
local voucherId=ARGV[1];
--- 1.2 用户ID
local userId=ARGV[2];
--- 2数据key
--- 2.1库存key ..表示拼接
local stockKey ='seckill:stock:'.. voucherId
--- 2.2 订单key
local orderKey ='seckill:order:'.. voucherId
--- 3.业务脚本
--- 3.1 判断库存是否充足
if (tonumber(redis.call('get',stockKey)) <=0) then
--- 3.2 库存不足
return 1
end
--- 3.2 判断用户是否已经下过单.如果存在就会返回1
if (redis.call('sismember',orderKey,userId)== 1)then
--- 3.3 存在即为重复下单,返回2
return 2
end
--- 3.4 扣减库存
redis.call('incrby',stockKey,-1)
--- 3.5 下单保存用户
redis.call('sadd',orderKey,userId)
return 0
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
private IVoucherOrderService proxy;
//创建阻塞队列
private BlockingQueue<VoucherOrder> orderTask=new ArrayBlockingQueue<>(1024*1024);
public Result seckillVoucher(Long voucherId) {
// 1.获取用户
Long userId = UserHolder.getUser().getId();
// 2.执行Lua脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,// 执行的Lua脚本
Collections.emptyList(),// 生成空集合
voucherId.toString(), userId.toString());// 脚本中需要获取的ARGV参数
// 3.判断结果不为0
if (result.intValue() !=0){
return Result.fail(1==result.intValue() ? "库存不足":"不能重复下单");
}
//4.为0,有购买资格,把下单信息保存到阻塞队列中
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdworker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setVoucherId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
orderTask.add(voucherOrder);
// 获取线程的代理对象
proxy=(IVoucherOrderService) AopContext.currentProxy();
return Result.ok(orderId);
}
import java.util.concurrent.ExecutorService;
//创建线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR=Executors.newSingleThreadExecutor();
@PostConstruct // 当前类初始化完成开始执行
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while (true){
try {
// 获取队列中的订单信息orderTask.take()获取队列中的第一个元素,如果队列中没有元素,则一直等待。
VoucherOrder voucherOrder = orderTask.take();
//创建订单
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常",e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//1.获取用户
Long userId = voucherOrder.getUserId();
//2.创建锁对象
RLock lock = redissonClient.getLock("lock:order" + userId);
//3.获取锁
boolean isLock = lock.tryLock();
// 4.判断是否获取锁成功
if (!isLock){
log.error("不允许重复下单");
return;
}
try {
proxy.createVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("创建订单异常",e);
}
}
@Override
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 1.查询用户
Long userId = voucherOrder.getUserId();
// 2 查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// 3 判断订单是否存在
if (count > 0) {
log.error("用户已购买过一次!");
}
// 4.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock=stock -1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0)//where id=? and stock > 0
.update();
if (!success){
log.error("库存不足");
return;
}
// 5.保存信息
save(voucherOrder);
}
消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
消息队列:存储和管理消息,也被称为消息代理(Message Broker)
生产者:发送消息到消息队列
消费者:从消息队列获取消息并处理消息
Redis提供了三种不同的方式来实现消息队列:
list结构:基于List结构模拟消息队列
PubSub:基本的点对点消息模型
Stream:比较完善的消息队列模型
消息队列和阻塞队列的区别:
因list结构和PubSub都无法解决消丢失的问题,本文主要使用Redis 5.0引入的Stream
---1.参数列表
--- 1.1 优惠券id
local voucherId=ARGV[1];
--- 1.2 用户ID
local userId=ARGV[2];
--- 1.3 订单id
local orderId=ARGV[3];
--- 2数据key
--- 2.1库存key ..表示拼接
local stockKey ='seckill:stock:'.. voucherId
--- 2.2 订单key
local orderKey ='seckill:order:'.. voucherId
--- 3.业务脚本
--- 3.1 判断库存是否充足
if (tonumber(redis.call('get',stockKey)) <=0) then
--- 3.2 库存不足
return 1
end
--- 3.2 判断用户是否已经下过单.如果存在就会返回1
if (redis.call('sismember',orderKey,userId)== 1)then
--- 3.3 存在即为重复下单,返回2
return 2
end
--- 3.4 扣减库存
redis.call('incrby',stockKey,-1)
--- 3.5 下单保存用户
redis.call('sadd',orderKey,userId)
--- 3.6发送消息到队列中 XADD stream.order * k1 v1 k2 v2 ...
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId)
return 0
import cn.hutool.core.bean.BeanUtil;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@PostConstruct // 当前类初始化完成开始执行
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable {
String queueName = "stream.orders";
@Override
public void run() {
while (true) {
try {
//1.从消息队列中获取订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAM STREAMS stream.order >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
// 2.判断消息获取是否成功
if (list == null || list.isEmpty()) {
//如果为空,则说明没有消息,则进行下一次循环
continue;
}
// 3.解析消息
MapRecord<String, Object, Object> entries = list.get(0);
Map<Object, Object> value = entries.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//创建订单
handleVoucherOrder(voucherOrder);
// ACK 确认
stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", entries.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
handlePendingList();
}
}
}
private void handlePendingList() {
while (true) {
try {
//1.从消息队列中获取订单信息 XREADGROUP GROUP g1 c1 COUNT 1 STREAM STREAMS stream.order 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(queueName, ReadOffset.from("0"))
);
// 2.判断消息获取是否成功
if (list == null || list.isEmpty()) {
//如果为空,则说明PendLing-list没有消息,则结束循环
break;
}
// 3.解析消息
MapRecord<String, Object, Object> entries = list.get(0);
Map<Object, Object> value = entries.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//创建订单
handleVoucherOrder(voucherOrder);
// ACK 确认
stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", entries.getId());
} catch (Exception e) {
log.error("处理Pending-list订单异常", e);
try {
Thread.sleep(50);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
}