为数据表开启局部事务后,使用局部事务功能,您可以创建数据范围在一个分区键值内的局部事务并对局部事务中的数据进行读写操作。通过使用局部事务您可以实现单行或多行读写的原子操作。
场景
使用局部事务功能,可以实现单行或多行读写的原子操作,扩展了使用场景。 具体场景样例如下:
简单场景:读-写场景
当需要进行读取-修改-写回(Read-Modify-Write)操作时,您可以选择如下两种方式,但这两种方式有一些限制。
为了解决上述问题,您可以使用局部事务功能实现一个分区键值范围内的通用读取-修改-写回流程。
-
使用StartLocalTransaction接口为分区键值创建一个局部事务,并获取局部事务ID。
-
使用GetRow或GetRange接口获取数据,且在请求中需要带上局部事务ID。
-
在客户端本地修改数据。
-
使用PutRow、UpdateRow、DeleteRow或BatchWriteRow接口将修改后的数据写回,且在请求中需要带上事务ID。
-
使用CommitTransaction提交局部事务。
复杂场景:邮箱场景
使用局部事务功能,您可以实现对同一个用户邮件的原子操作。
为了能正常使用局部事务功能,在一张数据表上创建两张索引表,其主键列请参见下表。下表中使用Type列区分数据表和不同的索引表,不同的索引行使用IndexField列保存不同含义的字段,而数据表无IndexField列。
|
表 |
主键 |
|||
|
UserID |
Type |
IndexField |
MailID |
|
|
数据表 |
用户ID |
"Main" |
N/A |
邮件ID |
|
Folder索引表 |
用户ID |
"Folder" |
$Folder |
邮件ID |
|
SendTime索引表 |
用户ID |
"SendTime" |
$SendTime |
邮件ID |
在邮箱场景中,使用局部事务功能可以完成如下使用场景。
场景一:列出某个用户发送的最近100封邮件
-
使用UserID创建一个局部事务,并获取局部事务ID。
-
使用GetRange接口从SendTime表获取100封邮件,且请求中需要带上局部事务ID。
-
使用BatchGetRow接口从主表获取100封邮件的详细信息,且在请求中需要带上局部事务ID。
-
使用CommitTransaction接口提交局部事务或者使用AbortTransaction丢弃局部事务。
场景二:将某个目录下的所有邮件移到另一个目录下
-
使用UserID创建一个局部事务,并获取局部事务ID。
-
使用GetRange接口从Folder表获取若干封邮件,且在请求中需要带上局部事务ID。
-
使用BatchWriteRow接口对Folder表进行写操作,且在请求中需要带上局部事务ID。
每封邮件对应两行写操作,一行写操作是将对应旧Folder的行删掉,另一行写操作是在对应新Folder中增加一行。
-
使用CommitTransaction接口提交局部事务。
场景三:统计某个目录下已读邮件与未读邮件的数量
-
使用UserID创建一个局部事务,并获取局部事务ID。
-
使用GetRange接口从Folder表获取若干封邮件,且在请求中需要带上局部事务ID。
-
使用BatchGetRow接口从数据表获取每封邮件的已读状态。
-
使用CommitTransaction接口提交局部事务或者使用AbortTransaction接口丢弃局部事务。
此实现流程非最优方案。在此场景中,您可以通过增加新的索引表加速查询操作。使用局部事务后,无需担心数据表与索引表的状态不一致,降低开发难度。例如“统计邮件数量”功能在上面的方案中需要读取很多封邮件,开销较大,您可以使用一个新的索引表保存已读邮件与未读邮件的数量,从而降低开销,加速查询。
前提条件
使用表格存储控制台、Java SDK 5.11.0及以上版本或Go SDK最新版本在创建数据表时开启局部事务。
如果创建数据表时未开启局部事务功能,创建数据表后需要使用该功能,请提交工单进行申请或者加入钉钉群36165029092(表格存储技术交流群-3)进行咨询。
注意事项
-
主键自增列功能和局部事务功能不能同时使用。
-
局部事务通过悲观锁(Pessimistic Lock)实现并发控制。
-
每个局部事务从创建开始生命周期最长为60秒。
如果超过60秒未提交局部事务或丢弃局部事务,则表格存储服务端会认为此局部事务超时,并将局部事务丢弃。
-
如果创建局部事务时超时,则请求可能在表格存储服务端已执行成功,此时请等待该局部事务超时后重新创建。
-
未提交的局部事务可能会失效,如果出现此情况,则需要重试该局部事务内的操作。
-
如果未对局部事务范围内的数据进行写操作,则提交局部事务或丢弃局部事务的操作是等同的。
-
在局部事务中读写数据有如下限制:
-
不能使用局部事务ID访问局部事务范围(即创建时使用的分区键值)以外的数据。
-
同一个局部事务中所有写请求的分区键值必须与创建局部事务时的分区键值相同,读请求则无此限制。
-
一个局部事务同时只能用于一个请求中,在使用局部事务期间,其他使用此局部事务ID的操作均会失败。
-
每个局部事务中两次读写操作的最大间隔为60秒。
如果超过60秒未操作局部事务,则表格存储服务端会认为此局部事务超时,并将局部事务丢弃。
-
每个局部事务中写入的数据量最大为4 MB,按正常的写请求数据量计算规则累加。
-
如果在局部事务中写入了未指定版本号的Cell,则该Cell的版本号会在写入数据时(而非提交局部事务时)由表格存储服务端自动生成,生成规则与正常写入一个未指定版本号的Cell相同。
-
如果BatchWriteRow请求中带有局部事务ID,则此请求中所有行只能操作该局部事务ID对应的表。
-
在使用局部事务期间,对应分区键值的数据会被加上写锁,只有持有局部事务ID在局部事务范围内的写请求才会成功。其他非事务请求或持有其他局部事务ID在局部事务范围内的写请求均会失败。在局部事务提交、丢弃或超时后,对应的锁也会被释放。
-
带有局部事务ID的读写请求失败不会影响局部事务本身的存活情况,您可以指定重试规则进行重试或者主动丢弃当前局部事务。
-
操作步骤
只支持通过SDK方式使用局部事务功能。
使用局部事务时,您需要先创建数据范围在一个分区键值内的局部事务,然后对局部事务中的数据进行读写操作,最后根据实际提交或者丢弃局部事务。此处以Java SDK为例介绍使用局部事务读写数据的操作。
使用局部事务写入一行数据
以下示例用于为表的指定分区键创建一个局部事务后,在局部事务内写入一行数据。
String tableName = "local_tx_demo";
// 1. 为指定分区键值创建局部事务,获取事务 ID
PrimaryKeyBuilder pkBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
pkBuilder.addPrimaryKeyColumn("pk1", PrimaryKeyValue.fromString("pkvalue"));
PrimaryKey partitionKey = pkBuilder.build();
StartLocalTransactionRequest startRequest =
new StartLocalTransactionRequest(tableName, partitionKey);
String txnId = client.startLocalTransaction(startRequest).getTransactionID();
// 2. 在事务内写入一行数据,需指定完整主键并携带事务 ID
PrimaryKeyBuilder rowKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
rowKeyBuilder.addPrimaryKeyColumn("pk1", PrimaryKeyValue.fromString("pkvalue"));
rowKeyBuilder.addPrimaryKeyColumn("pk2", PrimaryKeyValue.fromLong(10001));
PrimaryKey rowKey = rowKeyBuilder.build();
RowPutChange rowPutChange = new RowPutChange(tableName, rowKey);
rowPutChange.addColumn(new Column("col1", ColumnValue.fromString("colvalue")));
rowPutChange.addColumn(new Column("col2", ColumnValue.fromLong(10)));
PutRowRequest putRequest = new PutRowRequest(rowPutChange);
putRequest.setTransactionId(txnId);
client.putRow(putRequest);
// 3. 提交事务使所有写入生效;如需放弃修改,调用 abortTransaction()
CommitTransactionRequest commitRequest = new CommitTransactionRequest(txnId);
client.commitTransaction(commitRequest);
使用局部事务读取一行数据
以下示例用于为表的指定分区键创建一个局部事务后,在局部事务内读取一行数据。
String tableName = "local_tx_demo";
// 1. 为指定分区键值创建局部事务
PrimaryKeyBuilder pkBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
pkBuilder.addPrimaryKeyColumn("pk1", PrimaryKeyValue.fromString("pkvalue"));
PrimaryKey partitionKey = pkBuilder.build();
StartLocalTransactionRequest startRequest =
new StartLocalTransactionRequest(tableName, partitionKey);
String txnId = client.startLocalTransaction(startRequest).getTransactionID();
// 2. 在事务内读取一行数据,需指定完整主键并携带事务 ID
PrimaryKeyBuilder rowKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
rowKeyBuilder.addPrimaryKeyColumn("pk1", PrimaryKeyValue.fromString("pkvalue"));
rowKeyBuilder.addPrimaryKeyColumn("pk2", PrimaryKeyValue.fromLong(10001));
PrimaryKey rowKey = rowKeyBuilder.build();
SingleRowQueryCriteria criteria = new SingleRowQueryCriteria(tableName, rowKey);
criteria.setMaxVersions(1);
GetRowRequest getRequest = new GetRowRequest(criteria);
getRequest.setTransactionId(txnId);
GetRowResponse getResponse = client.getRow(getRequest);
// 3. 提交或丢弃事务。对于只读事务,两者效果相同,均会释放事务
CommitTransactionRequest commitRequest = new CommitTransactionRequest(txnId);
client.commitTransaction(commitRequest);
Row row = getResponse.getRow();
System.out.println(row);
开发集成
您可以通过以下语言的SDK使用局部事务。
计费说明
-
StartLocalTransaction、CommitTransaction和AbortTransaction操作分别消耗1单位写CU。
-
读写操作的计费与正常的读写请求相同。关于计费的更多信息,请参见计费概述。
错误码
|
错误码 |
说明 |
|
OTSRowOperationConflict |
该分区键值已被其他局部事务占用。 |
|
OTSSessionNotExist |
事务ID对应的事务不存在,或该事务已失效或超时。 |
|
OTSSessionBusy |
该事务的上一次请求尚未结束。 |
|
OTSOutOfTransactionDataSizeLimit |
事务内的数据量超过上限。 |
|
OTSDataOutOfRange |
用户操作数据的分区键超出了事务创建的分区键范围。 |