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