2017-12-27 1223 views
1

我正在編寫一個Java API,並且正在使用MySQL。線程安全讀取和寫入數據庫的最佳做法是什麼?如何線程安全地讀取和寫入數據庫?

例如,採取以下createUser方法:

// Create a user 
// If the account does not have any users, make this 
// user the primary account user 
public void createUser(int accountId) { 
    String sql = 
      "INSERT INTO users " + 
      "(user_id, is_primary) " + 
      "VALUES " + 
      "(0, ?) "; 
    try (
      Connection conn = JDBCUtility.getConnection(); 
      PreparedStatement ps = conn.prepareStatement(sql)) { 

     int numberOfAccountsUsers = getNumberOfAccountsUsers(conn, accountId); 
     if (numberOfAccountsUsers > 0) { 
      ps.setString(1, null); 
     } else { 
      ps.setString(1, "y"); 
     } 

     ps.executeUpdate(); 
    } catch (SQLException e) { 
     e.printStackTrace(); 
    } 
} 

// Get the number of users in the specified account 
public int getNumberOfAccountsUsers(Connection conn, int accountId) throws SQLException { 
    String sql = 
      "SELECT COUNT(*) AS count " + 
      "FROM users "; 
    try (
      PreparedStatement ps = conn.prepareStatement(sql); 
      ResultSet rs = ps.executeQuery()) { 

     return rs.getInt("count"); 
    } 
} 

說源A調用createUser,和剛剛讀該帳戶ID 100具有0個用戶。然後,源B調用createUser,並且在源A執行更新之前,源B也讀取了該帳戶ID具有0個用戶。結果,源A和源B都創建了主要用戶。

這種情況如何安全實施?

+1

所以這個問題實際上與線程安全無關。這是你真正的問題還是你用它來解釋你的問題?另外,你正在使用什麼數據庫系統(MySQL,SQL Server等)? –

+0

他/她已經提到了MySQL – Ele

+0

順便說一句,您如何在第二種方法中使用'accountId'? –

回答

2

這種事情是交易的目的,它們允許你輸入多條語句,並保證一致性。技術上的原子性,一致性,隔離性和耐久性見https://stackoverflow/q/974596

您可以

select for update 

鎖定行和鎖將保持到事務的結束。

對於這種情況,您可能會鎖定整個表格,運行select來計算行數,然後執行插入操作。鎖定表格後,您知道在選擇和插入之間行數不會改變。

通過將選擇放入插入中去除2語句的需要是一個好主意。但是一般來說,如果你使用數據庫,你需要知道事務。

對於本地jdbc事務(與xa相反),您需要對參與相同事務的所有語句使用相同的jdbc連接。一旦所有的語句都運行完畢,在連接上調用commit。

易於使用交易是春季框架的賣點。

0

問題概述

要開始,你的問題沒有什麼關係線程安全的,它與交易和代碼,可以更好地做了優化。

如果我正在閱讀這個正確的你試圖做的是設置第一個用戶作爲主要用戶。我可能根本不會採用這種方法(也就是說,爲第一個用戶創建一個is_primary行),但如果我這樣做了,我根本不會在Java應用程序中包含該條件。每當你創建一個用戶時,你不僅要評估一個條件,還要對數據庫進行不必要的調用。相反,我會像這樣重構。

代碼

首先,確保你的表是這樣的。

CREATE TABLE `users` (
    user_id INT NOT NULL AUTO_INCREMENT, 
    is_primary CHAR(1), 
    name VARCHAR(30), 
    PRIMARY KEY (user_id) 
); 

換句話說,你的user_id應該auto_incrementnot null。我列出了name列,因爲它有助於說明我的觀點(但您可以用user_idis_primary以外的其他列代替)。我也是主要的user_id,因爲它可能是。

如果你只想修改你當前的表,它會是這樣的。

ALTER TABLE `users` MODIFY COLUMN `user_id` INT not null auto_increment; 

然後創建一個觸發器,以便每次插入一行它會檢查,看它是否是第一排和時間如果是,則相應地更新。

delimiter | 
    CREATE TRIGGER primary_user_trigger 
    AFTER INSERT ON users 
     FOR EACH ROW BEGIN 

     IF NEW.user_id = 1 THEN 
      UPDATE users SET is_primary = 'y' where user_id = 1; 
     END IF; 
     END; 
| delimiter ; 

在這一點上你可以有一個createUser方法插入一個新的記錄到數據庫中,你不需要指定無論是USER_ID或主要領域都。因此,讓我們說你的表是這樣的:

你的createUser方法將基本上只是看起來像下面

// Create a user 
// If the account does not have any users, make this 
// user the primary account user 
public void createUser(String name) { 
    String sql = 
      "INSERT INTO users (name) VALUES(?)" 
    try (
      Connection conn = JDBCUtility.getConnection(); 
      PreparedStatement ps = conn.prepareStatement(sql)); 

      ps.setString(1, name); 
      ps.executeUpdate(); 
    } catch (SQLException e) { 
     e.printStackTrace(); 
    } 
} 

然users表中兩次會看起來像

| user_id | is_primary | name | 
+--------------+----------------+----------+ 
|  1  |  y  | Jimmy | 
---------------+----------------+----------+ 
|  2  |  null  | Cindy | 

但是..

但是,即使我認爲上述解決方案比原始問題提出的要好,但我仍然認爲它不是處理這個問題的最佳方法。在提出任何建議之前,我需要更多地瞭解該項目。第一個用戶主要是什麼?什麼是主要用戶?爲什麼只有一個用戶只能手動設置主用戶?有沒有管理控制檯?等

如果你的問題就是一個例子....

所以,如果你的問題只是用來說明有關事務管理的一個更大的問題,你可以使用自動提交連接到錯誤並管理一個例子你手動交易。您可以在Oracle JDBC Java文檔中閱讀有關自動提交的更多信息。此外,如上所述,您可以根據具體操作更改表格/行級鎖定,但我認爲這是一個非常不切實際的解決方案。你也可以把select作爲子查詢,但是你只是在壞習慣上做了一個創可貼。總而言之,除非你想完全改變你的主要用戶模式,我認爲這是最有效率和組織有效的方式來做到這一點。

0

我覺得這是更好地包括你的條件設置is_primary值在一個插入查詢:

public void createUser(int accountId) { 
    String sql = "INSERT INTO users (user_id, is_primary) " + 
       "SELECT 0, if(COUNT(*)=0, 'y', null) " + 
       "FROM users"; 
    //.... 
} 

因此,這將是安全的在多線程環境中運行你的代碼,你可以擺脫getNumberOfAccountsUsers方法。

而且爲了擺脫任何疑問的同時insert(感謝@tsolakp comment)的知名度,通過the documentation說:

併發INSERT的結果可能不會立即可見。

您既可以使用相同的Connection對象插入語句時MySQL服務器將依次運行它們,

使用unique indexis_primary列,(我認爲ynull是可能的值注意多個null值是所有MySql引擎允許的),所以在違反唯一約束的情況下,您只需要重新運行您的insert查詢。這種獨特的索引解決方案也將與您現有的代碼一起工作

+1

你仍然需要表鎖,以確保兩個插入不會計數爲0. – tsolakp

+0

@tsolakp,我不這麼認爲,請檢查此問題[answer](https://stackoverflow.com/a/32288484/2114786) –

+0

並不意味着insert2將會看到insert1的數據,並且仍然可以得到0的數量。從MySQL文檔:「併發INSERT的結果可能不會立即可見」。 – tsolakp