Search code examples
mysqlsqlnode.jstypeorm

MySQL deadlock on account registration


I'm trying to implement a feature that detects whether or not an account has already signed up. Now when doing parallel requests I'm getting a deadlock. I think I understand why it is happening but I'm unsure on how to solve it.

Here's a simplified version of what I'm doing;

START TRANSACTION;

-- check if user has already signed up (returned rows > 0, throw error if so)
SELECT * FROM users WHERE email = '[email protected]' FOR UPDATE; 

-- user has not signed up yet.. create the account.
INSERT INTO users SET ...;
COMMIT;

Now this in itself works fine. However when two parallel request happen, a deadlock is made because the transaction will both create a FOR UPDATE lock, which is allowed because initially when there is no account signed up yet there are no rows to lock. Atleast, that's what I think is happening.. correct me if I'm wrong.

I'm curious on how I were to fix this, I still want to check whether not an account has registered already so I can show the user a message. Of course the email has a unique constraint but I do not want to rely on that because the auto increment index will increment, even when it violates the constraint.

Also I'm using typeorm, a sample of my code;

  public async registerUser(email: string, password: string, displayName?: string) {
    const connection = await getConnection();
    connection.transaction(async (manager) => {
      // First we need to make sure that this email isn't already registered. If
      // it has been registered we can throw a simple UserError which will be
      // caught by our error handler.
      const hasAlreadyRegistered = await this.findUser(email, manager);
      if (hasAlreadyRegistered) throw new UserError('Email has already been registered.');

      // At last we can create the user, linking him to the previously created
      // authentication strategy.
      const user = new User();
      user.email = email;
      user.displayName = displayName || randomBytes(8).toString('hex');
      user.strategies = [authentication];
      await manager.save(user);
      logger.silly('> Created user row.');

      return user;
    });
  }

Solution

  • I have solved this by just checking for the constraint error in the end (per suggestion of @Shadow). It saves me a lot of hassle.

    Code

     try {
        await manager.save(user);
      } catch (err: any) {
        // Check whether or not this entry violates an unique constraint.
        if (err.code === 'ER_DUP_ENTRY') {
          throw new UserError('Email has already been registered.');
        } else throw err;
      }