如何检测 2 位管理员是否在 php 中同时更新 table 行

How to detect if 2 admins update a table row at the same time in php

我正在开发的网站有一个管理页面,可以将产品添加到数据库或编辑数据库中的现有产品。如果 2 位管理员想要编辑同一产品,第二位管理员应该会收到一条提醒,该产品已经 been/is 正在更新。当 2 位管理员试图编辑同一产品时,我如何在 php/sql 中检测到?

我还没有真正尝试过任何东西,因为我不知道该尝试什么或从哪里开始。

下面是我的函数如何查找更新数据库中的产品:

//I know this is a VERY unsafe function, this website will never go online!
FUNCTION updateProduct($data) {
  include_once 'dbconn.php';
  try {
    $conn = connectToDb();
    if ($data['imageChanged'] == false) {
      $query = "SELECT img_url FROM products WHERE product_id=".$data['productId'];
      $result = mysqli_query($conn, $query);
      if ($result == false) {
        throw new Exception('Failed to execute: '. $query . 'Error: '. mysqli_error($conn));
      }
      $imgUrl = mysqli_fetch_row($result)[0];
    } else {
      $imgUrl = $data['image'];
    }
    $query2 = "img_url='".$imgUrl."'";
    $query = "UPDATE products
              SET product_name='".$data['productName']."', product_desc='".$data['productDesc']."', price=".$data['price'].", ". $query2 . " 
              WHERE product_id=".$data['productId'];
    $result = mysqli_query($conn, $query);
    if ($result == false) {
      throw new Exception('Failed to execute: '. $query . 'Error: '. mysqli_error($conn));
    }
  } finally {
    mysqli_close($conn);
  }
}

编辑:不需要在进行更新后存储旧数据,也不需要存储正在进行的更新(关于这可能是重复的问题)。我想知道的是:如果 admin_1 正在更新一行,而 admin_2 正在尝试更新同一行,而 admin_1 的事务仍在进行中,如何才能我的脚本检测到正在发生这种情况?

考虑以下几点 1)你想要多个管理员编辑不同的产品 2) 你不希望多个管理员编辑同一个产品

3) 在您的代码中,您将产品保存为单个 table。在真正的电子商务网站中,产品信息被分成几个 tables

要实现这一点,您必须设置应用程序范围 mysql 锁

mysql> select get_lock('product_id_12345', 5);
+---------------------------------+
| get_lock('product_id_12345', 5) |
+---------------------------------+
|                               1 |
+---------------------------------+
1 row in set (0.00 sec)

上面的代码使用 product_id 设置了一个锁,如果其他连接试图使用相同的 product_id 获取锁,您将得到 0 作为响应 - 表明其他用户正在更新同样的product_id

mysql> 
mysql> select IS_FREE_LOCK('product_id_12345');
+----------------------------------+
| IS_FREE_LOCK('product_id_12345') |
+----------------------------------+
|                                0 |
+----------------------------------+
1 row in set (0.00 sec)

mysql> 
mysql> select IS_USED_LOCK('product_id_12345');
+----------------------------------+
| IS_USED_LOCK('product_id_12345') |
+----------------------------------+
|                               46 |
+----------------------------------+
1 row in set (0.00 sec)

mysql> 
mysql> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
|              46 |
+-----------------+
1 row in set (0.00 sec)

示例算法

<?php
ini_set('display_errors', 1);

function updateProduct() {

$user = 'root';
$pass = 'xxx';
$DB = 'test';
$host = 'localhost';

  try 
  {
    $conn = new mysqli($host, $user, $pass, $DB);


    $data['productId'] = 'product_id_12345';
    $data['productName'] = 'test';
    $data['productDesc'] = 'testing';

    $isLockFree = 'select IS_USED_LOCK("'.$data['productId'].'") ';
    $isLockFreeResult = mysqli_query($conn, $isLockFree);
    $row=mysqli_fetch_row($isLockFreeResult);

    if(empty($row[0]))
    {
        $lock = 'select get_lock("'.$data['productId'].'")';
        $result = mysqli_query($conn, $lock);

        echo 'lock established';

        $query = "UPDATE products
                  SET product_name='".$data['productName']."', 
                      product_desc='".$data['productDesc']."', 
                      price=".$data['price']."
                  WHERE 
                  product_id=".$data['productId'];

        $result = mysqli_query($conn, $query);
        if ($result == false) 
        {
          throw new Exception('Failed to execute: '. $query . 'Error: '. mysqli_error($conn));
        }
    }
    else
    {
        echo 'sorry! could not lock. somebody is updating the product info';
    }
  } 
  finally 
  {
    mysqli_close($conn);
  }
}
updateProduct();
?>

这通常是通过添加版本列来完成的,该版本列在每次行更改时更新。在更新行之前,检查该值是否仍然与上次读取该行时相同:

SELECT img_url, version FROM products WHERE product_id = ?;
-- Suppose this returns ('https://example.com/foo.jpg', 1)
-- Remember that the current version is 1.

以后:

BEGIN;

SELECT version FROM products WHERE product_id = ? FOR UPDATE;
-- Check if version is still 1. If it's not, someone modified the row (execute ROLLBACK in this case). 
-- Otherwise:
UPDATE products SET img_url = ?, version = version + 1 WHERE product_id = ?;

COMMIT;

如果出于某种原因交易不可用,另一种方法是使用简单的 compare-and-swap:

UPDATE products SET img_url = ?, version = version + 1 WHERE product_id = ? AND version = 1;

如果更新的行数为零,则该行同时被修改。

可以说,这比 SELECT FOR UPDATE 后跟 UPDATE 稍微快一些。但是,具有冲突版本的行可以实现更丰富的用户反馈。例如,通过作者列,您可以告诉 更新了该行,而不仅仅是它发生了。