如何處理高並發情況下的DB插入


轉載以便以后學習使用,謝謝!

插入數據庫,在大家開發過程中是很經常的事情,假設我們有這么一個需求:

1、  我們需要接收一個外部的訂單,而這個訂單號是不允許重復的

2、  數據庫對外部訂單號沒有做唯一性約束

3、  外部經常插入相同的訂單,對於已經存在的訂單則拒絕處理

對於這個需求,很簡單我們會用下面的代碼進行處理(思路:先查找數據庫,如果數據庫存在則直接退出,否則插入)

package com.yhj.test;

 

import com.yhj.dao.OrderDao;

import com.yhj.pojo.Order;

 

/**

 * @Description:並發測試用例

 * @Author YHJ  create at 2011-7-7 上午08:41:44

 * @FileName com.yhj.test.TestCase.java

 */

public class TestCase {

    /**

     * data access object class for deal order

     */

    private OrderDao orderDao;

 

    /**

     * @Description:插入測試

     * @param object 要插入的object實例

     * @author YHJ create at 2011-7-7 上午08:43:15

     * @throws Exception

     */

    public void doTestForInsert(Order order) throws Exception {

       Order orderInDB = orderDao.findByName(order.getOrderNo());

       if(null != orderInDB)

           throw new Exception("the order has been exist!");

       orderDao.save(order);

    }

   

}

這樣很顯然,在單線程下是沒問題的,但是多線程情況下就會出現一個問題,線程1先去訪問DB,查找沒有,開始插入,這時候線程2又來查找DB,而此時線程1插入的事務還沒有提交,線程2沒有查到該數據,也進行插入,於是,問題出現了,插入了2條一樣訂單。

對於這種情況,好像如果不用數據庫做唯一性約束又不借助外部其他的一些工具,是沒有辦法實現的。那怎么做呢?

引入緩存,我們看下面的代碼

package com.yhj.test;

 

import com.yhj.dao.OrderDao;

import com.yhj.pojo.Order;

import com.yhj.util.MemcacheUtil;

import com.yhj.util.MemcacheUtil.UNIT;

 

/**

 * @Description:並發測試用例

 * @Author YHJ  create at 2011-7-7 上午08:41:44

 * @FileName com.yhj.test.TestCase.java

 */

public class TestCase {

    /**

     * data access object class for deal order

     */

    private OrderDao orderDao;

 

    /**

     * @Description:插入測試

     * @param object 要插入的object實例

     * @author YHJ create at 2011-7-7 上午08:43:15

     * @throws Exception

     */

    public void doTestForInsert(Order order){

       String key=null;

       try{

           Order orderInDB = orderDao.findByName(order.getOrderNo());

           //查DB,如果數據庫已經有則拋出異常

           if(null != orderInDB)

              throw new Exception("the order has been exist!");

           key=order.getOrderNo();

           //插緩存,原子性操作,插入失敗 表明已經存在

           if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

              throw new Exception("the order has been exist!");

           //插DB

           orderDao.save(order);

       }catch (Exception e) {

           e.printStackTrace();

       }finally{

           MemcacheUtil.del(key);

       }

    }

 

}

運行步驟如下:

1、  查找數據庫,如果數據庫已經存在則拋出異常

2、  插入緩存,如果插入失敗則表明緩存中已經存在,拋出異常

3、  如果上述2步都沒有拋出異常,則執行插入數據庫的操作

4、  刪除緩存

在並發的情況下,線程1先查找數據庫,發現沒有,繼續執行,寫緩存,這時候線程2開始查找數據庫,發現沒有,則寫緩存,結果緩存中已經存在,寫緩存失敗,拋出異常,返回已存在。線程1執行插入數據庫成功,刪除緩存。以后再來的線程發現數據庫已經存在了,則不在向下執行,直接返回.。

機器異常情況下,不能執行finally語句,但是放在memcache中的數據會在1分鍾后超時。

貌似沒有問題。使用LodeRunner測試100個並發的操作,發現仍然有重復的訂單插入,這個是為什么呢?我們再來看這段代碼!

