秒殺系統-下單解決方案(從0到1)

秒殺系統-下單解決方案(從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理論(最終一致性)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章