2017-12-27 50 views
1

자바 API를 작성 중이며 MySQL을 사용하고 있습니다. 스레드로부터 안전하게 읽고 데이터베이스에 쓰는 가장 좋은 방법은 무엇입니까?데이터베이스에 스레드로부터 안전하게 읽고 쓰는 방법은 무엇입니까?

// 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 사용자가 읽기 :

는 예를 들어, 다음 createUser 방법을. 그런 다음 소스 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 연결을 사용해야합니다. 모든 명령문이 실행되면 연결에서 커밋을 호출하십시오.

트랜잭션 사용의 용이성은 스프링 프레임 워크의 판매 시점입니다.

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(); 
    } 
} 

과 같을 것이다 두 배의 사용자 테이블 그러나

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

처럼 보일 것이다 란 ..

그러나 앞서 말한 해결책이 원래의 질문에서 제안 된 것보다 낫다고 생각하지만, 나는 아직도 그것이라고 생각하지 않습니다. 이것을 처리하는 최적의 방법. 아무것도 제안하기 전에 나는 프로젝트에 대해 더 많이 알 필요가있다. 첫 번째 사용자는 무엇입니까? 기본 사용자 란 무엇입니까? 기본 사용자가있는 경우 기본 사용자를 수동으로 설정하지 못하는 이유는 무엇입니까? 관리 콘솔이 있습니까? 등은 귀하의 질문에 예였다

경우

....

그래서 귀하의 질문은 단순히 거짓에 대한 연결을 커밋 당신이 자동차를 사용하여 트랜잭션 관리에 대한 더 큰 질문을 설명하고 관리하는 데 사용되는 예였다 경우 트랜잭션을 수동으로. 자동 커밋에 대한 자세한 내용은 Oracle JDBC Java 문서를 참조하십시오. 또한 위에 언급 한 바와 같이 특정 작업에 따라 테이블/행 수준 잠금을 변경할 수 있지만이 문제에 대해서는 매우 비현실적인 해결책이라고 생각합니다. 하위 쿼리로 선택을 포함시킬 수도 있지만 다시 나쁜 행동에 대해 반창고를 붙입니다.

당신이 기본 사용자 패러다임을 완전히 바꾸고 싶지 않다면이 모든 것이 가장 효율적이고 잘 조직 된 방법이라고 생각합니다.

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의

결과가 바로 표시되지 않을 수 있습니다 : the documentation에 의해 명시된 바와 같이

그리고 순서

는 동시 insert (감사 @tsolakp comment까지)의 가시성에 대한 의심을 제거합니다.MySQL 서버가 순차적으로 실행됩니다, 그래서 당신은 당신의 삽입 문에 같은 Connection 개체를 사용할 수도 있고

,

또는

사용 unique indexis_primary 열의, (나는 ynull가 있다고 가정 가능한 값. 참고 : 모든값은 모든 MySql 엔진에서 허용됩니다. 따라서 고유 한 제약 조건 위반의 경우을 다시 실행해야합니다.210 쿼리. 이 고유 한 인덱스 솔루션은 기존 코드에서도 작동합니다.

+1

두 개의 인서트가 0으로 계산되지 않도록하려면 테이블 잠금이 여전히 필요합니다. – tsolakp

+0

@tsolakp, 나는 그렇게 생각하지 않습니다.이 [대답] (https://stackoverflow.com/a/32288484/2114786)을 확인하십시오. –

+0

insert2가 insert1의 데이터를 볼 수 있고 count에 대해 0을 얻을 수 있음을 의미하지는 않습니다. MySQL docs : "동시 INSERT 결과가 즉시 표시되지 않을 수 있습니다." – tsolakp