public void doTestForInsert(Order order){

       String key=null;

       try{

           Order orderInDB = orderDao.findByName(order.getOrderNo());

           //查DB,如果數據庫已經有則拋出異常

           if(null != orderInDB)

              throw new Exception("the order has been exist!");

           key=order.getOrderNo();

           //插緩存,原子性操作,插入失敗 表明已經存在

           if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

              throw new Exception("the order has been exist!");

           //插DB

           orderDao.save(order);

       }catch (Exception e) {

           e.printStackTrace();

       }finally{

           MemcacheUtil.del(key);

       }

    }

我們所預料的是2個線程同時操作,假設有更多的並發線程呢?

時刻1:

線程1到達,查數據庫,發現沒有

時刻2

線程1寫緩存

線程2到達,查數據庫發現沒有

時刻3

線程1緩存寫入成功,開始寫數據庫

線程2開始寫緩存

線程3到達,查數據庫,發現沒有

時刻4

線程1繼續插入數據庫

線程2寫緩存失敗,拋出異常,執行finally

線程3開始寫緩存

時刻5

線程1插入數據庫成功,開始構建返回結果

線程2執行finally,刪除緩存,開始構建返回結果

線程3發現緩存不存在(被線程2刪除),寫緩存

時刻6

線程1成功返回

線程2成功返回

線程3寫緩存成功,開始寫數據庫

時刻7

線程3寫數據庫成功,返回

因此上述代碼仍然有插入多條重復記錄的可能,我們在並發20的測試中發現成功插入了5筆訂單,其中4筆是不應該插入的!

那我們應該怎么解決呢?其實只要解決一個問題,只有插入DB時候的異常是可以刪除的,其他地方不應該刪除,那能不能將代碼改成下面的呢?

    public void doTestForInsert(Order order){

       String key=null;

       try{

           Order orderInDB = orderDao.findByName(order.getOrderNo());

           //查DB,如果數據庫已經有則拋出異常

           if(null != orderInDB)

              throw new Exception("the order has been exist!");

           key=order.getOrderNo();

           //插緩存,原子性操作,插入失敗 表明已經存在

           if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

              throw new Exception("the order has been exist!");

           //插DB

           orderDao.save(order);

           MemcacheUtil.del(key);

       }catch (Exception e) {

           e.printStackTrace();

       }//finally{

//         MemcacheUtil.del(key);

//     }

    }

這樣顯然不行,為什么呢?

這樣是保證了只有插入DB成功了才會刪除緩存,但是當插入DB的時候發生了一個異常,刪除緩存就不會再執行,雖然我們有一分鍾超時,但意味着我們一分鍾內該筆訂單是不能再被處理的,而實際上這邊訂單並沒有處理成功,所以這樣是不滿足需求的!

繼續改進

代碼如下:加一個標志位

public void doTestForInsert(Order order){

       String key=null;

       boolean needDel=false;

       try{

           Order orderInDB = orderDao.findByName(order.getOrderNo());

           //查DB,如果數據庫已經有則拋出異常

           if(null != orderInDB)

              throw new Exception("the order has been exist!");

           key=order.getOrderNo();

           //插緩存,原子性操作,插入失敗 表明已經存在

           if(!MemcacheUtil.add(key, order, MemcacheUtil.getExpiry(UNIT.MINUTE, 1)))

              throw new Exception("the order has been exist!");

           needDel=true;

           //插DB

           orderDao.save(order);

       }catch (Exception e) {

           e.printStackTrace();

       }finally{

           if(needDel)

              MemcacheUtil.del(key);

       }

    }

這樣是不是完美解決了呢?

在其他異常執行的時候是不會刪除緩存的,我們套在之前的代碼上,線程2判斷緩存中存在拋出異常執行finally的時候是不會刪除緩存的,因此線程3沒有機會執行寫緩存的操作,從而保證了線程1是唯一能夠插入DB的。

還有沒有其他漏洞呢?期待大家發現……

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM