轉載以便以后學習使用,謝謝!
插入數據庫,在大家開發過程中是很經常的事情,假設我們有這么一個需求:
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的。
還有沒有其他漏洞呢?期待大家發現……