内容提要
对MicrosoftAzure的CloudTable进行操作,有很多种操作失败的可能,比如网络连接异常,比如短时间内发送的请求数太多。很多时候我们在失败之后过一段时间再重试,就能操作成功。但是有些失败不是简单重试就解决的。本文讲述的在修改操作时候发生的PreconditionFailed错误就是一个例子。
问题描述
在上一篇博客里,我们定义一个类型Account,用来模拟社交网站的账户基本信息。这里我们继续用这个类型为例子来讲述多个线程同时发送更新请求时可能发生的问题。
我们首先用如下代码创建10个账户,并且在多个线程里同时把他们互相加为好友
public async Task AddFriend(string email1, string email2) { Account account1 = await GetAccount(email1); Account account2 = await GetAccount(email2); if (account1 == null || account2 == null) { return; } account1.Friends.Add(email2); account2.Friends.Add(email1); await UpdateAccount(account1); await UpdateAccount(account2); }
下面代码实现了把两个账号互相加为好友的功能:
public async Task<Account> GetAccount(string email) { string partition = Account.AccountsPartitionKey; var retrieve = TableOperation.Retrieve<Account>(partition, email); TableResult result = await this.accountsTable.ExecuteAsync(retrieve); Account account = (Account)result.Result; return account; } public async Task UpdateAccount(Account account) { var update = TableOperation.Replace(account); await this.accountsTable.ExecuteAsync(update); }
运行上述代码,将会出现如下错误:
问题根源
从错误信息中,我们可以看到错误代码是412,错误类型是PreconditionFailed。出现这个错误的原因是修改操作所需要的前提条件不满足。
存入CloudTable的每个TableEntity都有一个字段叫ETag,它标识了每个数据的版本。每当一个数据被更新时,ETag字段也会被更新。
在跟新一个数据时,先要把该数据从CloudTable中读出来(函数GetAccount的功能),接着对数据进行修改,最后在把修改之后存入到CloudTable中去(函数UpdateAccount的功能)。在把数据存入到CloudTable之前,Azure会比较数据的ETag和CloudTable里对应的数据的ETag是不是一致。如果不一致,表明CloudTable里的数据已经被其他人修改了。这时Update操作不能再继续,而是抛出代码为412的错误并中断操作。
在这个例子中,实现添加好友的AddFriend函数实际上是一个修改操作。当我们向一个账号添加好友时,他的Friends字段会被修改。
在这个例子中,实现添加好友的AddFriend函数实际上是一个修改操作。当我们向一个账号添加好友时,他的Friends字段会被修改。
出现这个问题是有可能多个线程同时往一个账号里添加好友,也就有可能同时修改一个账号。在一个线程读出一个账号的数据之后并在再次存入CloudTable之前,有可能其他线程已经往该线程里添加了好友,从而导致出错。
解决问题
如前面分析的那样,出现代码为412的错误的原因是在把一个账号的数据存回CloudTable的时候该数据已经被另外一个线程修改了。此时简单地重复执行TableOperation.Replace并不能解决问题,因为此时数据的ETag已经和CloudTable里数据的ETag不一致,再重试也枉然。
正确的方法时我们重新从CloudTable里读出账号数据,再执行修改,然后再存入到CloudTable里去。
如果有两个线程同时往同一个账号里添加好友。一个线程添加好友A,一个线程添加B。假设添加好友A的线程先结束。添加B的线程准备把数据存回CloudTable的时候,就会抛出代码为412的错误。此时我们应该重新从CloudTable里读出账号的数据(包含好友A),然后再加入好友B,接着再存回CloudTable。当操作结束的时候,CloudTable里的数据同时包含了好友A和B。这个结果和我们预期的一致。
修改之后的代码如下所示:
public async Task AddFriendWithRetry(string email1, string email2) { int maxRetry = 10; int retry = 0; int maxDelay = 2000; int minDelay = 10; StorageException storageException = null; while (retry < maxRetry) { try { await AddFriend(email1, email2); break; } catch (StorageException ex) { storageException = ex; } if (storageException != null) { if (storageException.Message.Contains("412")) { retry++; await Task.Delay(new Random().Next(minDelay, maxDelay)); } else { throw storageException; } } } if(retry == maxRetry && storageException != null) { throw storageException; } }
在上述代码中,我们当捕捉到代码为412的异常,就完整里重复执行AddFriend函数。AddFriend函数会重新从CloudTable里读出账号,添加好友,然后再把修改过的账号存回到CloudTable。
Azure编程笔记(2):重复CloudTable的修改操作