前段接收的情况经过nginx负载均衡到每个tomcat上,而我们的业务流程中,由单个线程对业务进行串行执行,存在许多需要查询数据库的操作,而查询数据库操作本身是耗时的,而且我们还用到了分布式锁,其实大大降低了执行性能。
我们先在redis中做能否成功下单的验证,验证通过后,开启一个独立的异步线程,去完成下单的操作。
这样一来,减少了数据库的查询操作,将本来串行执行的业务分给了两个线程取执行,一个负责验证用户是否能够下单,一个负责完成下单动作。
redis中需要保存某个优惠券的库存,这个用string就可以实现,而对于一人一单的校验,就需要一对多的结构了,这个时候需要用到set
@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryVoucherOfShop(Long shopId) {
// 查询优惠券信息
List<Voucher> vouchers = getBaseMapper().queryVoucherOfShop(shopId);
// 返回结果
return Result.ok(vouchers);
}
@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中
stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(),voucher.getStock().toString());
}
}
-- 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.判断库存是否充足 get stockKey
if(tonumber(redis.call('get',stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember',orderKey,userId) == 1) then
-- 3.3.存在,说明重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1return
redis.call('incrby',stockKey,-1)
-- 3.5.下单(保存用户) sadd orderKey userId
redis.call('sadd',orderKey,userId)
return 0
判断用户是否有购买资格的业务代码:
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
@Resource
private RedisIdWorker redisIdWorker;
//在类加载的时候就将脚本读取好方便直接使用
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) throws InterruptedException {
//获取用户
Long userId = UserHolder.getUser().getId();
//1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);
//2.判断结果是否为0
int r = result.intValue();
if (r != 0) {
//2.1不为0,代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不可重复下单");
}
//2.2为0,有购买资格,把下单信息保存到阻塞队列
long orderId = redisIdWorker.nextId("order");
//TODO 保存阻塞队列
//3.返回订单id
return Result.ok(0);
}
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
@Resource
private RedisIdWorker redisIdWorker;
//在类加载的时候就将脚本读取好方便直接使用
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注解的方法在所在类初始化的时候执行
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while(true){
try {
//1.获取队列中的订单信息
//take()会检索并删除此队列的头,必要时等待,直到某个元素可用为止。
VoucherOrder voucherOrder = orderTasks.take();
//2.创建订单
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常",e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//获取用户id
Long userId = voucherOrder.getUserId();
//创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
//获取锁
boolean isLock = lock.tryLock();
if(!isLock){
//获取锁失败,返回错误或者重试
log.error("不允许重复下单");
}
try {
proxy.createVoucherOrder(voucherOrder);
}finally {
//释放锁
lock.unlock();
}
}
private IVoucherOrderService proxy;
@Override
public Result seckillVoucher(Long voucherId) throws InterruptedException {
//获取用户
Long userId = UserHolder.getUser().getId();
//1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);
//2.判断结果是否为0
int r = result.intValue();
if (r != 0) {
//2.1不为0,代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不可重复下单");
}
//2.2为0,有购买资格,把下单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
//2.3订单id
//redisIdWorker可以帮助生成id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//2.4用户id
voucherOrder.setUserId(userId);
//2.5代金券id
voucherOrder.setVoucherId(voucherId);
//2.6放入阻塞队列
orderTasks.add(voucherOrder);
//3.获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
//4.返回订单id
return Result.ok(orderId);
}
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
//5.一人一单
Long userId = UserHolder.getUser().getId();
//userId.toString().intern()确保了相同的userId过来,锁住的是一个String对象
//这里的toString()方法实际会创建一个新的字符串对象
//intern()则会返回字符串常量池中与新创建的字符串对象equal的字符串对象的引用
//从而保证相同的userId进行了toString().intern()返回的是同一个字符串对象的引用
//5.1查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
//5.2判断是否存在该订单
if (count > 0) {
//用户已经购买过了
log.error("用户已经购买过一次!");
}
//6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0)//更新的条件:where id = ? stock > 0
.update();
if (!success) {
//扣减失败
log.error("库存不足!");
}
save(voucherOrder);
}
}
将优惠券订单添加到阻塞队列中,并由异步线程不断从中取出优惠券订单并创建订单到数据库中。