關於秒殺系統的實現

背景

這個項目是從github上拉下來的一個項目,借鑑項目上的代碼來實現的秒殺系統,主要有
基於Mysql悲觀鎖,樂觀鎖實現,利用redis的watch監控,以及利用AtomicInteger的CAS機制特性等四種方法來實現高併發高負載的場景,也算是補充一下這塊知識的空白。

使用到的註解

1 ) @ControllerAdvice
全局捕獲異常類,主要用於配合@ExceptionHandler,只要作用在@RequestMapping上,所有的異常都會被捕獲,如果使用的話返回的異常類型一般需要加上@ResponseBody,因爲返回的數據類型是json格式的。

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    //不加ResponseBody的話會會報錯
    @ExceptionHandler(value = SecKillException.class)
    @ResponseBody
    public Message handleSecKillException(SecKillException secKillException){
        log.info(secKillException.getSecKillEnum().getMessage());
        return new Message(secKillException.getSecKillEnum());
    }
}

2 ) @Data
使用這個註解,Getter,Setter,equals,canEqual,hasCode,toString等方法會在編譯時自動加進去
3 ) @NoArgsConstructor
使用後創建一個無參構造函數

4 ) @AllArgsConstructor
使用後添加一個構造函數,該構造函數含有所有已聲明字段屬性參數

5 ) @PostConstruct
被註解的方法,在對象加載完依賴後執行,只執行一次

6 ) @Qualifier
表明那個參數纔是我們所需要的,需要注意的是@Qualifier的參數名稱爲我們之前定義的註解的名稱之一

7 ) @Value("${spring.datasource.url}")
加載properties文件中對應的字段

8 )@Primary
當一個接口有多個實現時,使用這個註解就可以實現默認採取它進行注入

9)@Scope
scope是一個非常關鍵的概念,定義了用戶在spring容器中的生命週期,也可以理解爲對象在spring容器中的創建方式
a singleton (單一實例)
此取值時表明容器中創建時只存在一個實例,所有引用此bean都是單一實例。
此外,singleton類型的bean定義從容器啓動到第一次被請求而實例化開始,只要容器不銷燬或退出,該類型的bean的單一實例就會一直存活

b prototype
spring容器在進行輸出prototype的bean對象時,會每次都重新生成一個新的對象給請求方,雖然這種類型的對象的實例化以及屬性設置等工作都是由容器負責的,但是隻要準備完畢,並且對象實例返回給請求方之後,容器就不在擁有當前對象的引用,請求方需要自己負責當前對象後繼生命週期的管理工作,包括該對象的銷燬。也就是說,容器每次返回請求方該對象的一個新的實例之後,就由這個對象“自生自滅”

c 還有session,global session,request等三種類型 這裏就不詳講

使用mysql的update行鎖悲觀鎖

用到的sql語句是這一句

<update id="updatePessLockInMySQL">
		update product set stock=stock-1
			where id=#{id} and stock>0
	</update>

根據Mysql的知識可以知道,update行會給指定的記錄加上記錄鎖,因此會封鎖索引記錄
上面的語句它會在 id相等的那一行的索引記錄上鎖,防止其他事務的插入更新。
主要看一下service層的方法

	@Transactional
    public SecKillEnum handleByPessLockInMySQL(Map<String, Object> paramMap) {
        Jedis jedis = redisCacheHandle.getJedis();
        Record record;

        Integer userId = (Integer) paramMap.get("userId");
        Integer productId = (Integer) paramMap.get("productId");

        User user = secKillMapper.getUserById(userId);
        Product product = secKillMapper.getProductById(productId);
        /**
         * 拿到用戶所買商品在redis中對應的key
         */
        String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());

        //判斷該用戶是否重複購買該商品
        boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
        if (isBuy) {
            log.error("用戶:" + user.getUsername() + "重複購買商品" + product.getProductName());
            throw new SecKillException(SecKillEnum.REPEAT);
        }

        /**
         * 判斷該商品的庫存  利用update行實現悲觀鎖 這裏應該取消事務的自動提交功能
         */
        boolean secKillSuccess = secKillMapper.updatePessLockInMySQL(product);
        if (!secKillSuccess) {
            log.error("商品:" + product.getProductName() + "庫存不足!");
            throw new SecKillException(SecKillEnum.LOW_STOCKS);
        }

        long result = jedis.sadd(hasBoughtSetKey, user.getId().toString());
        if (result > 0) {
            record = new Record(null, user, product, SecKillEnum.SUCCESS.getCode(), SecKillEnum.SUCCESS.getMessage(), new Date());
            log.info(record.toString());
            boolean insertFlag = secKillMapper.insertRecord(record);
            if (insertFlag) {
                log.info("用戶:" + user.getUsername() + "秒殺商品:" + product.getProductName() + "成功!");
                return SecKillEnum.SUCCESS;
            } else {
                log.error("系統錯誤!");
                throw new SecKillException(SecKillEnum.SYSTEM_EXCEPTION);
            }
        } else {
            log.error("用戶:" + user.getUsername() + "重複秒殺商品" + product.getProductName());
            throw new SecKillException(SecKillEnum.REPEAT);
        }
    }

