Java:使来自多个客户端的并发MySQL查询同步
我在一家游戏网吧工作,我们这里有一个系统smartlaunch跟踪游戏许可证。我已经写了一个程序,它实际上与这个系统接口,它的后端MySQL数据库。该程序将在客户端PC上运行,1查询数据库以从可用池中选择未使用的许可证,2将此许可证标记为客户端PC正在使用 问题是,我有一个并发错误。该程序旨在在多台机器上同时启动,当这种情况发生时,一些机器通常会尝试获取相同的许可证。我认为这是因为第1步和第2步不同步,即一个程序确定许可证5可用并选择它,但在它可以将5标记为正在使用之前,另一台电脑上的另一个程序副本尝试获取相同的许可证 我试图通过使用事务和表锁定来解决这个问题,但这似乎没有任何区别——我这样做对吗?以下是相关代码:Java:使来自多个客户端的并发MySQL查询同步,java,mysql,concurrency,locking,Java,Mysql,Concurrency,Locking,我在一家游戏网吧工作,我们这里有一个系统smartlaunch跟踪游戏许可证。我已经写了一个程序,它实际上与这个系统接口,它的后端MySQL数据库。该程序将在客户端PC上运行,1查询数据库以从可用池中选择未使用的许可证,2将此许可证标记为客户端PC正在使用 问题是,我有一个并发错误。该程序旨在在多台机器上同时启动,当这种情况发生时,一些机器通常会尝试获取相同的许可证。我认为这是因为第1步和第2步不同步,即一个程序确定许可证5可用并选择它,但在它可以将5标记为正在使用之前,另一台电脑上的另一个程序
public LicenseKey Acquire() throws SmartLaunchException, SQLException {
Connection conn = SmartLaunchDB.getConnection();
int PCID = SmartLaunchDB.getCurrentPCID();
conn.createStatement().execute("LOCK TABLE `licensekeys` WRITE");
String sql = "SELECT * FROM `licensekeys` WHERE `InUseByPC` = 0 AND LicenseSetupID = ? ORDER BY `ID` DESC LIMIT 1";
PreparedStatement statement = conn.prepareStatement(sql);
statement.setInt(1, this.id);
ResultSet results = statement.executeQuery();
if (results.next()) {
int licenseID = results.getInt("ID");
sql = "UPDATE `licensekeys` SET `InUseByPC` = ? WHERE `ID` = ?";
statement = conn.prepareStatement(sql);
statement.setInt(1, PCID);
statement.setInt(2, licenseID);
statement.executeUpdate();
statement.close();
conn.commit();
conn.createStatement().execute("UNLOCK TABLES");
return new LicenseKey(results.getInt("ID"), this, results.getString("LicenseKey"), results.getInt("LicenseKeyType"));
} else {
throw new SmartLaunchException("All licenses of type " + this.name + "are in use");
}
}
根据,锁定的正确语法为:
LOCK TABLES ...
你有
LOCK TABLE ...
但是您没有任何错误检查。因此,您可能无法获得锁,而它会默默地忽略这一点
FWIW,我会将清理代码UNLOCK TABLES、conn.commit等放在finally块中,以确保在发生异常时始终正确清理
事实上,您似乎有可能泄漏数据库连接句柄,如果没有免费许可证,则永远不会释放锁。我建议您只执行一个update语句,并检查更新的行数。我会用psudo代码写出来
int uniqueId = SmartLaunchDB.getCurrentPCID();;
int updatedRows = execute('UPDATE `licensekeys` SET `InUseByPC` = uniqueId WHERE `InUseByPC` NOT null LIMIT1')
if (updatedRows == 1)
SUCCESS
else
FAIL
如果成功,您可以通过选择来获取许可证密钥/ID。您必须做两件事: 将代码包装在事务中,以避免自动提交立即释放锁 使用选择。。。对于更新和mysql,将在提交时为您提供所需的锁
选择。。。FOR UPDATE优于LOCK TABLE,因为它可以通过行级锁定来实现,而不是像通常情况那样自动锁定整个表,OP是个白痴。我发布的代码实际上是有效的,但我刚刚在数据库中发现了一个重复的行-我猜有人错误地输入了同一个许可证两次。这让我相信,我通过引入表锁修复的并发错误仍然没有修复
感谢您的一般建议,我为这个方法引入了更好的异常处理。这也是我注意到的。虽然本手册在别名示例中使用了锁表。是的,我需要检查该语法是否被允许。但是,除此之外,锁获取看起来是正确的。不过,缺少错误检查仍然是一个问题。是的,这看起来很容易成为自动提交错误。如果执行LOCK TABLES语句,并且该语句自动提交,则该表将被锁定一纳秒,然后在事务提交时释放。该语句未显示在已发布的提交中,但SmartLaunchDB.getConnection方法将自动提交设置为false。你是对的,选择更新可能是一个更好的锁定机制,谢谢。但是,这还不足以关闭自动提交。在更新完成之前,您仍然必须清楚地划分您的事务,以使锁有效。建议您为license ID字段指定一个唯一的索引-这将确保您不能输入同一个索引两次,或者在尝试时会出错。DB是第三方产品的一部分。虽然我同意它应该有一个独特的约束条件,但我并不热衷于将手指伸进我没有代码的软件中。我不知道如果有人尝试输入重复数据,SmartLaunch是否会优雅地处理这样的SQL错误。