目录
1.问题
在优惠券秒杀下单的整个业务流程中,因为每一步的逻辑都是串行执行的,并且数据库的并发能力本身较差,还要对数据库进行写操作,而且为了避免并发安全问题还加了分布式锁,导致整个业务的耗时时间太长,并发能力较差。因此我们采用基于阻塞队列实现异步秒杀下单,进而提高并发性能,减少业务的耗时。
2.实现思路
- 首先在新增秒杀优惠券的同时,将优惠券信息保存到Redis中
库存信息:
订单信息:
- 基于Lua脚本,判断库存余量、一人一单,决定用户是否有购买资格
- 如果可以购买,将优惠券id和用户id封装后存入阻塞队列
- 开启独立线程,不断从阻塞队列中获取信息,实现异步下单功能
3.代码实现
--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 这里的value为用户ID
local orderKey = 'seckill:order:' .. voucherId
--3.脚本业务
--3.1判断库存是否充足 get stockKey
if(tonumber(redis.call('get',stockKey)) <= 0) then
--库存不足,返回1
return 1
end
--3.2判断用户是否已经购买过 sismember orderKey userId
if(redis.call('sismember',stockKey,userId) == 1) then
--存在,则该用户重复下单,返回2
return 2
end
--3.3扣减库存 incrby stockKey -1
redis.call('incrby',stockKey,-1)
--3.4生成订单(保存用户)
redis.call('sadd',orderKey,userId)
/**
* 优惠券订单模块
*/
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIDWorker redisIDWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//初始化脚本对象,用于加载Lua脚本 这里定义为static是为了在类加载时就初始化该脚本对象,并且只会初始化一次,提高IO性能
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
//创建阻塞队列:当线程尝试从该队列中获取元素,若没有元素则线程阻塞,直到队列中有元素才会被唤醒
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
//创建单线程的线程池
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 {
//获取阻塞队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take();
//创建订单到数据库
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常",e);
}
}
}
}
//创建代理对象
private IVoucherOrderService proxy;
/**
* 创建订单到数据库,独立线程会执行此方法
* @param voucherOrder
*/
public void handleVoucherOrder(VoucherOrder voucherOrder){
//获取用户ID 这里不能使用ThreadLocal获取用户信息,因为此时是新的独立线程在执行该方法
Long userId = voucherOrder.getUserId();
//创建锁对象
SimpleRedisLock redisLock = new SimpleRedisLock("order"+userId.toString(),stringRedisTemplate);
//尝试获取锁
boolean isLock = redisLock.tryLock(2);
//判断是否获取到
if (!isLock){
//获取锁失败,返回错误
log.error("不允许重复下单");
return;
}
try {
//创建订单到数据库
proxy.createVoucherOrder(voucherOrder);
} finally {
//释放锁
redisLock.unLock();
}
}
/**
* 优惠券秒杀下单功能:基于阻塞队列实现异步秒杀
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
//获取当前用户
Long userId = UserHolder.getUser().getId();
//执行Lua脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString());
//判断结果是否为0
int r = result.intValue();
if (r != 0){
//不为0,返回异常信息
return Result.fail(r==1 ? "库存不足" : "不能重复购买");
}
//为0,则有购买资格,将用户id,优惠券id,订单id保存到阻塞队列
//创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//订单id
long orderId = redisIDWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
voucherOrder.setUserId(userId);
//优惠券id
voucherOrder.setVoucherId(voucherId);
//将订单保存到阻塞队列
orderTasks.add(voucherOrder);
//获取代理对象(事务) 保证事务提交之后再释放锁
proxy = (IVoucherOrderService) AopContext.currentProxy();
//返回订单id
return Result.ok(orderId);
}
/**
* 优惠券订单生成
* @param voucherOrder
*/
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
//实现一人一单
Long userId = voucherOrder.getUserId();
//查询优惠券订单
int count = query().eq("voucher_id", voucherOrder.getVoucherId()).eq("user_id", userId).count();
//判断订单是否存在
if (count>0){
//该用户已经购买过了
log.error("不能重复购买");
return;
}
//扣减库存 update tb_seckill_voucher set stock=stock-1 where voucher_id=? and stock>0
boolean success = seckillVoucherService.update()
.setSql("stock = stock -1")
.eq("voucher_id", voucherOrder.getVoucherId())
.gt("stock",0)
.update();
//判断扣减是否成功
if (!success){
log.error("库存不足");
return;
}
//新增订单
save(voucherOrder);
}
}
4.基于阻塞队列存在的问题
1、内存限制问题
因为上述优化方式的阻塞队列是JDK提供的,它是基于JVM内存的,当有大量用户并发请求时,会产生大量的订单信息保存到阻塞队列中,就会造成虚拟机内存溢出的问题。
2、数据安全问题
因为阻塞队列中的订单信息是基于内存存储的,如果此时服务突然宕机了,那么这些内存中的数据就会丢失了,而用户已经完成了下单,数据库中却没有该订单信息,就会导致数据不一致的问题;或者如果某个线程从阻塞队列中取到了订单信息并开始执行下单任务,就在此时因为某种情况或异常导致该任务终止,导致订单信息并没有写入数据库中,而阻塞队列中也没有该订单信息了,造成数据丢失,就会出现数据不一致的问题。