我正在编写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都创建了主要用户。
如何安全地实施这种情况?
答案 0 :(得分:2)
这种事情是交易的目的,它们允许您输入多个语句,并保证一致性。技术上的原子性,一致性,隔离性和持久性,请参阅https://stackoverflow/q/974596。
您可以使用
锁定行select for update
锁定将一直持续到交易结束。
对于这种情况,您可以锁定整个表,运行select来计算行数,然后执行插入。锁定表后,您知道select和insert之间的行数不会改变。
通过将select放入插入中来删除对2个语句的需要是一个好主意。但总的来说,如果您使用数据库,则需要了解交易。
对于本地jdbc事务(与xa事务相反),您需要对参与同一事务的所有语句使用相同的jdbc连接。一旦所有语句都运行,就在连接上调用commit。
易于使用交易是Spring框架的卖点。
答案 1 :(得分: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_increment
和not null
。我添加了name
列,因为它有助于说明我的观点(但您可以将其替换为除user_id
和is_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 |
然而,即使我认为前面提到的解决方案比原始问题中提出的解决方案更好,但我仍然认为这不是解决这个问题的最佳方法。在提出任何建议之前,我需要了解更多有关该项目的信息。什么是第一个用户主要?什么是主要用户?如果只有一个用户,为什么不能手动设置主要用户?有管理控制台吗?等
因此,如果您的问题仅仅是用于说明有关事务管理的更大问题的示例,则可以使用连接上的自动提交为false并手动管理您的事务。您可以在Oracle JDBC Java文档中阅读有关autocommit的更多信息。另外,如上所述,您可以根据具体操作更改表/行级别锁定,但我认为这是解决此问题的非常不切实际的解决方案。你也可以把选择作为一个子查询包括在内,但你只是在一个不好的练习上加上创可贴。
总而言之,除非你想彻底改变你的主要用户范例,我认为这是最有效和最有条理的方式。
答案 2 :(得分: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的结果可能不会立即显示。
您可以为insert语句使用相同的Connection
对象,以便MySql服务器按顺序运行它们,
或
对is_primary
列使用unique index,(我假设y
和null
是可能的值。注意多个{{1}所有MySql引擎都允许使用值),因此如果出现唯一约束违规,您只需重新运行null
查询即可。这种独特的索引解决方案也适用于您现有的代码。