赞
踩
(1)在一人一单优惠券秒杀中,先是解决了因线程安全导致的超卖问题,为了满足一人一单的业务需求需要给查询订单表查询库存表上锁,但是synchronized只能满足单机环境下线程安全,在分布式环境对多个JVM需要使用分布式锁,于是手写分布式锁,但是在极端情况下,例如业务超时,会出现“误删”问题,手写锁无法解决,于是引入redisson,借助看门狗机制可以有效解决这个问题。
(2)在整个过程中,涉及到了多次查询和写入数据库操作,通过Jmeter测试性能,最长响应时间达到了秒级,用户体验很差,想到借助异步秒杀(用到了Stream流),并且将两次查询数据库的操作放到redis中以Lua脚本方式(主要是为了保证原子性),将“处理订单”的操作放到消息队列当中,让其他线程接收并且处理。
我们只需要执行一个判断的逻辑,如果库存足够并且满足“一人一单”,就给用户响应下单成功的结果。
当用户发起请求时,会请求到nginx,nginx会访问tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤:
1、查询优惠卷
2、判断秒杀库存是否足够
3、查询订单
4、校验是否是一人一单
5、扣减库存
6、创建订单
我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池,当然这里边有两个难点。
当用户下单之后,判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作。
我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。
内存限制/数据安全都无法得到保证
//异步处理线程池 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 { // 1.获取队列中的订单信息 VoucherOrder voucherOrder = orderTasks.take(); // 2.创建订单 handleVoucherOrder(voucherOrder); } catch (Exception e) { log.error("处理订单异常", e); } } } private void handleVoucherOrder(VoucherOrder voucherOrder) { //1.获取用户 Long userId = voucherOrder.getUserId(); // 2.创建锁对象 RLock redisLock = redissonClient.getLock("lock:order:" + userId); // 3.尝试获取锁 boolean isLock = redisLock.lock(); // 4.判断是否获得锁成功 if (!isLock) { // 获取锁失败,直接返回失败或者重试 log.error("不允许重复下单!"); return; } try { //注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效 proxy.createVoucherOrder(voucherOrder); } finally { // 释放锁 redisLock.unlock(); } } //a private BlockingQueue<VoucherOrder> orderTasks =new ArrayBlockingQueue<>(1024 * 1024); @Override public Result seckillVoucher(Long voucherId) { Long userId = UserHolder.getUser().getId(); long orderId = redisIdWorker.nextId("order"); // 1.执行lua脚本 Long result = stringRedisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(), String.valueOf(orderId) ); int r = result.intValue(); // 2.判断结果是否为0 if (r != 0) { // 2.1.不为0 ,代表没有购买资格 return Result.fail(r == 1 ? "库存不足" : "不能重复下单"); } VoucherOrder voucherOrder = new VoucherOrder(); // 2.3.订单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) { Long userId = voucherOrder.getUserId(); // 5.1.查询订单 int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count(); // 5.2.判断是否存在 if (count > 0) { // 用户已经购买过了 log.error("用户已经购买过了"); return ; } // 6.扣减库存 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 ; } save(voucherOrder); }
-- 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.判断库存是否充足 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 -1 redis.call('incrby', stockKey, -1) -- 3.5.下单(保存用户)sadd orderKey userId redis.call('sadd', orderKey, userId) -- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ... redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId) return 0
public Result seckillVoucher(Long voucherId){ Long userId = UserHolder.getUser().getId(); //生成订单编号 long orderId = redisIdWorker.nextId("order"); // 执行lua脚本 // 会在lua脚本当中将订单放到stream流中 Long result = stringRedisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(), String.valueOf(orderId) ); int success = result.intValue(); if (success != 0) return Result.fail(success == -1 ? "库存不足" : "不能重复下单"); //将订单保存至阻塞队列 //3.返回订单id return Result.ok(orderId); }
//异步秒杀业务更改,执行lua脚本之后异步下单 private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor(); @PostConstruct private void init(){ SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); } static { SECKILL_SCRIPT = new DefaultRedisScript<>(); SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); SECKILL_SCRIPT.setResultType(Long.class); } //子线程 private class VoucherOrderHandler implements Runnable{ @Override public void run() { while (true){ try { // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 > List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create("stream.orders", ReadOffset.lastConsumed()) ); //2.判断订单是否为空 if (list == null || list.isEmpty()) continue; //3.解析数据 MapRecord<String,Object,Object> record = list.get(0); //使用value来填充voucherOrder的属性 Map<Object, Object> value = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true); //3.创建订单 createVoucherOrder2(voucherOrder); //4.确认消息xack stringRedisTemplate.opsForStream().acknowledge("s1","g1",record.getId()); }catch (Exception e){ e.printStackTrace(); handlePendingList(); } } } private void handlePendingList() { while (true){ try { // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 > List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create("stream.orders", ReadOffset.lastConsumed()) ); //2.判断订单是否为空 if (list == null || list.isEmpty()) continue; //3.解析数据 //因为只读取一条记录所以list.get(0) MapRecord<String,Object,Object> record = list.get(0); //使用value来填充voucherOrder的属性 Map<Object, Object> value = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true); //3.创建订单 createVoucherOrder2(voucherOrder); //4.确认消息xack stringRedisTemplate.opsForStream().acknowledge("s1","g1",record.getId()); }catch (Exception e){ log.error("处理订单异常{}",e.getMessage()); // handlePendingList(); } } } /** * 改造成为异步下单 * 创建订单 * @param voucherOrder 订单信息 */ private void createVoucherOrder2(VoucherOrder voucherOrder) { //添加逻辑:该用户是否已经已经下过单了 //防止黄牛恶意刷单 但是会存在和超卖相同的问题 同一个用户但是超卖了三张票 Long userId = voucherOrder.getUserId(); RLock lock = redissonClient.getLock("lock:order:" + userId); boolean tryLock = lock.tryLock(); if (!tryLock) { log.error("不允许重复下单"); return; } try { Long voucherId = voucherOrder.getVoucherId(); //3.查询订单 Integer count = query() .eq("user_id", userId) .eq("voucher_id", voucherId) .count(); //4.校验一人一单 if (count > 0) { log.error("无法重复下单"); return; } //5.库存扣减 超减问题并发量3000的情况下 超卖10张 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId) .gt("stock", 0).update(); if (!success) { log.error("库存不足,下单失败"); return; } //6.添加订单 voucherOrderService.save(voucherOrder); }catch (Exception e){ log.error("下单失败{}",e.getMessage()); }finally { lock.unlock(); } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。