2017-12-27 15 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

について言及しています。ちなみに、2番目の方法ではどのように 'accountId'を使用していますか? –

答えて

2

このようなことはトランザクションの目的であり、一貫性に関する何らかの保証を含む複数のステートメントを入力できるようにします。技術的に原子性、一貫性、分離性、耐久性については、https://stackoverflow/q/974596を参照してください。

あなたは

select for update 

で行をロックすることができ、ロックは、トランザクションが終了するまで保持されます。

この場合は、テーブル全体をロックし、selectを実行して行数をカウントしてから挿入を実行します。テーブルがロックされていると、行数は選択と挿入の間で変化しません。

2つのステートメントの必要性を取り除くことは、挿入物内にセレクトを入れておくことをお勧めします。しかし、一般的に、データベースを使用している場合は、トランザクションを認識する必要があります。

ローカルjdbcトランザクション(xaトランザクションとは異なります)では、同じトランザクションに参加するすべてのステートメントに同じjdbc接続を使用する必要があります。すべてのステートメントが実行されたら、接続でコミットを呼び出します。

トランザクションの使いやすさは、春のフレームワークのセールスポイントです。

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 ; 

あなただけのデータベースに新しいレコードを挿入し、あなたはすべてのでのuser_idまたはプライマリフィールドのいずれかを指定する必要はありませんcreateUserメソッドを持つことができます。それでは、あなたのテーブルには、このようなものであるとしましょう:

あなたの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(); 
    } 
} 

のようになります。二回ユーザーテーブルがしかし

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

ようになります蘭..

しかし、前述の解決策が元の質問で提案された解決策よりも優れているとは思うが、私はまだそれがこれを処理する最適な方法です。何かを提案する前に、プロジェクトについてもっと知る必要があります。最初のユーザープライマリとは何ですか?プライマリユーザとは何ですか?プライマリユーザーが1つしかない場合、プライマリユーザーを手動で設定できないのはなぜですか?管理コンソールはありますか?など

あなたの質問を例だった場合....

だからあなたの質問は、単にあなたが自動偽への接続にコミットし、あなたの管理に使用できるトランザクション管理について大きな疑問を説明するために使用した場合の例トランザクションを手動で実行します。自動コミットの詳細は、Oracle JDBC Javaドキュメントを参照してください。さらに、上記のように、特定のアクションに応じてテーブル/行レベルのロックを変更することはできますが、これは非常に非現実的な解決策です。サブクエリとして選択を含めることもできますが、やはり悪い練習にバンド援助を加えるだけです。

あなたが主なユーザーのパラダイムを完全に変更しない限り、私はこれが最も効果的で組織的な方法だと思います。

0

私は1つのINSERTクエリで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の

結果がすぐに表示されない場合があります。MySQLサーバが順番に実行されますので、あなたは、あなたの挿入ステートメントで同じConnectionオブジェクトを使用することができますいずれか

または

使用unique indexis_primary列に対して、(私はynullがあることを前提としてい可能な値注:すべてのMySqlエンジンで複数のnullの値が許可されています)、ユニークな制約違反の場合は、を再実行する必要があります210クエリ。このユニークなインデックスソリューションは、既存のコードでも動作します。

+1

2つの挿入がカウントされないようにするには、まだテーブルロックが必要です。 – tsolakp

+0

@tsolakp、私はそうは思わない、この[answer](https://stackoverflow.com/a/32288484/2114786)を確認してください –

+0

insert2がinsert1のデータを参照し、カウントのために0を得ることを意味するものではありません。 MySQL docsから: "並行INSERTの結果がすぐには見えないかもしれません"。 – tsolakp

関連する問題