【開發技巧】-- 使用同步關鍵字sychronized與基於redis分佈式鎖分別實現秒殺業務

1. 秒殺業務下需要解決的問題有哪些?

秒殺業務當然對於系統服務的可用性要求、以及數據的一致性是要求非常嚴格的,當然服務的高可以可以通過服務器集羣化來解決,而數據的一致的的話這個解決起來要複雜很多,可能我們習慣會用sychronized來解決秒殺系統下的超賣問題,但是這樣是有侷限性的,sychronized會使得我們的服務器的請求響應速度變慢,並且sychronized並不適合分佈式系統,而我們如果使用redis實現分佈式鎖就可以很好的解決以上問題。

2. 秒殺業務原始實現代碼

2.1 秒殺業務邏輯實現類 SecKillServiceImpl
@Service
public class SecKillServiceImpl implements SecKillService {


    /**
     * 國慶活動,皮蛋粥特價,限量100000份
     */
    static Map<String,Integer> products;
    static Map<String,Integer> stock;
    static Map<String,String> orders;


    static
    {
        /**
         * 模擬多個表,商品信息表,庫存表,秒殺成功訂單表
         */
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        products.put("123456", 100000);
        stock.put("123456", 100000);
    }

    /**
     * 查詢秒殺活動特價商品的信息
     *
     * @param productId
     * @return
     */
    @Override
    public String querySecKillProductInfo(String productId) {
        return "國慶活動,皮蛋粥特價,限量份"
                + products.get(productId)
                +" 還剩:" + stock.get(productId)+" 份"
                +" 該商品成功下單用戶數目:"
                +  orders.size() +" 人" ;
    }

    /**
     * 模擬不同用戶秒殺同一商品的請求
     *
     * @param productId
     * @return
     */
    @Override
    public void orderProductMockDiffUser(String productId) {
        //1.查詢該商品庫存,爲0則活動結束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活動結束");
        }else {
            //2.下單(模擬不同用戶openid不同)
            orders.put(KeyUtil.genUniqueKey(),productId);
            //3.減庫存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }
    }
}
2.2 秒殺業務請求處理器實現
/**
 * Created with IntelliJ IDEA.
 * User: 李敷斌.
 * Date: 2020-02-08
 * Time: 16:01
 * Explain: 秒殺商品請求處理器
 */
@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {

    @Autowired
    private SecKillService secKillService;

    /**
     * 查詢秒殺活動特價商品的信息
     * @param productId
     * @return
     */
    @GetMapping("/query/{productId}")
    public String query(@PathVariable String productId)throws Exception
    {
        return secKillService.querySecKillProductInfo(productId);
    }


    /**
     * 秒殺,沒有搶到獲得"哎呦喂,xxxxx",搶到了會返回剩餘的庫存量
     * @param productId
     * @return
     * @throws Exception
     */
    @GetMapping("/order/{productId}")
    public String skill(@PathVariable String productId)throws Exception
    {
        log.info("@skill request, productId:" + productId);
        secKillService.orderProductMockDiffUser(productId);
        return secKillService.querySecKillProductInfo(productId);
    }
}
2.3 使用ab壓測來測試我們的秒殺下單接口
  • 通過查詢接口查看目前商品餘量
    在這裏插入圖片描述
    可以看到當前餘量爲100000

  • 手動請求下單連續下單幾次
    在這裏插入圖片描述
    可以發現數據準確無誤,這時因爲我們的的程序是能夠承受一定的併發量的,而且我們的手速也是不夠快的。

  • 通過ab壓測,通過100個線程請求500次 ab -n 500 -u 100 請求地址
    在這裏插入圖片描述

  • 壓測後查詢商品秒殺情況
    在這裏插入圖片描述
    從上圖可以發現,在程序未做任何處理時,當請求激增的時候我們的商品超賣了,而且超賣數量還特別的多。

3. 使用sychronized同步關鍵字解決秒殺系統超賣問題

3.1 在秒殺業務實現類的下單方法上加入sychronized關鍵字。
    /**
     * 模擬不同用戶秒殺同一商品的請求
     *
     * @param productId
     * @return
     */
    @Override
    public synchronized void  orderProductMockDiffUser(String productId) {
        //1.查詢該商品庫存,爲0則活動結束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活動結束");
        }else {
            //2.下單(模擬不同用戶openid不同)
            orders.put(KeyUtil.genUniqueKey(),productId);
            //3.減庫存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }
    }
