一.問(wèn)題的背景
-
前段時(shí)間,我們作了一個(gè)項(xiàng)目,讓客戶使用了一段時(shí)間后發(fā)現(xiàn)一些問(wèn)題,我們打開(kāi)客戶的數(shù)據(jù)庫(kù)看了看,發(fā)現(xiàn)存在很多重復(fù)數(shù)據(jù),重復(fù)數(shù)據(jù)從何而來(lái),我看了看我們的程序,好像我們的代碼已經(jīng)保證了數(shù)據(jù)的唯一性。當(dāng)時(shí)通過(guò)我深入的研究發(fā)現(xiàn)在里面存在一個(gè)大大問(wèn)題。
二.問(wèn)題的引入
我們程序的思路代碼思路大致如下(這里為了闡述問(wèn)題,只是舉個(gè)簡(jiǎn)單例子):
1. 假如存在一個(gè)表student,表結(jié)構(gòu)如下:
- studentid int (z主鍵,遞增字段,MsSql 2000通過(guò)identity標(biāo)識(shí),oracle通過(guò)sequence和觸發(fā)器來(lái)實(shí)現(xiàn))
- name varchar(20)
- age int
- 假如此表的數(shù)據(jù)如下:
-
studentid
|
name
|
age
|
1
|
張三
|
16
|
2
|
李四
|
20
|
3
|
王五
|
23
|
...
|
...
|
...
|
-
- 2. 程序處理步驟:
- (a)外部傳來(lái)一個(gè)stuid,stuname,stuage
-
(b)先根據(jù)外部傳來(lái)的stuid在數(shù)據(jù)庫(kù)中查詢此stuid對(duì)應(yīng)的記錄行,如果數(shù)據(jù)庫(kù)里沒(méi)有此條件的記錄,則把 (stuid,stuname,stuage)插入數(shù)據(jù)庫(kù);如果數(shù)據(jù)庫(kù)里有此紀(jì)錄行,則把此記錄行的studentname和age列的數(shù)據(jù)改為stuname,stuage;程序代碼如下:
public class Student{
public static void insert(String stuname,String stuage)
{
try{
DataBase db=new DataBase();//對(duì)數(shù)據(jù)庫(kù)連接,查詢進(jìn)行了封裝
db.conectDb();
String sql="select * from student where name='"+stuname+"' ";
ResultSet rs=db.query(sql);
boolean b=false;
String stuid="";
while(rs.next){
stuid=rs.getString("studentid");
b=true;
break;
}
if(b)//如果表里沒(méi)有stuname,則插入一條新紀(jì)錄
{
sql="insert into student (name,age)values("'"+stuname+"'","+age+")";//student表里的studentid是自動(dòng)產(chǎn)生的,oracle里用sequence實(shí)現(xiàn)
}else{//如果找著了stuid對(duì)應(yīng)的行,則更新
sql=”update student set name='"+stuname+"'",age="+age+" where studentid="+stuid;
}
db.update(sql);
db.close();
}catch(Exception e)
{
db.close();
e.printStackTrace();
}
}
代碼片段1-1
- (3)servlet處理
-
當(dāng)然,程序是通過(guò)servlet來(lái)調(diào)用Student.insert(stuid,stuname,stuage)來(lái)處理用戶的請(qǐng)求的,servlet代碼如下:
public class InsertServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String stuname=request.getParameter(“stuname”);
String stuage=request.getParameter(“stuage”);
Student.insert(stuname,stuage);
}
}
三.問(wèn)題分析
從上面代碼片段1-1不仔細(xì)看,還真覺(jué)得沒(méi)什么問(wèn)題,程序好像也保證了向數(shù)據(jù)庫(kù)插入或更新數(shù)據(jù)的唯一性。哈哈,這只是表面現(xiàn)象,因?yàn)槭聦?shí)是數(shù)據(jù)庫(kù)里存在重復(fù)數(shù)據(jù)是千真萬(wàn)確的。如果你不信,現(xiàn)在來(lái)測(cè)試一下。我們用多線程來(lái)測(cè)試,每個(gè)線程就相當(dāng)于一個(gè)客戶端。線程如下:
public class Test extends Thread{
public Test(){
start();
}
public void run(){
Student.insert("小龍", 20+"");
}
public static void main(String[] args){
for(int i=0; i<1000; i++){
new Test();
}
}
}
從上面的代碼可以看出,我要向數(shù)據(jù)庫(kù)里插入studentname='小龍' and studentage=20的數(shù)據(jù),但先要檢查是否存在studentid=3的記錄,如果沒(méi)有才插入。通過(guò)此線程我們會(huì)發(fā)現(xiàn)數(shù)據(jù)庫(kù)里有時(shí)候會(huì)出現(xiàn)兩條關(guān)于name為='小龍'并且age=20的記錄。 有時(shí)候運(yùn)行此測(cè)試代碼一段時(shí)間后數(shù)據(jù)里依然沒(méi)有重復(fù)數(shù)據(jù),你可以把此代碼同時(shí)放到多個(gè)機(jī)器,如果是雙核cpu的話出現(xiàn)重復(fù)的機(jī)率可能會(huì)大一些。
大概你已經(jīng)知道問(wèn)題所在,問(wèn)題就在于并發(fā)。因?yàn)橛泻芏嘤脩艨赡芡瑫r(shí)在向數(shù)據(jù)庫(kù)發(fā)送請(qǐng)求。
public class Student{
public static void insert(String stuname,String stuage)
{
try{
DataBase db=new DataBase();//對(duì)數(shù)據(jù)庫(kù)連接,查詢進(jìn)行了封裝
db.conectDb();
String sql="select * from student where name='"+stuname+"' ";
ResultSet rs=db.query(sql);
boolean b=false;
String stuid="";
/*假設(shè)有兩個(gè)線程同時(shí)運(yùn)行到此處,假設(shè)這兩個(gè)線程都查找student表里name為"小龍"的記錄,此時(shí)他們都會(huì)發(fā)現(xiàn)student表里沒(méi)有記錄,所以他們都會(huì)向student表里插入“小龍”的記錄,這就造成了重復(fù)記錄。*/
while(rs.next){
stuid=rs.getString("studentid");
b=true;
break;
}
if(b)//如果表里沒(méi)有stuname,則插入一條新紀(jì)錄
{
sql="insert into student (name,age)values("'"+stuname+"'","+age+")";//student表里的studentid是自動(dòng)產(chǎn)生的,oracle里用sequence實(shí)現(xiàn)
}else{//如果找著了stuid對(duì)應(yīng)的行,則更新
sql=”update student set name='"+stuname+"'",age="+age+" where studentid="+stuid;
}
db.update(sql);
db.close();
}catch(Exception e)
{
db.close();
e.printStackTrace();
}
}
上述紅體分析是產(chǎn)生數(shù)據(jù)庫(kù)出現(xiàn)重復(fù)的原因,也是程序員容易犯的一個(gè)錯(cuò)誤,最簡(jiǎn)單的解決辦法是加上唯一性約束條件, 假設(shè)表student的name是唯一的,那我們就給name加唯一性約束unique。所以數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)唯一確定的name,當(dāng)兩個(gè)請(qǐng)求同時(shí)向數(shù)據(jù)庫(kù)插入相同的name時(shí),會(huì)采用搶占式插入,誰(shuí)先插入其他方就不能再插入數(shù)據(jù)。
上述方法解決了數(shù)據(jù)庫(kù)里出現(xiàn)重復(fù)性數(shù)據(jù)問(wèn)題。但還可以用其他的方法解決,這就涉及到數(shù)據(jù)庫(kù)的事務(wù)的并發(fā)控制。下次再討論。