【问题标题】:Java: Making concurrent MySQL queries from multiple clients synchronisedJava:使来自多个客户端的并发 MySQL 查询同步
【发布时间】:2011-01-15 19:49:19
【问题描述】:

我在一家游戏网吧工作,我们这里有一个系统(智能启动)来跟踪游戏许可证。我编写了一个与该系统接口的程序(实际上,它是后端 MySQL 数据库)。该程序旨在在客户端 PC 上运行,并且 (1) 查询数据库以从可用池中选择未使用的许可证,然后 (2) 将此许可证标记为客户端 PC 正在使用。

问题是,我有一个并发错误。该程序旨在同时在多台机器上启动,当这种情况发生时,一些机器通常会尝试获取相同的许可证。我认为这是因为步骤 (1) 和 (2) 不同步,即一个程序确定许可证 #5 可用并选择它,但在它可以将 #5 标记为正在使用之前,另一台 PC 上的程序的另一个副本尝试获取相同的许可证。

我尝试通过使用事务和表锁定来解决这个问题,但它似乎没有任何区别 - 我这样做对吗?下面是有问题的代码:

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

【问题讨论】:

    标签: java mysql concurrency locking


    【解决方案1】:

    你必须做两件事:

    • 将您的代码包装在事务中(以避免自动提交立即释放锁)
    • 使用 SELECT ... FOR UPDATE 和 mysql 会给你你需要的锁(在提交时释放)

    SELECT ... FOR UPDATE 比 LOCK TABLE 更好,因为它可以通过行级锁定来解决,而不是自动锁定整个表

    【讨论】:

    • 是的,这看起来很容易成为自动提交“错误”。如果您执行“LOCK TABLES”语句,并且它会自动提交,那么该表会被锁定一纳秒,然后在事务提交时释放。
    • 它没有显示在已发布的提交中,但 SmartLaunchDB.getConnection() 方法将 autocommit 设置为 false。没错,选择更新可能是一种更好的锁定机制,谢谢。
    • 好吧,关闭自动提交是不够的。在更新完成之前,您仍然必须清楚地划分您的事务以使锁定有效。
    【解决方案2】:

    根据online manual,正确的加锁语法是:

    LOCK TABLES ...
    

    你有

    LOCK TABLE ...
    

    但是您没有任何错误检查。因此,您可能无法获得锁,而它会默默地忽略它。

    FWIW,我会将您的清理代码(UNLOCK TABLESconn.commit() 等)放在 finally 块中,以确保您在发生异常时始终正确清理。

    事实上,您似乎可能会泄漏数据库连接句柄,并且如果没有免费许可证,则永远不会释放锁。

    【讨论】:

    • 这也是我注意到的。虽然手册在别名示例中使用了 LOCK TABLE。
    • 是的,我需要检查该语法是否真的被允许。除此之外,锁定获取似乎是正确的。不过,缺少错误检查仍然是个问题。
    【解决方案3】:

    我想建议只做一个更新语句并检查更新了多少行。我会用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。

    【讨论】:

      【解决方案4】:

      通常情况下,OP 是个白痴。我发布的代码实际上是有效的,但我刚刚在数据库中发现了一个重复的行——我猜有人错误地输入了同一个许可证两次。这让我相信我已经修复的并发错误(通过引入表锁)仍未修复。

      感谢您的一般建议,我已经为这个方法引入了更好的异常处理。

      【讨论】:

      • 建议您为许可证 ID 字段提供一个唯一索引 - 这将确保您不能两次输入相同的索引,否则会出现错误。
      • 数据库是第三方产品的一部分。虽然我同意它应该有一个独特的约束,但我并不热衷于开始将手指插入我没有代码的软件。如果有人尝试输入重复数据,我不知道 SmartLaunch 是否会优雅地处理此类 SQL 错误。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-23
      • 1970-01-01
      • 2017-03-10
      • 1970-01-01
      • 2022-01-23
      • 1970-01-01
      相关资源
      最近更新 更多