【问题标题】:JDBC Lock a row using SELECT FOR UPDATE, doesn't workJDBC 使用 SELECT FOR UPDATE 锁定一行,不起作用
【发布时间】:2025-11-25 14:05:01
【问题描述】:

我在使用 MySQL 的 SELECT .. FOR UPDATE 时遇到问题,这是我要运行的查询:

SELECT * FROM tableName WHERE HostName='UnknownHost' 
        ORDER BY UpdateTimestamp asc limit 1 FOR UPDATE

在此之后,相关线程将执行更新并更改主机名,然后它应该解锁该行。

我正在运行一个多线程的 java 应用程序,所以 3 个线程正在运行这个 SQL 语句,但是当线程 1 运行它时,它不会锁定线程 2 和 3 的结果。因此线程 2 和 3 正在获取相同的结果,他们可以更新同一行。

每个线程都在自己的mysql连接上。

我正在使用 Innodb,事务隔离 = READ-COMMITTED,并且在执行 select for update 之前自动提交已关闭

我可以错过什么吗?或者也许有更好的解决方案? 非常感谢。

代码:

public BasicJDBCDemo()
{
    Le_Thread newThread1=new Le_Thread();
    Le_Thread newThread2=new Le_Thread();
    newThread1.start();
    newThread2.start();         
}

线程:

class Le_Thread extends Thread  
{

    public void run() 
    {
    tring name = Thread.currentThread().getName();
        System.out.println( name+": Debut.");
    long oid=Util.doSelectLockTest(name);
    Util.doUpdateTest(oid,name);        
    }

}

选择:

public  static long doSelectLockTest(String threadName)
  {
    System.out.println("[OUTPUT FROM SELECT Lock ]...threadName="+threadName);
    PreparedStatement pst = null;
    ResultSet rs=null;
    Connection conn=null;
    long oid=0;
    try
    {
     String query = "SELECT * FROM table WHERE Host=? 
                               ORDER BY Timestamp asc limit 1 FOR UPDATE";


      conn=getNewConnection();
      pst = conn.prepareStatement(query);
      pst.setString(1, DbProperties.UnknownHost);
      System.out.println("pst="+threadName+"__"+pst);
      rs = pst.executeQuery();

      if (rs.first())
      {
        String s = rs.getString("HostName");
        oid = rs.getLong("OID");
        System.out.println("oid_oldest/host/threadName=="+oid+"/"+s+"/"+threadName);

      }   

    }
    catch (SQLException ex)
    {
      ex.printStackTrace();
    }
    finally
    {
        DBUtil.close(pst);
        DBUtil.close(rs);
        DBUtil.close(conn);
    }
    return oid;
  }

请帮忙....:

结果:

线程 1:首次亮相。 线程 2:首次亮相。 [选择锁的输出]...threadName=Thread-1 新连接.. [选择锁的输出]...threadName=Thread-2 新连接.. pst=Thread-2: SELECT * FROM b2biCheckPoint WHERE HostName='UnknownHost' ORDER BY UpdateTimestamp asc limit 1 FOR UPDATE pst=Thread-1: SELECT * FROM b2biCheckPoint WHERE HostName='UnknownHost' ORDER BY UpdateTimestamp asc limit 1 FOR UPDATE oid_oldest/host/threadName==1/UnknownHost/Thread-2 oid_oldest/host/threadName==1/UnknownHost/Thread-1 [执行更新] ... oid = 1, thread=Thread-2 新连接.. [执行更新] ... oid = 1, thread=Thread-1 pst_threadname=Thread-2: UPDATE b2bicheckpoint SET HostName='1_host_Thread-2',UpdateTimestamp=1294940161838 where OID = 1 新连接.. pst_threadname=Thread-1: UPDATE b2bicheckpoint SET HostName='1_host_Thread-1',UpdateTimestamp=1294940161853 where OID = 1

【问题讨论】:

  • 你是如何明智地实现这段代码的?

标签: java sql jdbc


【解决方案1】:

您创建的用于更新的连接必须与用于进行更新的连接相同。否则它不是同一个事务的一部分,它会释放锁,所以你的其他线程也开始执行它。所以在你的代码中你需要这样做:

if (rs.first())
  {
    String s = rs.getString("HostName");
    oid = rs.getLong("OID");
    System.out.println("oid_oldest/host/threadName=="+oid+"/"+s+"/"+threadName);

  }   
Util.doUpdateTest(oid,name,conn);
conn.commit();

【讨论】:

  • 您好 Hiro2k,感谢您的回答。我无法更改数据库服务器配置,我的意思是我无法更改 my.ini 文件。但是在 MySQL 手册中,他们说如果设置了 READ-COMMITTED,则只有索引将被锁定,这在我的情况下是可以的,因为 HostName 列是一个索引....
  • 能否把你的示例代码发给我,然后我和我的比较一下?
  • 您好 Rachid,我无法分享代码,但我只记得您可以使用 setTransactionIsolation 更改连接的隔离级别。试一试,看看它是否适合你。
  • 我更改了 my.ini 只是为了进行测试,但它仍然是相同的行为,我的意思是两个线程都可以在同一行上运行 select FOR UPDATE,它们也执行更新。所以两个更新是在同一行上执行的!正如我在控制台中看到的那样: pst_threadname=Thread2_: UPDATE b2bicheckpoint SET HostName='1_host_Thread-2',UpdateTimestamp=1294940161838 where OID = 1 pst_threadname=Thread1_: UPDATE b2bicheckpoint SET HostName='1_host_Thread-1',UpdateTimestamp=1294940161853 where OID = 1
  • 提交释放了对你行的锁定。但是,我刚刚看到了您的代码中进行更新的部分,并且它在自己的方法中,所以这是您的问题!您需要将已创建的连接传递给该方法,而不是创建新的。否则就像你开始了一个新的交易。
【解决方案2】:

您非常困惑,但至少在您编辑后情况看起来更好。有多种方法可以做到这一点,但我发现最好的方法是实际使用 JDBC 的 ResultSet.update* 方法:

首先,您需要准备带有 ResultSet.CONCUR_UPDATABLE 参数的 SELECT ... FOR UPDATE 语句,如下所示:

ps = conn.prepareStatement(query,
                           ResultSet.TYPE_FORWARD_ONLY,
                           ResultSet.CONCUR_UPDATABLE);

然后,您必须使用 ResultSet 实际更新表:

if(rs.next())
{
    rs.updateString(columnIndex, "new_hostname");
    rs.updateRow();
}

第三,您可能需要使用交易,我可以在您的更新中看到。希望您的 DbUtil.close 方法不会抛出任何异常、检查 null 等。此外,如果您的方法变得更复杂,您也应该在其中包含回滚逻辑。

您不应出于任何原因修改my.ini

【讨论】:

    【解决方案3】:

    Select 语句不能生成由任何导致排序的 group 或 order by 操作引起的工作结果集。选择必须在表格上保持一个活动位置,任何排序都不会提供。

    【讨论】: