java 併發問題分析解決

    項目中我們經常能見到一些併發問題,現對一些常見併發問題進行總結,知識結構不會很全,但比較實用。

  • 基本概念
  1. 什麼是併發問題

我們以記錄網站的訪問量爲例,先看一下併發問題是如何產生的。

private Integer count=1;

private
AtomicInteger atomicCount = new AtomicInteger(1);

/**
 *
非線程安全方式
 * @param
request
 
* @return
 
*/

@RequestMapping("/getCount")
public BaseResponse<Integer> visitCount(HttpServletRequest request){
   
return new BaseResponse<>(true, count++);
}

 

我們通過jmeter模擬併發1000個請求,會發現我們得到的的值並不一定是1000。這裏因爲count++ 並不是線程安全的,所以在併發情況下會存在線程安全問題。

 

  1. 幾組重要概念

同步VS異步

同步和異步通常用來形容一次方法調用。同步方法調用開始後,調用者必須等待被調用的方法結束後,調用者後面的代碼才能執行。而異步調用,指的是,調用者不用管被調用方法是否完成,都會繼續執行後面的代碼,當被調用的方法完成後會通知調用者。

併發與並行

併發和並行是十分容易混淆的概念。併發指的是多個任務交替進行,而並行則是指真正意義上的“同時進行”。實際上,如果系統內只有一個CPU,使用多線程時,在真實系統環境下不能並行,只能通過切換時間片的方式交替進行,從而併發執行任務。真正的並行只能出現在擁有多個CPU的系統中。
 

阻塞和非阻塞

阻塞和非阻塞通常用來形容多線程間的相互影響,比如一個線程佔有了臨界區資源,那麼其他線程需要這個資源就必須進行等待該資源的釋放,會導致等待的線程掛起,這種情況就是阻塞,而非阻塞就恰好相反,它強調沒有一個線程可以阻塞其他線程,所有的線程都會嘗試地往前運行。
 

臨界區

臨界區用來表示公共資源或者說是共享數據,可以被多個線程使用。但是每個線程使用時,一旦臨界區資源被一個線程佔有,那麼其他線程必須等待。

 

  • 常見併發問題解決方案
  1. Java代碼層面
  • 使用synchronized
/**

 * 使用synchronized

 * @param request

 * @return

 */

@RequestMapping("/getCountWithSyn")

public BaseResponse<Integer> visitCountWithSynchro(HttpServletRequest request){

    synchronized (this){

        count++;

    }

    return new BaseResponse<>(true, count);

}

 

  • 使用Lock
  • /**  * 使用synchronized  * @param request  * @return  */ @RequestMapping("/getCountWithSyn") public BaseResponse<Integer> visitCountWithSynchro(HttpServletRequest request){     synchronized (this){         count++;     }     return new BaseResponse<>(true, count); }
  • 使用原子操作類
  • /**  * 使用原子類  * @param request  * @return  */ @RequestMapping("/getCountWithAct") public BaseResponse<Integer> visitCountWithAct(HttpServletRequest request){     return new BaseResponse<>(true, atomicCount.getAndIncrement()); }
  1. 數據庫層面

實際項目開發中,經常會遇到併發下單類似的場景,我們以商品購買下單爲例,講解一下如何通過數據庫悲觀鎖樂觀鎖控制併發,本案例有一定的參考性,但其中代碼比較簡單,省去了很多邏輯校驗,遇到類似場景請勿照搬。

所涉及表:

  1. 商品庫存表:

 

create table T_STOCK

(  

  id           NUMBER not null,

  product_name VARCHAR2(255),

  price        NUMBER,

  num          NUMBER,

  create_time  TIMESTAMP(6),

  version      NUMBER

);

-- Add comments to the columns

comment on column T_STOCK.id

  is 'id';

comment on column T_STOCK.product_name

  is '商品名稱';

comment on column T_STOCK.price

  is '商品價格';

comment on column T_STOCK.num

  is '商品數量';

comment on column T_STOCK.create_time

  is '創建時間';

comment on column T_STOCK.version

  is '版本號';

 

  1. 訂單表:

-- Create table

create table T_ORDER

(

  id           NUMBER not null,

  stock_id     NUMBER,

  product_name VARCHAR2(255),

  num          NUMBER,

  user_name    VARCHAR2(255)

);

-- Add comments to the columns

comment on column T_ORDER.id

  is 'id';

comment on column T_ORDER.stock_id

  is '庫存id';

 

  • 最Low實現(非線程安全)

日常實現中很容易出現超發或者庫存數量扣爲負數線下,先看下我們初學者很容易實現的方式。

Service層代碼:

@Override

@Transactional

public String dealOrder(String userName, Long num, Long stockId) {

    //查詢stock

    Stock stock = stockDao.getById(stockId);

    if(stock.getNum()>=num){

        Order order=new Order();

        order.setProductName(stock.getProductName());

        order.setUserName(userName);

        order.setStockId(stockId);

        order.setNum(num);

        //保存訂單信息

        orderDao.insert(order);

        stock.setNum(stock.getNum() - num);

        stock.setVersion(stock.getVersion()+1);

        //更新庫存信息

        stockDao.update(stock);

    }

    return "success";

}

 

  • 悲觀鎖
  • @Override @Transactional public void dealOrderWithPessLock(String userName, Long num, Long stockId) {     Stock stock = stockDao.getByIdForUpdate(stockId);     if(stock.getNum()>=num){           Order order=new Order();         order.setProductName(stock.getProductName());         order.setUserName(userName);         order.setStockId(stockId);         order.setNum(num);         orderDao.insert(order);         stock.setNum(stock.getNum() - num);         stock.setVersion(stock.getVersion()+1);         stockDao.update(stock);     } }

 

Dao層

@Select("select * from t_stock where id=#{id} for update")

@Results({

        @Result(property = "id",column = "id"),

        @Result(property = "productName",column = "product_name"),

        @Result(property = "price",column = "price"),

        @Result(property = "num",column = "num"),

        @Result(property = "createTime",column = "create_time"),

        @Result(property = "version",column = "version")

})

Stock getByIdForUpdate(Long stockId);

 

 

適用場景:

 

比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。

  • 樂觀鎖

Service層:

@Override

@Transactional

public void dealOrderWithOptLock(String userName, Long num, Long stockId) {

    Stock stock = stockDao.getById(stockId);

    if(stock.getNum()>=num){



        Order order=new Order();

        order.setProductName(stock.getProductName());

        order.setUserName(userName);

        order.setStockId(stockId);

        order.setNum(num);

        stock.setNum(stock.getNum() - num);

        int i= stockDao.updateWithVersion(stock);

        if(i==1){

            orderDao.insert(order);

        }

       logger.info("更新庫存成功條數:"+i);

    }else{

        logger.error("庫存不足");

    }

}

 

Dao層:

@Update("update t_stock  set  num = #{num}, version =version+1 where id = #{id} and version=#{version}")

int updateWithVersion(Stock stock);

 

適用場景:

 

比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,數據發生衝突的可能性就會增大,爲了保證數據的一致性,應用層需要不斷的重新獲取數據,這樣會增加大量的查詢操作,降低了系統的吞吐量。

  1. 分佈式鎖層面

我們這裏適用redis 做分佈式鎖控制,這裏僅僅做分佈式鎖,並不做緩存使用。

 

Service層:



@Override

@Transactional

public void dealOrderWithDisLock(String userName, Long num, Long stockId) {

    RedissLockUtil.lock("lock", TimeUnit.SECONDS, 3);

    //查詢stock

 @Override

@Transactional

public void dealOrderWithDisLock(String userName, Long num, Long stockId) {

    RedissLockUtil.lock("lock", TimeUnit.SECONDS, 3);

    //查詢stock

    Stock stock = stockDao.getById(stockId);

    if(stock.getNum()>=num){

        Order order=new Order();

        order.setProductName(stock.getProductName());

        order.setUserName(userName);

        order.setStockId(stockId);

        order.setNum(num);

        //保存訂單信息

        orderDao.insert(order);

        stock.setNum(stock.getNum() - num);

        stock.setVersion(stock.getVersion()+1);

        //更新庫存信息

        stockDao.update(stock);

        logger.info("下單成功:"+userName+"--num:"+num);



    }else{

        logger.error("庫存不足,下單失敗");

    }

    RedissLockUtil.unlock("lock");

}
 

 

 

  1. 性能比較

 

數據庫悲觀鎖:

數據庫樂觀鎖:

 

Redis分佈式鎖

 

 

  1. 總結:

         在我們日常項目開發中,涉及併發控制的地方一般建議使用數據庫樂觀鎖就可以解決,對於有些場景需要控制插入記錄數量時,可以通過分佈式鎖去解決。

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