秒杀优化
原先的秒杀功能虽然完善,但性能不是最佳,并发能力不强。如上图,所有业务逻辑串行执行,那总的执行时间就是每一个功能的执行时间,但是其中还有四个对数据库的操作并且有两个写操作,我们知道数据库的性能其实不好,所以这整个秒杀功能的性能固然不好。
但是整个秒杀的业务逻辑可以分为两部分,查询资格和减库存创订单。我们可以把查询资格和创订单分开。就好像饭店的前台和后厨一样,分工合作。一个线程查询有没有资格,另一个线程创建订单,这样异步执行,提高性能。
而查询资格是查数据库,性能不高。我们可以把数据放缓存里,查缓存来提高性能。
那问题就是怎么把数据存进redis里面,选什么类型的数据。查询优惠券库存用string就行,查是否下单要求一个key能存多个value,且里面的value唯一。所以使用set。业务流程使用lua脚本保证原子性,如下图:
秒杀优化实现
第一步功能实现:
@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
redisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), String.valueOf(voucher.getStock()));
}
第二步实现:lua脚本
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Mayn.
--- DateTime: 2025/2/28 上午10:45
---
--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' .. userId
--3.判断库存是否充足
if (tonumber(redis.call('get', stockKey)) <= 0) then
--3.1.库存不充足返回1
return 1
end
--4.库存充足,判断用户是否下单
if (redis.call('sismember', orderKey, userId) == 1)then
--4.1.已下单,返回2
return 2
end
--5.扣库存
redis.call('incrby',stockKey,-1)
--6.创建订单
redis.call('sadd',orderKey,userId)
return 0
java里面执行lua脚本
//LUA脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
//静态代码块初始化lua脚本
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));//找位置
SECKILL_SCRIPT.setResultType(Long.class);//设置返回值类型
}
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//1、执行lua脚本
Long result = redisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),userId.toString()
);
//2、判断返回结果
int r = result.intValue();
if (r != 0) {
//2.1、返回结果不为0,返回异常信息
return r == 1? Result.fail("库存不充足"):Result.fail("一人只能买一单!");
}
//2.2、返回结果为0,TODO 存入阻塞队列
Long orderId = redisIdWorker.nextId("order");
//3、返回订单id
return Result.ok(orderId);
}
第三步:存入阻塞队列
//创建阻塞队列
BlockingQueue<VoucherOrder> orderTask = new ArrayBlockingQueue<>(1024 * 1024);
//2.2、返回结果为0,存入阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
Long orderId = redisIdWorker.nextId("order");
//2.3、设置订单的各个id
//2.4、代金券id
voucherOrder.setVoucherId(voucherId);
//2.5、用户id
voucherOrder.setUserId(userId);
//2.6、订单id
voucherOrder.setId(orderId);
//2.7、将订单存入阻塞队列
orderTask.add(voucherOrder);
第四步:创建异步线程,完成异步下单
创建线程池,创建线程(像下面这样写)
先创建一个线程池,然后创建一个内部类继承Runnable接口,里面重写run方法,run方法里面写业务逻辑。
由于我们这个下单操作是spring服务器启动就要开始的,所以服务启动后就要开启这个线程执行业务。用到注解@PostConstruct,如下,在里面用线程池submit实现Runnable的类。
//创建线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
@PostConstruct
public void init() {
executorService.submit(new voucherOrderHandler());
}
private class voucherOrderHandler implements Runnable{
@Override
public void run() {
while (true) {
try {
//1、获取阻塞队列中的订单
VoucherOrder order = orderTask.take();
//2、创建订单
handleVoucherOrder(voucherOrder);
} catch (InterruptedException e) {
log.error("下单异常:{}",e.getMessage());
}
}
}
}
创建订单
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//userId要从线程池里取
Long userId = voucherOrder.getUserId();
//创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
if (!isLock) {
//获取锁失败
log.error("获取锁失败。");
return;
}
try {
proxy.createVoucherOrder(voucherOrder);
} finally {
lock.unlock();
}
}
IVoucherOrderService proxy;//代理对象要在外面获取
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
//5、确保一人一单
Long userId = voucherOrder.getUserId();
int count = query().eq("voucher_id", voucherOrder.getVoucherId()).eq("user_id", userId).count();
if (count > 0) {
log.error("一人只能一单!");
}
//6、库存充足,扣减库存
boolean success = seckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", voucherOrder.getVoucherId())
.gt("stock", 0)
.update();
if (!success) {
log.error("库存不足!");
}
//7.4、保存订单
save(voucherOrder);
}
userID不能从Userholder里面拿了,要从线程池里拿。//代理对象要在外面获取。
基于jvm的阻塞队列完成优化秒杀存在问题,如上图。因为jvm的阻塞队列是在内存上工作的。