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