Objectify: Handle race condition to prevent duplic

2019-06-01 10:25发布

I'm using Google App Engine and Datastore with objectify.

I'm trying to create an account for a user (Google user), so I need to check if that users exist, and if not, create an account for that user, but I'm facing the fact that sometimes the account is created twice if I spam the createAccount API method

@ApiMethod(name = "account.google.create")
public Account createGoogleAccount(final User user) throws OAuthRequestException {
    if (user == null) {
        throw new OAuthRequestException("createAccount: OAuthRequestException<User is not authenticated>");
    }
    Account alreadyExisting = RObjectifyService.getObjectify().load().type(Account.class).filter("accountId.GOOGLE", user.getUserId()).filter("email", user.getEmail()).first().now();
    if (alreadyExisting != null) {
        throw new OAuthRequestException("createAccount: OAuthRequestException<Account already exist>");
    }
    return RObjectifyService.getObjectify().transactNew(new Work<Account>() {
        @Override
        public Account run() {
            Account account = AccountProvider.createAccountFromGoogleProvider(user);
            RObjectifyService.save(account);
            return account;
        }
    });
}

I read that I should use transactions but I can't because if I do this in the transaction:

RObjectifyService.getObjectify().load().type(Account.class).filter("accountId.GOOGLE", user.getUserId()).filter("email", user.getEmail()).first().now()

I get an error "Only ancestor queries are allowed inside transactions", but I don't see another way to do it

Is this the right way to do it?

Thanks

2条回答
Lonely孤独者°
2楼-- · 2019-06-01 11:23

You need a transaction and you need an entity whose primary key is the value you are trying to make unique (ie the username).

The pattern is a little tricky. There is some discussion of it here. The basic idea in pseudocode is:

  • start transaction
  • load entity with the unique PK
  • if there is an entity
    • abort and return duplicate error
  • else
    • create the entity with the unique PK (+ whatever extra work you need)
    • commit the transaction
    • if the commit fails
      • abort and return duplicate error
    • else
      • everything is great!

You probably don't want to make username the primary key of your User entity, so create a separate Username entity and mix in creation of Username with User in the same transaction. Be sure to leave the Username entity around; that's what guarantees uniqueness.

This problem (uniqueness) is actually one of the more technically challenging problems in a massively distributed system like the GAE datastore. It's simple to solve in a traditional RDBMS only if the traditional RDBMS is a single-master system, with the resulting impact on scalability and fault tolerance. GAE gives you the necessary primitives to enforce clusterwide uniqueness; they just aren't super easy to use.

查看更多
ら.Afraid
3楼-- · 2019-06-01 11:25

The best way in this case for have strong consistency is retrieve the entity by key.

查看更多
登录 后发表回答