注:@Transactional默認的是該方法執行完事務才進行提交

通過在數據庫中添加version字段來實現樂觀鎖

sql語句

<update id="updatePosiLockInMySQL">
		update product set stock=#{stock},version=version+1
			where id=#{id} AND version=#{version}
	</update>

在數據庫中添加版本號字段,當version相同時允許修改庫存

@Transactional
    public SecKillEnum handleByPosiLockInMySQL(Map<String, Object> paramMap) {
        Jedis jedis = redisCacheHandle.getJedis();
        Record record = null;

        Integer userId = (Integer) paramMap.get("userId");
        Integer productId = (Integer) paramMap.get("productId");
        User user = secKillMapper.getUserById(userId);
        Product product = secKillMapper.getProductById(productId);

        String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());
        boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
        if (isBuy) {
            log.error("用戶:" + user.getUsername() + "重複購買商品" + product.getProductName());
            throw new SecKillException(SecKillEnum.REPEAT);
        }
        //手動庫存減一
        int lastStock = product.getStock() - 1;
        if (lastStock >= 0) {
            product.setStock(lastStock);
            /**
             * 修改庫存在version相同的情況下
             */
            boolean secKillSuccess = secKillMapper.updatePosiLockInMySQL(product);
            if (!secKillSuccess) {
                log.error("用戶:" + user.getUsername() + "秒殺商品" + product.getProductName() + "失敗!");
                throw new SecKillException(SecKillEnum.FAIL);
            }} else {
                log.error("商品:" + product.getProductName() + "庫存不足!");
                throw new SecKillException(SecKillEnum.LOW_STOCKS);
            }
            long addResult = jedis.sadd(hasBoughtSetKey, user.getId().toString());
            if (addResult > 0) {
                record = new Record(null, user, product, SecKillEnum.SUCCESS.getCode(), SecKillEnum.SUCCESS.getMessage(), new Date());
                log.info(record.toString());
                boolean insertFlag = secKillMapper.insertRecord(record);
                if (insertFlag) {
                    log.info("用戶:" + user.getUsername() + "秒殺商品" + product.getProductName() + "成功!");
                    return SecKillEnum.SUCCESS;
                } else {
                    throw new SecKillException(SecKillEnum.SYSTEM_EXCEPTION);
                }
            } else {
                log.error("用戶:" + user.getUsername() + "重複秒殺商品:" + product.getProductName());
                throw new SecKillException(SecKillEnum.REPEAT);
            }
        }

使用redis的watch事務加decr操作,RabbitMQ作爲消息隊列記錄用戶搶購行爲,MySQL做異步存儲。

 /**
     * redis的watch監控
     * @param paramMap
     * @return
     */
    public SecKillEnum handleByRedisWatch(Map<String, Object> paramMap) {
        Jedis jedis = redisCacheHandle.getJedis();
        Record record;
        Integer userId = (Integer) paramMap.get("userId");
        Integer productId = (Integer)paramMap.get("productId");
        User user = secKillMapper.getUserById(userId);
        Product product = secKillMapper.getProductById(productId);

        /**
         * 獲得該產品的鍵值對
         */
        String productStockCacheKey = product.getProductName()+"_stock";
        /**
         * 拿到用戶所買商品在redis中對應的key
         */
        String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());
        /**
         * 開啓watch監控
         * 可以決定事務是執行還是回滾
         * 它首先會去比對被 watch 命令所監控的鍵值對,如果沒有發生變化,那麼它會執行事務隊列中的命令,
         * 提交事務;如果發生變化,那麼它不會執行任何事務中的命令,而去事務回滾。無論事務是否回滾,
         * Redis 都會去取消執行事務前的 watch 命令
         */
        jedis.watch(productStockCacheKey);

        boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
        if (isBuy){
            log.error("用戶:"+user.getUsername()+"重複購買商品"+product.getProductName());
            throw new SecKillException(SecKillEnum.REPEAT);
        }
        String stock = jedis.get(productStockCacheKey);
        if (Integer.parseInt(stock) <= 0) {
            log.error("商品:"+product.getProductName()+"庫存不足!");
            throw new SecKillException(SecKillEnum.LOW_STOCKS);
        }
        //開啓redis事務
        Transaction tx = jedis.multi();

        //庫存減一
        tx.decrBy(productStockCacheKey,1);
        //執行事務
        List<Object> resultList = tx.exec();

        if(resultList == null || resultList.isEmpty()){
            jedis.unwatch();
            //watch監控被更改過----物品搶購失敗;
            log.error("商品:"+product.getProductName()+",watch監控被更改,物品搶購失敗");
            throw new SecKillException(SecKillEnum.FAIL);
        }

        //添加到已買隊列
        long addResult = jedis.sadd(hasBoughtSetKey,user.getId().toString());
        if(addResult>0){
            //秒殺成功
            record =  new Record(null,user,product,SecKillEnum.SUCCESS.getCode(),SecKillEnum.SUCCESS.getMessage(),new Date());
           //添加record到rabbitmq消息隊列
            rabbitMQSender.send(JSON.toJSONString(record));
            return SecKillEnum.SUCCESS;
        }else{
            //重複秒殺
            //這裏拋出RuntimeException異常,redis的decr操作並不會回滾,所以需要手動incr回去
            jedis.incrBy(productStockCacheKey,1);
            throw new SecKillException(SecKillEnum.REPEAT);
        }

    }


這裏是rabbitmq的可可靠確認模式

@Slf4j
@Component
public class RabbitMQSender implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String message){
        rabbitTemplate.setConfirmCallback(this);//指定 ConfirmCallback

        // 自定義消息唯一標識
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        /**
         * 發送消息
         */
        rabbitTemplate.convertAndSend("seckillExchange", "seckillRoutingKey", message, correlationData);

    }

    /**
     * 生產者發送消息後的回調函數
     * @param correlationData
     * @param b
     * @param s
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        log.info("callbakck confirm: " + correlationData.getId());
        if(b){
            log.info("插入record成功,更改庫存成功");
        }else{
            log.info("cause:"+s);
        }
    }
}

基於AtomicInteger的CAS機制

 @Transactional
    public SecKillEnum handleByAtomicInteger(Map<String, Object> paramMap) {
        Jedis jedis = redisCacheHandle.getJedis();
        Record record;

        Integer userId = (Integer) paramMap.get("userId");
        Integer productId = (Integer)paramMap.get("productId");
        User user = secKillMapper.getUserById(userId);
        Product product = secKillMapper.getProductById(productId);

        String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());
        //判斷是否重複購買
        boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
        if (isBuy){
            log.error("用戶:"+user.getUsername()+"重複購買商品"+product.getProductName());
            throw new SecKillException(SecKillEnum.REPEAT);
        }
        AtomicInteger atomicInteger = atomicStock.getAtomicInteger(product.getProductName());
        int stock = atomicInteger.decrementAndGet();

        if(stock < 0){
            log.error("商品:"+product.getProductName()+"庫存不足, 搶購失敗!");
            throw new SecKillException(SecKillEnum.LOW_STOCKS);
        }

        long result = jedis.sadd(hasBoughtSetKey,user.getId().toString());
        if (result > 0){
            record = new Record(null,user,product,SecKillEnum.SUCCESS.getCode(),SecKillEnum.SUCCESS.getMessage(),new Date());
            log.info(record.toString());
            boolean insertFlag =  secKillMapper.insertRecord(record);
            if (insertFlag) {
                //更改物品庫存
                secKillMapper.updateByAsynPattern(record.getProduct());
                log.info("用戶:"+user.getUsername()+"秒殺商品"+product.getProductName()+"成功!");
                return SecKillEnum.SUCCESS;
            } else {
                log.error("系統錯誤!");
                throw new SecKillException(SecKillEnum.SYSTEM_EXCEPTION);
            }
        } else {
            log.error("用戶:"+user.getUsername()+"重複秒殺商品"+product.getProductName());
            atomicInteger.incrementAndGet();
            throw new SecKillException(SecKillEnum.REPEAT);
        }
    }

相關知識點來自的微博:
https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651961471&idx=1&sn=da257b4f77ac464d5119b915b409ba9c&chksm=bd2d0da38a5a84b5fc1417667fe123f2fbd2d7610b89ace8e97e3b9f28b794ad147c1290ceea&scene=21#wechat_redirect

代碼借鑑:https://github.com/SkyScraperTwc/SecKillDesign
源碼:https://github.com/OnlyGky/SecondKill

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