3.2 重啓應用程序,並查詢當前商品餘量信息

在這裏插入圖片描述
當前商品總餘量爲100000份

3.3 使用ab壓測,用100個線程發起500次請求

在這裏插入圖片描述
在測試的過程中明顯感覺服務器響應速度變慢了許多

3.4 請求完畢後查看結果

在這裏插入圖片描述
可以看到我們的數據是沒有問題的,雖然響應速度慢了點,但是至少還是完成了任務。

4. 使用redis實現分佈式鎖解決秒殺系統超賣問題

4.1. redis分佈式鎖加鎖與解鎖方法實現
package com.qingyun.wechat_ordersys.service.impl;

import com.lly835.bestpay.utils.StringUtil;
import com.qingyun.wechat_ordersys.service.RedisLockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * Created with IntelliJ IDEA.
 * User: 李敷斌.
 * Date: 2020-02-08
 * Time: 19:46
 * Explain: 基於redis的分佈式鎖實現
 */
@Component
@Slf4j
public class RedisLockServiceImpl implements RedisLockService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 加鎖方法
     *
     * @param key
     * @param value 鎖超時時間=當前時間+加鎖時長
     * @return 是否加鎖成功
     */
    @Override
    public boolean onLock(String key, String value) {

        //使用redis命令setnf設置 如果值不存在就設置 如果存在就不設置
        if(redisTemplate.opsForValue().setIfAbsent(key,value)){
            return true;
        }

        String currentValue=redisTemplate.opsForValue().get(key).toString();

        //判斷值是否過期
        if(!StringUtil.isEmpty(currentValue)&&Long.valueOf(currentValue)<System.currentTimeMillis()){
            //獲取上一個鎖的時間
            String oldValue=redisTemplate.opsForValue().getAndSet(key,value).toString();

            //防止多個線程都重新設值
            if(!StringUtil.isEmpty(oldValue)&&oldValue.equals(currentValue)){
                return true;
            }
        }

        return false;
    }

    /**
     * 解鎖方法
     *
     * @param key
     * @param value
     */
    @Override
    public void offLock(String key,String value) {
        try {
            String currentValue=redisTemplate.opsForValue().get(key).toString();

            if(!StringUtils.isEmpty(currentValue)&&value.equals(currentValue)){
                //刪除鎖
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        }catch (Exception e){
            log.error("【redis分佈式鎖解鎖操作】解鎖時發生異常");
        }
    }
}

4.2 修改秒殺訂單創建業務實現類
@Service
public class SecKillServiceImpl implements SecKillService {

    @Autowired
    private RedisLockServiceImpl redisLockService;

    //設置一個常量 超時時間爲 10s
    private static final Long TIMEOUT=10*1000L;
  
    /**
      省略部分業務代碼
    **/

    /**
     * 模擬不同用戶秒殺同一商品的請求
     *
     * @param productId
     * @return
     */
    @Override
    public void  orderProductMockDiffUser(String productId) {

        //加鎖
        Long expireTime=System.currentTimeMillis()+TIMEOUT;

        if(! redisLockService.onLock(productId,String.valueOf(expireTime))){
            throw new SellException(ResultEnums.ON_REDIS_LOCK_FAILED);
        }

        //1.查詢該商品庫存,爲0則活動結束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活動結束");
        }else {
            //2.下單(模擬不同用戶openid不同)
            orders.put(KeyUtil.genUniqueKey(),productId);
            //3.減庫存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }

        //解鎖操作
        redisLockService.offLock(productId,String.valueOf(expireTime));
    }
}
4.3 啓動項目,並查詢商品餘量信息

在這裏插入圖片描述

商品初始餘量爲100000份

4.4 通過ab壓測來測試該業務,測試請求500次下單

在這裏插入圖片描述
使用redis分佈式鎖之後明顯加快了請求的響應速度,這樣做其實更節省系統資源,因爲請求能夠快速的響應,而不需要等待其他請求的結束才能夠執行。

4.5 查詢商品餘量信息

在這裏插入圖片描述可以看到信息準確無誤,沒有出現超賣的情況。

發佈了97 篇原創文章 · 獲贊 36 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章