[英]How to thread-safely read from and write to the database?
我正在编写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都创建了主要用户。
如何安全地实施这种情况?
这种事情就是交易的目的,它们使您可以输入多个具有一致性保证的语句。 从技术上讲原子性,一致性,隔离性和持久性,请参阅https:// stackoverflow / q / 974596 。
您可以使用
select for update
并且锁将保持到交易结束。
对于这种情况,您可以锁定整个表,运行select来对行进行计数,然后执行插入。 锁定表后,您知道在选择和插入之间行数不会改变。
通过将select插入插入来消除对2条语句的需要是一个好主意。 但是通常,如果您使用的是数据库,则需要了解事务。
对于本地jdbc事务(与xa事务相反),您需要对参与同一事务的所有语句使用相同的jdbc连接。 所有语句运行后,在连接上调用commit。
轻松使用事务是spring框架的卖点。
首先,您的问题与线程安全无关,它与可以更好地优化的事务和代码有关。
如果我正确阅读了这篇文章,则尝试将第一个用户设置为主用户。 我可能根本不会采用这种方法(也就是说,第一个用户有一个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
primary,因为它可能是。
如果您只想修改当前表,它将是这样。
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或primary字段 。 因此,假设您的表格是这样的:
您的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文档中阅读有关自动提交的更多信息。 此外,如上所述,您可以根据您的特定操作来更改表/行级别的锁定,但是我认为这对于此问题是非常不现实的解决方案。 您还可以将select包含为子查询,但同样,您只是在临时作弊上使用创可贴。
总而言之,除非您想完全改变您的主要用户范例,否则我认为这是最有效且组织最完善的方式。
我认为最好在一个插入查询中包含设置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 ),如文档所述 :
并发INSERT的结果可能不会立即可见。
您可以对插入语句使用相同的Connection
对象,以便MySql服务器将按顺序运行它们,
要么
为is_primary
列使用唯一索引 (我假设y
和null
是可能的值。 请注意 ,所有MySql引擎都允许使用多个null
值),因此,在违反唯一约束的情况下,您只需要重新运行insert
查询。 这种独特的索引解决方案还将与您现有的代码一起使用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.