用于用户注册的 MySQL 事务 VS 表锁


MySQL Transaction VS Table Lock for User Registration

我是MySQL的新手,我已经阅读了许多我在这里和网络上找到的谈论交易和表锁定的文章,所以我认为我的问题不应该是多余的。

我正在尝试优化查询,主要用于用户注册和会话。Web应用程序是用PHP/MySQL(i)编写的,我使用的是InnoDB引擎。

我不使用 $_SESSIONS 来存储用户会话,但我在 DB 上使用一个表,在其中存储有关用户会话的一些信息,这些信息通过 cookie 保持活动状态。此方法要求我检查每个用户页面请求在DB上的用户会话。这样做,我还在选择会话查询中加入"用户"表,以便每次都获取新的用户信息。

关于用户注册

查询,我不确定哪种方法更适合:性能,代码质量和安全性,以避免多个用户尝试同时注册相同的用户名/电子邮件时出现问题。

用户表:

user_id (primary, AI)
username (unique)
email (unique)
password
field 1
field 2
...

如果查询失败是因为用户名或电子邮件已被占用,我需要知道已经使用哪一个。

方法 A:锁定用户表

LOCK TABLES users WRITE;
// users provided username and email are already taken?
SELECT COUNT(username) username, (SELECT COUNT(email) FROM users WHERE email = 'batman@heroes.com') email FROM users WHERE username = 'batman'
$res = fetch_array; 
if($res['username'] == 0 && $res['email'] == 0){
   INSERT INTO users (username,email,password) VALUES ('batman','batman@heroes.com','imtheonlytruehero')
   if(affected_rows == 1)
      $username = null;
      $email= null;
      $registration = true;
   }
   else {
      $username = null;
      $email= null;
      $registration = false;
   }
}
else {
   $username = ($res['username'] > 0) ? false : true;
   $email = ($res['email'] > 0) ? false : true;
   $registration = false;
}    
UNLOCK TABLES;

方法 B:具有插入+更新的事务

START TRANSACTION;
INSERT INTO users (username,email,password) VALUES ('batman','0','imtheonlytruehero');
if(affected_rows > 0) {
   $username = true;
   UPDATE users SET email='batman@heroes.com' WHERE username='batman';
   if(affected_rows > 0) {
      $email = true;
      $registration = true;
      COMMIT;
   }
   else {
      $email = false;
      $registration = true;
      ROLLBACK;
   }
}
else {
   $username = false;
   $email = null;
   $registration = false;
   ROLLBACK;
}

我使用方法 A 的问题是:当新用户注册并且他的查询锁定用户表时,当尝试验证他们的会话以导航(记住"会话检查"也加入用户表)时,其他登录用户(假设他们很多)会发生什么?缓慢?超时?或者查询(选择和插入)是否足够轻,不会影响性能?

方法 B 生成死锁的可能性是否很高?

我能做什么?选择此方法之一?混合它们?扔掉一切,重新开始?

我感谢任何帮助。谢谢。

锁定整个表只应在特殊情况下使用,方法 A 会妨碍并发性。假设您在用户名电子邮件上都有唯一的索引方法B可以重写为下面提供的示例。

这种方法应该在 mysql 和 postgresql(伪代码)中的 READ COMMIT 和 REPEATABLE READ 隔离模式下工作:

while (1) {
    START TRANSACTION;
    SELECT * FROM users WHERE username = ? OR email = ?;
    if (something_found) {
        // figure out what was found — email, login or both
        // ...
    } else {
        try {
            INSERT INTO users …;
        } catch (UniqueViolation) {
            // somebody else could have inserted a record with the same
            // username/email after our SELECT query
            ROLLBACK;
            // try again
            continue;
        }           
    }
    COMMIT;
    break;
}

如果可能,应避免锁定表。对于方法 A,没有理由锁定任何内容或使用事务。

这就是处理方法 A 的方式(请注意,如果插入用户数据,则应使用预准备语句)

$result =  $db->query('SELECT COUNT(id) as total FROM USER WHERE username = "batman" or email = "batman@heroes.com"');
$row = $result->fetch_assoc();
if($row['total'] > 0}{
  // redirect back to form, exit here
}
try{
  // no user with email, username exists, so try to insert that user
  $db->query('INSERT INTO USER ...');
  $registration = true;
}
catch(Exception e){
   // If a different user with same username or email registered in 
   // the mean time then redirect to form. 
}