秒杀系统-下单解决方案(从0到1)

下单解决方案

单机版(不考虑库存问题):

  • 普通下单——不考虑库存、不考虑超卖、不考虑并发问题,只考虑性能问题。

单机版(考虑库存问题):

  • 程序锁。
  • aop锁。
  • 队列(blockingQueue)

分布式:

  • 数据库锁(悲观锁、乐观锁)。
  • 分布式锁。
  • 队列(mq)

库存控制:

下单操作的时候,不进行库存控制,出现同一件商品被售卖多次的现象。也就是我们通常所说的超卖现象。(纠正概念:超卖不是把商品库存卖成负数,而是同一件商品被卖多次


单机版(不考虑库存问题)

普通下单

不考虑库存、不考虑超卖、不考虑并发问题,只考虑性能问题。
没有任何锁操作,直接减库存加销量,直接下单(向订单库中直接插入订单数据——以上操作都是直接操作数据库)

private void startSubmitOrder() {
        //查询商品信息
        seckillGoods.findOneById(商品Id);
        //判断商品是否上架、活动是否开始、库存有没有.....
        //商品库存减1、销量加1,更新商品表
        seckillGoods.update();
        //保存订单
        order.save();
}
优化:多线程下单
private void startSubmitOrderMultiThread() {
        //查询商品信息
        seckillGoods.findOneById(商品Id);
        //判断商品是否上架、活动是否开始、库存有没有.....
        //商品库存减1、销量加1,更新商品表
        seckillGoods.update();
		//保存订单(多线程方式下单)
		new Thread(() -> {
			order.save();
		}).start();
}
优化:缓存

活动开始之前把商品信息放入缓存中,查询商品直接从缓存中获取。

private void startSubmitOrderCache() {
		//从缓存中查询商品信息
		redisTemplate.get(商品Id);
        //判断商品是否上架、活动是否开始、库存有没有.....
        //商品库存减1、销量加1,更新商品表
        seckillGoods.update();
		//保存订单(多线程方式下单)
		new Thread(() -> {
			order.save();
		}).start();
}

10000用户同时下单,理论上库存还剩0,现库存还剩113个,说明程序锁出现超卖现象。

问题:10000个用户同时下单(他们都认为自己买到商品,实际上存在严重问题),只购买了887个商品。

单机版(考虑库存问题)

程序锁
// 定义全局程序锁(ReentrantLock)
private ReentrantLock reentrantLock = new ReentrantLock();

@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderReentrantLock() {
		try {
            //加锁
            reentrantLock.lock();
            //从缓存中查询商品信息
            redisTemplate.get(商品Id);
            //判断商品是否上架、活动是否开始、库存有没有.....
            //商品库存减1、销量加1,更新商品表
            seckillGoods.update();
            //保存订单
            order.save();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            reentrantLock.unlock();
        }
}

10000用户同时下单,理论上库存还剩0,现库存还剩281个,说明程序锁出现超卖现象。

库存无法控制的原因,和Spring事务有关系:

  • 事务回滚原理:方法抛出异常,反之提交。
  • 程序锁:事务提交时间的问题,也就是说锁释放了,事务还未提交,导致其它线程读取到脏数据。

解决方案:

  • 锁上移。
  • 手动提交事务。

AOP锁

利用Spring切面编程模式,实现AOP锁。

/**
 * @Author: LailaiMonkey
 * @Description:自定义aop切面注解
 * @Date:Created in 2020-12-06 14:19
 * @Modified By:
 */
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
    String description() default "";
}

aop拦截器:

/**
 * @Author: LailaiMonkey
 * @Description:注解拦截
 * @Date:Created in 2020-12-06 14:21
 * @Modified By:
 */
@Component
@Scope
@Aspect
//order越小越先执行
@Order(1)
public class LockAspect {
    private ReentrantLock reentrantLock = new ReentrantLock();

    @Around("@annotation(com.monkey.test.sdfsdaf.ServiceLock)")
    public Object lockAspect(ProceedingJoinPoint joinPoint) {
        //加锁
        Object object = null;
        try {
            reentrantLock.lock();
            object = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            //解锁
            reentrantLock.unlock();
        }
        return object;
    }
}

同普通下单业务逻辑,需要加拦截注解

@Transactional(rollbackFor = Exception.class)
@ServiceLock
private void startSubmitOrder() {
        //查询商品信息
        seckillGoods.findOneById(商品Id);
        //判断商品是否上架、活动是否开始、库存有没有.....
        //商品库存减1、销量加1,更新商品表
        seckillGoods.update();
        //保存订单
        order.save();
}

库存已经完美控制住!!!

队列

将下单信息放入队列,后台线程慢慢处理订单业务(订单异步处理)——blockingQueue
下单——数据放入队列——队列消费数据。

定义全局队列:

/**
 * @Author: LailaiMonkey
 * @Description:定义全局队列
 * @Date:Created in 2020-12-06 15:02
 * @Modified By:
 */
public class SeckillQueue {

    private final static BlockingQueue<OrderModel> BLOCKING_QUEUE = new LinkedBlockingDeque<>();

    //不可以实例化
    private SeckillQueue() {
    }

    private static class SingletonHolder {
        private static SeckillQueue queue = new SeckillQueue();
    }

    public static SeckillQueue getQueue() {
        return SingletonHolder.queue;
    }

    /**
     * 放入队列
     *
     * @param model
     * @return
     */
    public Boolean product(OrderModel model) {
        return BLOCKING_QUEUE.offer(model);
    }

    /**
     * 出队列
     *
     * @param model
     * @return
     */
    public OrderModel consumer() throws InterruptedException {
        return BLOCKING_QUEUE.take();
    }

    /**
     * 获得队列长度
     *
     * @param model
     * @return
     */
    public int size() {
        return BLOCKING_QUEUE.size();
    }
}

队列消费:

/**
 * @Author: LailaiMonkey
 * @Description:队列消费
 * @Date:Created in 2020-12-06 15:09
 * @Modified By:
 */
@Component
public class TaskRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        new Thread(() -> {
            while (true) {
                try {
                    OrderModel model = SeckillQueue.getQueue().consumer();
                    if (model != null) {
                        //商品库存减1、销量加1,更新商品表
                        seckillGoods.update();
                        //保存订单
                        order.save();
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }).start();
    }
}

下单:

@Transactional(rollbackFor = Exception.class)
@ServiceLock
private void startSubmitOrder() {
		//搭建下单模型
		OrderModel model = new OrderModel();
		model.set.......
        //放入队列 
		Boolean flag = SeckillQueue.getQueue().product(model);
		if (flag) {
			//通知用户下单成功
		} else {
			//秒杀失败
		}
}

队列大小为100,此时如果队列已经满了,放入队列的动作就会失败,也就是意味着下单失败,此时队列的大小根据后端业务处理能力进行设置。
使用队列作为缓存对象,减轻服务压力,提升服务吞吐能力。

优化:

  • 可以把放入队列过程改成多线程放入。

缺点:

  • BlockingQueue队列是内存队列,占用jvm内存大小,如果队列太大会和jvm进程抢占资源,导致性能下降,如果队列太小,导致下单队列满,会出现下单失败的现象。
  • 吞吐能力问题
  • 分布式情况下无法控制库存

解决方案:

  • 分布式库存、分布式部署、分布式队列

分布式

数据库悲观锁

查询商品库存加for update锁定该商品。

@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderBySqlLock() {
        //查询商品信息(select .... for update)
        seckillGoods.findOneByIdSqlLock(商品Id);
        //判断商品是否上架、活动是否开始、库存有没有.....
        //商品库存减1、销量加1,更新商品表
        seckillGoods.update();
        //保存订单
        order.save();
}

库存控制没有问题,性能差

数据库乐观锁

在商品表加一个version字段,当多线程(并发)查询修改时候就可以进行版本比对数据修改,防止数据进行脏读。

@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderBySqlLock() {
        //查询商品信息
        seckillGoods.findOneById(商品Id);
        //判断商品是否上架、活动是否开始、库存有没有.....
        //商品库存减1、销量加1,更新商品表,判断数据库version和查询version是否一样
        //update ..... where version = 查询商品的version
        int flag = seckillGoods.update();
        //乐观锁更新成功后方可继续操作
        if (flag > 0) {
			//保存订单
        	order.save();
		} else {
			//提示太火爆了
		}
}

由于数据库锁进行操作磁盘数据,性能消耗比较高,因此可以使用内存锁提高下单的性能。

分布式锁
@Transactional(rollbackFor = Exception.class)
private void startSubmitOrderReentrantLock() {
		boolean result = false;
		try {
            //使用redisson加锁,尝试等待3s,上锁后20s自动解除
            result = redisson.tryLock("seckill_goods_lock" + 商品Id, TimeUnit.SECONDS, 3, 10);
            if (result) {
				//从缓存中查询商品信息
				redisTemplate.get(商品Id);
	            //判断商品是否上架、活动是否开始、库存有没有.....
	            //商品库存减1、销量加1,更新商品表
	            seckillGoods.update();
	            //保存订单
	            order.save();
			} else {
				//提示太火爆
			}
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            if (result) {
				redisson.unlock("seckill_goods_lock" + 商品Id);
			}
        }
}

redis锁还是会和本地事务出现冲突,但是由于操作reids是远程操作,通过网络io进行,有网络延迟,因此基本不会出现库存控制失败的现象。redis在数据进行操作是异步的,延迟的,因些当数据事务提交后redis才释放,所以这样没问题。

确保100%没有问题需要redis升级为aop锁,把上述aop锁ReentrantLock换成redis的lock即可。

mq

同上,把blockingQueue换成rabbitMq即可。


思考

项目使用分布式部署,数据一致性处理非常困难:

  • 网络拉动
  • 网络延迟
  • 服务宕机
  • 机器宕机
  • 程序崩溃
  • 异常

以上问题必须然会发生,所以在处理数据一致性方面必有取舍,强一致性(基于数据库)——CAP(分布式)、Base理论(最终一致性)

你可能感兴趣的:(分布式,秒杀,下单,库存)